Ember CLI & ember-simple-auth-devise
Step by step guide that shows you how to authenticate to a Rails/Devise server from an Ember CLI app. We will build a Rails project and an Ember project from scratch.
The backend will manage authentication with the gem Devise. The frontend will be able to `login`, to the Rails backend, using the library Ember Simple Auth (ESA) with its Devise extension.
The complete source code for the app is hosted in Github. Check the ember-cli-simple-auth-devise repository for the most up to date code.
Tools needed:
ember -v
version: 0.1.1
node: 0.10.29
npm: 2.1.2
# ember-simple-auth 0.6.7
rails -v
Rails 4.0.2
First step, create a project folder:
mkdir ember-cli-simple-auth-devise
cd ember-cli-simple-auth-devise
Backend
Rails and Devise
Create a Rails project that uses Devise.
rails new my-backend
cd my-backend
# Basic Devise install
echo "gem 'devise'" >> Gemfile
bundle install
rails generate devise:install
rails generate devise User
Configure Devise
Devise dropped token support, so we are adding it back to our backend. We will also add a controller that will handle Ember Simple Auth requests.
rails generate migration add_authentication_token_to_users \
authentication_token:string
rake db:migrate
rails generate controller sessions
Auto-generate an authentication token on model creation.
# my-backend/app/models/user.rb
class User < ActiveRecord::Base
before_save :ensure_authentication_token
# leave the devise line
# devise :database_authenticatable etc.
def ensure_authentication_token
if authentication_token.blank?
self.authentication_token = generate_authentication_token
end
end
private
def generate_authentication_token
loop do
token = Devise.friendly_token
break token unless User.where(authentication_token: token).first
end
end
end
Enable Devise to respond to JSON requests.
# my-backend/app/controllers/sessions_controller.rb
# Note that we are extending
# from: Devise::SessionsController
class SessionsController < Devise::SessionsController
def create
respond_to do |format|
format.html { super }
format.json do
self.resource = warden.authenticate!(auth_options)
sign_in(resource_name, resource)
data = {
user_token: self.resource.authentication_token,
user_email: self.resource.email
}
render json: data, status: 201
end
end
end
end
Use the controller that we just made in the previous step.
# my-backend/config/routes.rb
#replace this line
#devise_for :users
devise_for :users, controllers: { sessions: 'sessions' }
Authenticate users with their email and authentication token.
# my-backend/app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_filter :authenticate_user_from_token!
# leave the line:
# protect_from_forgery
private
def authenticate_user_from_token!
authenticate_with_http_token do |token, options|
user_email = options[:user_email].presence
user = user_email && User.find_by_email(user_email)
if user && Devise.secure_compare(user.authentication_token, token)
sign_in user, store: false
end
end
end
end
Disable sessions and cookies
When using Ember Simple Auth with a Devise backend, it doesn't make sense to keep the server side session active as it will send cookies to the client which are actually redundant when using the authentication token mechanism.
We will disable completely creating sessions. If for some reason you have to keep the session, there are other options, you can read about them here: ember-simple-auth#201.
# my-backend/config/initializers/session_store.rb
Rails.application.config.session_store :disabled
and
#my-backend/app/controllers/application_controller.rb
protect_from_forgery with: :null_session
Dummy data
We will register two users to the database, so we can test logging-in later.
# my-backend/db/seeds.rb
User.create([
{email: 'green@mail.com',
password: '12345678', password_confirmation: '12345678'},
{email: 'pink@mail.com',
password: '12345678', password_confirmation: '12345678'}
])
Load the data and start the server.
# my-backend/
rake db:seed
rails server
Frontend
Ember CLI
Create a very simple Ember application with links to three pages:
- index
- protected
- login
# ember-cli-simple-auth-devise>_
ember new my-frontend
cd my-frontend
ember generate route application
ember generate route protected
ember generate route login
ember generate controller login
ember generate template index
echo "landing page" > app/templates/index.hbs
echo "this is a protected page" > app/templates/protected.hbs
then in my-frontend/app/templates/application.hbs
<h2 id='title'>Frontend</h2>
{{#link-to 'index'}}Home{{/link-to}}
{{#link-to 'protected'}}Protected{{/link-to}}
<hr>
{{outlet}}
You may test the app.
# my-frontend/
ember server
# visit http://0.0.0.0:4200
Ember Simple Auth
Install
We will be using Ember Simple Auth packaged as an ember cli addon.
# my-frontend/
npm install --save-dev ember-cli-simple-auth
npm install --save-dev ember-cli-simple-auth-devise
ember generate ember-cli-simple-auth
ember generate ember-cli-simple-auth-devise
in my-frontend/config/environment.js
module.exports = function(environment) {
ENV['simple-auth'] = {
authorizer: 'simple-auth-authorizer:devise'
};
}
Edit routes
We will use the route mixins provided by Ember Simple Auth.
The ApplicationRouteMixin provides the authenticate and invalidate actions.
// my-frontend/app/routes/application.js
import Ember from 'ember';
import ApplicationRouteMixin from 'simple-auth/mixins/application-route-mixin';
export default Ember.Route.extend(ApplicationRouteMixin);
The AuthenticatedRouteMixin makes a route available only to logged in users.
// my-frontend/app/routes/protected.js
import Ember from 'ember';
import AuthenticatedRouteMixin from 'simple-auth/mixins/authenticated-route-mixin';
export default Ember.Route.extend(AuthenticatedRouteMixin);
Edit controllers
// my-frontend/app/controllers/login.js
import Ember from 'ember';
import LoginControllerMixin from 'simple-auth/mixins/login-controller-mixin';
export default Ember.Controller.extend(LoginControllerMixin, {
authenticator: 'simple-auth-authenticator:devise'
});
Templates and Ember Simple Auth helpers
my-frontend/app/template/application.hbs
{{#if session.isAuthenticated}}
<button {{ action 'invalidateSession' }}>Logout</button>
{{else}}
{{#link-to 'login'}}Login{{/link-to}}
{{/if}}
my-frontend/app/templates/login.hbs
<form {{action 'authenticate' on='submit'}}>
<label for='identification'>Login</label>
{{input id='identification' placeholder='Enter Login'
value=identification}}
<label for='password'>Password</label>
{{input id='password' placeholder='Enter Password'
type='password' value=password}}
<button type='submit'>Login</button>
</form>
Try it out
# my-frontend/
ember server --proxy http://0.0.0.0:3000
# Visit http://0.0.0.0:4200
# Don't forget to have the rails server running too.
That is all, logging-in should be working now.
Errors
This are errors that, if you followed all the instructions, should not be appearing.
Error 422 - Unprocessable Entity
In the console you get a 422 error accompanied by a rails server error:
POST http://localhost:4200/token 422 (Unprocessable Entity)
Can't verify CSRF token authenticity Completed 422 Unprocessable Entity in 29ms
Solution
ember-cli needs to keep track of the CSRF token, this can be solved with rails-csrf. We will make changes to both the backend and the frontend.
Rails
# my-backend>_
rails generate controller api/csrf
my-backend/app/controllers/api/csrf_controller.rb
# add an index action
def index
render json: { request_forgery_protection_token => form_authenticity_token }.to_json
end
my-backend/config/routes.rb
# add a namespace
namespace :api do
get :csrf, to: 'csrf#index'
end
Ember
Install rails-csrf
# my-frontend/
bower install rails-csrf#0.0.5 --save
my-frontend/Brocfile.js
app.import('vendor/rails-csrf/dist/named-amd/main.js', {
'rails-csrf': [
'service'
]
});
my-frontend/app/app.js
+ loadInitializers(App, 'rails-csrf');
export default App;
my-frontend/app/routes/application.js
- export default Ember.Route.extend(ApplicationRouteMixin);
+ export default Ember.Route.extend(ApplicationRouteMixin, {
+ beforeModel: function () {
+ this._super.apply(this, arguments);
+ return this.csrf.fetchToken();
+ }
+ });
my-frontend/config/environment.js
module.exports = function(environment) {
var ENV = {
+ railsCsrf: {
+ csrfURL: 'api/csrf'
+ }
};
};
Everything should be working now, start both servers.
Error 404
POST http://localhost:4200/token 404 (Not Found)
# chances are you didn't specify a proxy when starting the ember server
Error 500
POST http://localhost:4200/token 500 (Internal Server Error)
# chances are you specified the wrong proxy when starting the ember server
Error 408 or 500 - Time out
In the console you get a 408 error:
POST http://localhost:4200/token 408 (Request Time-out)
Or a 500 error accompanied by an ember server error:
POST http://localhost:4200/token 500 (Internal Server Error)
Error: socket hang up
at createHangUpError
Solution
in my-frontend/server/index.js
//app.use(bodyParser());
This error appeared in previous versions of ember-cli, but no longer does.
To know more see
ember-cli#723.