Chaining Authentication in an Ember App

Mar 22, 2016 using tags emberjs, ember-simple-auth, jwt

This is something that comes in very handy when you need to use multiple authenticators but don’t necessarily care about keeping the state of the first authenticator around. I’ll explain what this means with an example!

Building from the example from GitHub Social Authentication with Ember Simple Auth and Torii, imagine that the mechanism you chose to authenticate requests with your backend was JSON Web Tokens. In the GitHub OAuth flow, the authorizationCode you receive back after the initial redirect needs to be exchanged for an access token. This access token will act as the OAuth Bearer Token for all authenticated GitHub requests.

The problem here is that in order to obtain an access token, one needs to supply GitHub both the client_id and client_secret. The latter is a no-go with client-side javascript apps.

It’s clear that you’ll need a backend here that does the token exchange portion. But how do you verify client sessions using just the GitHub OAuth token? This gets quite hairy. And with a distributed backend that does not necessarily keep state (like Lambda), callbacks become very expensive.

Fortunately JSON Web Tokens work nicely in this setup!

Bigger Picture

  1. Human clicks the “Login With GitHub” button inside your Ember app

  2. A popup appears asking the human to grant your app permission to the human’s GitHub account

  3. GitHub then redirects that request back to your Ember app, with a access_token

  4. The second (chained) authenticator takes this access_token and passes it along to your backend

  5. Your backend validates that this token is legit and returns a JWT back to the Ember app

  6. Any future API requests need to use this JSON Web Token

What does this look like in Ember?

Install the simple auth token addon:

ember install ember-simple-auth-token

From the GitHub Social Authentication with Ember Simple Auth and Torii example, change the login() function in app/controllers/application.js to the following:

login() {
  this.get('session').authenticate('authenticator:torii', 'github');
}

Update app/routes/application.js:

export default Ember.Route.extend(ApplicationRouteMixin, {

  sessionAuthenticated: function() {
    var authorizationCode = this.get('session.data.authenticated.authorizationCode');
    var _this = this;
    var authenticatedRoute = config['ember-simple-auth'].routeAfterAuthentication;
    var payload = {
      password: authorizationCode
    };
    this.get('session').authenticate('authenticator:jwt', payload).then(function() {
      _this.transitionTo(authenticatedRoute);
    });
  }

});

Add the following two environment variables to config/environment.js. Keep in mind that serverTokenEndpoint and serverTokenRefreshEndpoint will need to point to your real backend for production.

ENV['ember-simple-auth'] = {
  authorizer: 'authorizer:token'
};

ENV['ember-simple-auth-token'] = {
  serverTokenEndpoint: '/api/token-auth/',
  identificationField: 'username',
  passwordField: 'password',
  tokenPropertyName: 'token',
  authorizationPrefix: 'Bearer ',
  authorizationHeaderName: 'Authorization',
  headers: {},
  refreshAccessTokens: true,
  serverTokenRefreshEndpoint: '/api/token-refresh/',
  tokenExpireName: 'expires_in',
  refreshLeeway: 300,
  timeFactor: 1000
};

And that’s it! Assuming your backend is in good shape, you should have a functional Ember app authenticated against GitHub and your custom JWT backend!

The above works because Ember Simple Auth triggers the sessionAuthenticated method after the successful (primary) authentication. Without this in place, you potentially end up in a race condition where a view may attempt to use the (JWT) session before it is fully in place.

Testing

Wanna test to make sure that the Ember portion of this setup actually works? Good! Cause Ember’s mock server comes in very handy here.

ember g http-mock users
npm install --save-dev jsonwebtoken

Create the server/mocks/api/token-auth.js file with the following contents:

const util = require('util');

module.exports = function(app) {
  var express = require('express');
  var bodyParser = require('body-parser');
  var jwt = require('jsonwebtoken');
  var apiTokenAuthRouter = express.Router();

  apiTokenAuthRouter.post('/', function(req, res) {
    console.log('body is: ' + util.inspect(req.body));
    jwt.sign(req.body, 'secret', { expiresIn: 10 }, function(token) {
      res.send({
        token: token
      });
    });
  });

  app.use(bodyParser.json());
  app.use('/api/token-auth', apiTokenAuthRouter);
};

Create a server/mocks/api/token-refresh.js

module.exports = function(app) {
  var express = require('express');
  var bodyParser = require('body-parser');
  var jwt = require('jsonwebtoken');
  var apiTokenRefreshRouter = express.Router();

  apiTokenRefreshRouter.post('/', function(req, res) {
    jwt.verify(req.body.token, 'secret', function(err, decoded) {
      if (err) {
        res
          .status(401)
          .send({
            error: err
          });
      } else {
        res.send({
          token: jwt.sign(decoded, 'secret', { expiresIn: 10 })
        });
      }
    });
  });

  app.use(bodyParser.json());
  app.use('/api/token-refresh', apiTokenRefreshRouter);
};

Create a server/mocks/users.js

module.exports = function(app) {
  var express = require('express');
  var jwt = require('jsonwebtoken');
  var usersRouter = express.Router();

  usersRouter.get('/', function(req, res) {
    var authorizationHeader = req.headers.authorization || '';

    var token = authorizationHeader.split('Bearer ')[1];

    if (!token) {
      res.send({}, 200);
      return;
    }

    res.send(jwt.verify(token, 'secret'));
  });

  app.use('/api/users', usersRouter);
};

Kill and restart ember server and you should see all those little JWT requests in the ember console. Neat, huh!

Check out the ember-simple-auth-token project for more details!