Back to the front page

Session-less Authentication With Ember Simple Auth and Torii

Authentication is one of the things everyone wants to have but nobody has enough time to implement properly. This could be the reason why resources on this topic are very scarce and fragmented, especially those concerning Ember and other client-side frameworks.

The intent of this article is to explain the process of session-less authentication from top to bottom using Ember, Ember Simple Auth and Torii. We will be focusing on integrating with the Google API, but the same process can be used for any OAuth provider out there that supports the one-time token flow.

Flow Overview

The final intent of our flow is to authenticate an user in our API server, so we will assume certain behaviour coming from our API endpoints. I felt like the actual implementation of the endpoints was outside the scope of this article, so I left it up to the reader. At AlphaSights we're using Rails with devise, devise-token_authenticatable and omniauth-google-oauth2.

We will be implementing the One-Time Token Flow, which has many advantages over the traditional server-side flow.

  • It is immune to replay attacks, and conveys no useful information to a man in the middle.
  • It allows us to be fully session-less in the server.
  • All the communication with our server happens through AJAX requests in JSON, instead of the more common usage of redirects and postMessage from the auth popup. This way our back-end doesn't have to deal with HTML rendering.

Once we've obtained the code from Google, we will post it to our server, which is then expected to return a fresh authentication token that we can use to authenticate all our subsequent requests.

Getting Started

Installing

We'll need to add Ember Simple Auth, Torii and ic-ajax to our project.

npm install torii ember-cli-simple-auth-torii ember-cli-ic-ajax --save-dev  

Configuring

After installing the packages, it's necessary to configure them appropriately in our environment.

// config/environment.js

var ENV = {  
  // ...

  torii: {
    providers: {
      'google-api': {
        clientId: 'YOUR_GOOGLE_CLIENT_ID'
      }
    }
  },

  'simple-auth': {
    authenticationRoute: 'session',
    authorizer: 'authorizer:application',
    crossOriginWhitelist: ['http://localhost:3000']
  }
};

In the above snippet, our API server is assumed to be running on http://localhost:3000.

The authenticationRoute is where the app will redirect if it enters an authenticated route without being authenticated. This will be the route where we render the button to initiate the authentication process.

The configuration also assumes the existence of an application authorizer, which is described in the next sections.

Extending Ember Simple Auth

Ember Simple Auth provides many mixins which we can include in our routes and controllers. This will give our application, among other things, an authenticate action that we can access from the session's route template.

// routes/session.js

import Ember from 'ember';  
import LoginControllerMixin from 'simple-auth/mixins/login-controller-mixin';  
import UnauthenticatedRouteMixin from 'simple-auth/mixins/unauthenticated-route-mixin';

export default Ember.Route.extend(LoginControllerMixin, UnauthenticatedRouteMixin);  
// controllers/session.js

import Ember from 'ember';  
import AuthenticationControllerMixin from 'simple-auth/mixins/authentication-controller-mixin';

export default Ember.Controller.extend(AuthenticationControllerMixin, {  
  authenticator: 'authenticator:application'
});

Sign In Page

The template for the session page could be roughly as follows:

<!-- templates/session.hbs -->

<button {{action "authenticate" "google-api"}}>Connect with Google</button>  

The first parameter to the authenticate action is the provider Torii should use to authenticate the user.

The default Torii Google provider is meant to work with the regular token flow. We need to use the one-time token flow, so we'll have to create a google-api provider which does that.

The Torii Provider

Torii is only responsible for fetching the one-time token from Google, but it does not manage a session for us.
Ember Simple Auth will come into play later to do that.

The provider will first load the Google APIs client library for JavaScript and use it to fetch the token with the options configured in the environment.

The open method, which is required for Torii providers, must return a promise that resolves with the code when it's been successfully obtained, or that rejects if there's an error at any point in the process. Our provider will resolve its promise when gapi.auth.authorize succeeds.

// torii-providers/google-api.js

import Ember from 'ember';  
import Provider from 'torii/providers/base';  
import { configurable } from 'torii/configuration';

export default Provider.extend({  
  name: 'google-api',
  scope: configurable('scope', 'https://www.googleapis.com/auth/userinfo.email'),
  clientId: configurable('clientId'),
  loadPromise: null,

  open: function() {
    return this.load().then(this.authorize.bind(this));
  },

  load: function() {
    var loadPromise = this.get('loadPromise');

    if (loadPromise == null) {
      loadPromise = this._load();
      this.set('loadPromise', loadPromise);
    }

    return loadPromise;
  }.on('init'),

  _load: function() {
    var originalOnLoad = window.onGoogleApiLoad;

    return new Ember.RSVP.Promise(function(resolve, reject) {
      window.onGoogleApiLoad = function() {
        window.onGoogleApiLoad = originalOnLoad;

        gapi.auth.init(function() {
          Ember.run(null, resolve);
        });
      };

      Ember.$.getScript('//apis.google.com/js/client.js?onload=onGoogleApiLoad').
        fail(function(_, __, exception) {
          Ember.run(null, reject, exception);
        });
    });
  },

  authorize: function() {
    return new Ember.RSVP.Promise((resolve, reject) => {
      gapi.auth.authorize({
        response_type: 'code',
        client_id: this.get('clientId'),
        scope: this.get('scope')
      }, function(response) {
        if (response != null && response.error != null) {
          Ember.run(null, reject, response.error);
        }
        else {
          Ember.run(null, resolve, response);
        }
      });
    });
  }
});

The Authenticator

Authenticators in Ember Simple Auth are responsible for authenticating the user and storing the auth token inside a client-side session.

Our application authenticator will use Torii to fetch the one-time token and will then post this token to our API server which will answer with
a newly generated access token that we can use to authorize our requests. This is different from the OAuth token returned by Google.

The OAuth callback endpoint (which in our example is /users/auth/google_auth2/callback) should do the following things:

  • Exchange the code for an actual OAuth token, thus verifying its authenticity, using the endpoint provided by Google, which is described in more detail here.
  • Fetch OAuth data for the user.
  • Create/find an user in our database based on the OAuth data.
  • Ensure that an access token for that user is present, by generating a new random one if it's not.
  • Return JSON containing the access token in the access_token key.

Everything returned by the authenticate method will be stored in the client-side session by Ember Simple Auth.
Here we return the access token inside the accessToken property.

Our authenticator can easily be extended to support more Torii providers by just adding the function handling
the server request to the providers hash, which is mapped by provider name.

import Ember from 'ember';  
import ToriiAuthenticator from 'simple-auth-torii/authenticators/torii';  
import config from '../config/environment';  
import { request } from 'ic-ajax';

export default ToriiAuthenticator.extend({  
  providers: {
    'google-api': function(authResponse) {
      return request(`${config.apiBaseUrl}/users/auth/google_oauth2/callback`, {
        data: { code: authResponse.code },
        type: 'POST'
      }).then(function(response) {
        return { accessToken: response.access_token };
      });
    }
  },

  authenticate: function(provider, options) {
    return this.torii.open(provider, options).then((authResponse) => {
      return this.get('providers')[provider](authResponse);
    });
  },

  restore: function(data) {
    return Ember.RSVP.resolve(data);
  }
});

The Authorizer

Authorizers are responsible for authenticating all our AJAX requests from data contained in the client-side
session. Here we will fetch the access token set in the session for us by the authenticator and pass it in our request's Authorization header. Our API server should be able to authenticate a request from this header.

import Base from 'simple-auth/authorizers/base';  
import Ember from 'ember';

export default Base.extend({  
  header: function() {
    return `Token token="${this.get('session.accessToken')}"`;
  }.property('session.accessToken'),

  authorize: function(jqXHR) {
    if (!Ember.isBlank(this.get('session.accessToken'))) {
      jqXHR.setRequestHeader('Authorization', this.get('header'));
    }
  }
});

Initializers

The authentication initializer is needed to inject our custom authorizer and authenticator in the application main container.

The ajax initializer is needed by our back-end to recognize our requests as XHR.

// initializers/authentication.js

import ApplicationAuthenticator from '../authenticators/application';  
import ApplicationAuthorizer from '../authorizers/application';

export default {  
  name: 'authentication',
  before: 'simple-auth',

  initialize: function(container, application) {
    container.register('authorizer:application', ApplicationAuthorizer);
    container.register('authenticator:application', ApplicationAuthenticator);
    application.inject('authenticator:application', 'torii', 'torii:main');
  }
};
// initializers/ajax.js

import Ember from 'ember';

export default {  
  name: 'ajax',

  initialize: function() {
    Ember.$.ajaxPrefilter(function(options) {
      options.headers = {
        'Accept': 'application/json',
        'X-Requested-With': 'XMLHttpRequest'
      };
    });
  }
};

Next steps

At this point, if we visit our session route, we should be presented with a button that authenticates with Google and redirects to the index route at the end of the process.

The next step from here would be to fetch user's data (like name, email, etc.) and store it in a controller. To do that you probably want to return the user ID from the OAuth callback endpoint along with the token.

On GitHub you can find a complete example of the client-side app and the server-side app which also displays the email and name of the user obtained from Google.

Comments

comments powered by Disqus