How to implement token-based authentication between Ember.js and Rails using Devise backend

This post is more as a helpful reminder to myself, because this is the third time around I’ve had to implement this.

Here is the Rails code necessary in order to get seamless authentication working using ember-simple-auth’s Devise backend.

RAILS

# app/controllers/api_controller.rb
class ApiController < ActionController::Base
  attr_reader :current_user
  respond_to :json
  before_action :authenticate_user_from_token!
  protect_from_forgery with: :null_session

  private

  def authenticate_user_from_token!
    authenticated = authenticate_with_http_token do |user_token, options|
      user_email = options[:email].presence
      user       = user_email && User.find_by_email(user_email)

      if user && Devise.secure_compare(user.authentication_token, user_token)
        @current_user = user
        sign_in user, store: false
      else
        render json: { errors: ['Invalid authorization.'] }, status: :unauthorized
      end
    end

    unless authenticated
      render json: { errors: ['No authorization provided.'] }, status: :unauthorized
    end
  end
end
# app/controllers/api/sessions_controller.rb
class Api::SessionsController < Devise::SessionsController
  respond_to :json

  # POST /api/users/sign_in
  def create
    respond_to do |format|
      format.json do
        self.resource = warden.authenticate!(auth_options)
        data = {
          user_id: resource.id,
          token: resource.authentication_token,
          email: resource.email
        }
        render json: data, status: :created
      end
    end
  end
end
# app/models/user.rb
class User < ActiveRecord::Base
  before_save :ensure_authentication_token

  devise :database_authenticatable, :recoverable, :trackable, :validatable

  # Generate a token for this user if one does not already exist
  def ensure_authentication_token
    if authentication_token.blank?
      self.authentication_token = generate_authentication_token
    end
  end

  # Identical to above except it saves the user
  def ensure_authentication_token!
    ensure_authentication_token
    save
  end

  # Forces a new authentication token to be generated for this user and saves it
  # to the database
  def reset_authentication_token!
    self.authentication_token = generate_authentication_token
    save
  end

  private

  def generate_authentication_token
    loop do
      token = Devise.friendly_token
      break token unless User.find_by(authentication_token: token)
    end
  end
end

# config/routes.rb
Rails.application.routes.draw do
  namespace 'api', constraints: { format: 'json' } do
    devise_for :users, singular: 'user', module: 'api', controllers: { sessions: 'api/sessions' }
  end
end

And here is the Ember code counterpart:

EMBER

// app/adapters/application.js (if using Active Model Adapter)
import ActiveModelAdapter from 'active-model-adapter';
import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin';

export default ActiveModelAdapter.extend(DataAdapterMixin, {
  namespace: 'api',
  authorizer: 'authorizer:devise'
});

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

export default DeviseAuthenticator.extend({
  serverTokenEndpoint: '/api/users/sign_in'
});
// app/authorizors/devise.js
import DeviseAuthorizer from 'ember-simple-auth/authorizers/devise';

export default DeviseAuthorizer.extend();

// 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/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', EMAIL, PASSWORD).catch((reason) => {
        this.set('errorMessage', reason.error);
      });
    }
  }
});
// app/routes/some_authenticated_route
import Ember from 'ember';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';

export default Ember.Route.extend(AuthenticatedRouteMixin);
// app/routes/some_unauthenticated_route
import Ember from 'ember';
import UnauthenticatedRouteMixin from 'ember-simple-auth/mixins/unauthenticated-route-mixin';

export default Ember.Route.extend(UnauthenticatedRouteMixin);
// config/environment.js
module.exports = function(environment) {
  var ENV = {};

  ENV['ember-simple-auth'] = {
    authorizer: 'authorizer:devise',
    crossOriginWhitelist: ['*']
  }

  return ENV;
};