November 29, 2015

In response to a number of concerns to my old post about token auth with Ember and Rails, I’ve decided to make this post using up-to-date software.

Working code can be found here.

Let’s do part of the front-end first (using the ember-cli)

ember new client

Make sure you update your package.json and bower.json to include the latest libraries (ember, ember-data, etc.).

Then we’ll install ember-simple-auth

ember install ember-simple-auth

Let’s create a route that requires authentication:

ember g route protected

Open the protected route and add the AuthenticatedRouteMixin, so that only authenticated sessions can activate it.

//app/routes/protected.js
import Ember from 'ember';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';

export default Ember.Route.extend(AuthenticatedRouteMixin);
<!-- app/templates/protected.hbs -->
<div>This is protected!</div>

While we’re at it, let’s add ApplicationRouteMixin to the application route. This mixin will automatically handle the events that are fired when a user signs in and out.

//app/routes/application.js
import Ember from 'ember';
import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';

export default Ember.Route.extend(ApplicationRouteMixin);

Let’s make a login route with a simple login form:

ember g route login
<!-- app/templates/login.hbs -->
<form {{action 'authenticate' on="submit"}} >
Email: {{input value=email type="text"}}
Password: {{input value=password type="password"}}
<button type="submit">Submit</button>
</form>
//app/routes/login.js
import Ember from 'ember';
import UnauthenticatedRouteMixin from 'ember-simple-auth/mixins/unauthenticated-route-mixin';

export default Ember.Route.extend(UnauthenticatedRouteMixin);

Lastly, let’s make an application controller and template to hold the invalidation logic:

//app/controllers/application.js
import Ember from 'ember';

export default Ember.Controller.extend({
  session: Ember.inject.service('session'),

  actions: {
    invalidateSession(){
      this.get('session').invalidate();
    }
  }
});
<!-- app/templates/application.hbs -->
<h2 id="title">Welcome to Ember</h2>

{{#if session.isAuthenticated}}
  {{#link-to 'protected'}}Protected{{/link-to}}
  <button {{action 'invalidateSession'}}>Logout</button>
{{else}}
  {{#link-to 'login'}}Login{{/link-to}}
{{/if}}

{{outlet}}

Note: the session service needs to be injected into any controller whose associated template calls the session service.

We’ll go back and implement the actual authentication later, after we write some Rails. I’m going to use the Rails API gem (although you could get bleeding-edge Rails and use the API flag).

gem install rails-api
rails-api new back

Add the devise_token_auth gem (you might also need to add the omniauth gem) to the Gemfile, run bundle install then rails-api g devise_token_auth:install User users to create the User model and mount the auth routes at /users. To make things easy, remove :confirmable from the User model. Also, set config.change_headers_on_each_request = false in config/initializers/devise_token_auth. This saves us from having to keep track of the tokens on each API request on the Ember side.

While devise_token_auth exposes registration routes, I’ll just be doing login routes. So, let’s create a user using the Rails console:

User.create({email: '[email protected]', password: 'password'})

Going back to Ember, we need to create an authenticator that we can use. ember-simple-auth comes with a DeviseAuthenticator, but it needs to be extended to work with devise-token-auth.

//app/authenticators/devise.js
import DeviseAuthenticator from 'ember-simple-auth/authenticators/devise';
import Ember from 'ember';

const { RSVP, isEmpty, run } = Ember;

export default DeviseAuthenticator.extend({
  restore(data){
    return new RSVP.Promise((resolve, reject) => {
      if (!isEmpty(data.accessToken) && !isEmpty(data.expiry) &&
          !isEmpty(data.tokenType) && !isEmpty(data.uid) && !isEmpty(data.client)) {
        resolve(data);
      } else {
        reject();
      }
    });
  },

  authenticate(identification, password) {
    return new RSVP.Promise((resolve, reject) => {
      const { identificationAttributeName } = this.getProperties('identificationAttributeName');
      const data         = { password };
      data[identificationAttributeName] = identification;

      this.makeRequest(data).then(function(response, status, xhr) {
        //save the five headers needed to send to devise-token-auth
        //when making an authorized API call
        var result = {
          accessToken: xhr.getResponseHeader('access-token'),
          expiry: xhr.getResponseHeader('expiry'),
          tokenType: xhr.getResponseHeader('token-type'),
          uid: xhr.getResponseHeader('uid'),
          client: xhr.getResponseHeader('client')
        };

        run(null, resolve, result);
      }, function(xhr) {
        run(null, reject, xhr.responseJSON || xhr.responseText);
      });
    });
  },
});

Now, create a login controller using ember g controller login. This will handle the authenticate action called from the login template that we wrote above.

//app/controllers/login.js
import Ember from 'ember';

export default Ember.Controller.extend({
  session: Ember.inject.service('session'),

  actions: {
    authenticate(){
      this.get('session').authenticate('authenticator:devise', this.get('email'), this.get('password'));
    }
  }
});

Start up Rails and Ember (ember s --proxy localhost:3000) and go to localhost:4200. You should see something like this:

Navigate to the login route, and enter the credentials for the user above

Assuming the authentication works, you should be able to to go to the protected route!

While this tutorial shows a very simple login system, it is missing some key things:

  • Registration endpoints exist, but you can create Ember routes for them easily; you can even re-use some of the code above.
  • The invalidate() method on the authenticator should be extended to call /users/sign_out in order to invalidate the token. While they do expire every 2 weeks by default, it’s good practice to invalidate a token when the user signs out.
  • You can alter the restore() method to manually check the expiry date on the client-side token and return false if it’s expired to invalidate the session.
  • You can add a global AJAX handler to invalidate the session when a 401/Unauthorized response is returned.

As always, read the docs and code!

blog comments powered by Disqus