-
Notifications
You must be signed in to change notification settings - Fork 16
Devise
CHANGES This page has been updated for the 9.x
branch.
Legacy instructions are gone - clone this wiki repo and checkout the 8.x
tag.
Deprecation: Devise 3.1 has deprecated token_authenticatable
itself.
This demo will work fine with previous versions.
Read the explanation, and this temporary fix.
Create a user
devise model.
Devise will, by default, use email
as the identifier.
$ rails g devise user
The user
model should validate the presence of the email
field.
In spec/models/user_spec.rb
:
require 'spec_helper'
describe User do
it { should validate_presence_of :email }
end
Start Guard
, and watch your tests fail.
$ guard
Modify the migration file to enable token_authenticatable
specific fields.
db/migrate/(timestamp)_devise_create_users.rb
:
class DeviseCreateUsers < ActiveRecord::Migration
def change
create_table(:users) do |t|
# ...
## Token authenticatable
t.string :authentication_token
# ...
end
# ...
add_index :users, :authentication_token, :unique => true
end
end
Migrate the database, and prepare your test database.
$ rake db:migrate db:test:prepare
Modify the user
model. app/models/user.rb
:
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable,
:token_authenticatable
validates :email, presence: true
end
We have added :token_authenticatable
to the list of devise modules, added
the email
field presence validation, and removed some clutter.
Tests should pass now.
We expect to have a POST create
and a DELETE destroy
route, as #
and sign out API end points respectively. We will also wrap all our json
api inside the /api
namespace.
spec/controllers/api/sessions_controller_spec.rb
:
require 'spec_helper'
describe Api::SessionsController do
describe 'POST create' do
end
describe 'DELETE destroy' do
end
end
We need to mock the user
model, so make a Fabricator
.
spec/fabricators/user_fabricator.rb
:
Fabricator(:user) do
email { sequence(:email) { |i| "foo#{i}@example.com"} }
password 'foobarbaz'
end
Add the mock user
to the specs.
spec/controllers/api/sessions_controller_spec.rb
:
let(:user) { Fabricate(:user) }
We want to ensure that the user
has an authentication token.
before { user.ensure_authentication_token! }
The # API should accept email
and password
as params.
On successful authentication:
- it should return a
201 Created
status code - it should return a JSON response, with the following fields:
-
auth_token
: the authentication token -
user_id
: the corresponding authenticated user
-
Error handling:
- missing params: it should return a
400 Bad Request
status code - wrong params: it should return a
401 Unauthorized
status code
describe 'POST create' do
context 'no param' do
before { post :create }
it 'returns http 400' do
response.response_code.should == 400
end
end
context 'wrong credentials' do
before { post :create, email: user.email, password: '' }
it 'returns http 401' do
response.response_code.should == 401
end
end
context 'normal email + password auth' do
before { post :create, email: user.email, password: user.password }
subject { JSON.parse response.body }
it { should include 'user_id' }
it { should include 'auth_token' }
it 'returns http 201' do
response.response_code.should == 201
end
end
end
The sign out API should accept auth_token
as param.
On successful sign out:
- it should return a
200 OK
status code - it should return a JSON response, with the following field:
-
user_id
: the corresponding authenticated user
-
Error handling:
- missing params: it should return a
400 Bad Request
status code - wrong params: it should return a
401 Unauthorized
status code
describe 'DELETE destroy' do
context 'no param' do
before { delete :destroy }
it 'returns http 400' do
response.response_code.should == 400
end
end
context 'wrong credentials' do
before { delete :destroy, auth_token: '' }
it 'returns http 401' do
response.response_code.should == 401
end
end
context 'normal auth token param' do
before { delete :destroy, auth_token: user.authentication_token }
subject { JSON.parse response.body }
it { should include 'user_id' }
it 'returns http 200' do
response.response_code.should == 200
end
end
end
For reference, a complete spec/controllers/api/sessions_controller_spec.rb
:
require 'spec_helper'
describe Api::SessionsController do
let(:user) { Fabricate(:user) }
before { user.ensure_authentication_token! }
describe 'POST create' do
context 'no param' do
before { post :create }
it 'returns http 400' do
response.response_code.should == 400
end
end
context 'wrong credentials' do
before { post :create, email: user.email, password: '' }
it 'returns http 401' do
response.response_code.should == 401
end
end
context 'normal email + password auth' do
before { post :create, email: user.email, password: user.password }
subject { JSON.parse response.body }
it { should include 'user_id' }
it { should include 'auth_token' }
it 'returns http 201' do
response.response_code.should == 201
end
end
end
describe 'DELETE destroy' do
context 'no param' do
before { delete :destroy }
it 'returns http 400' do
response.response_code.should == 400
end
end
context 'wrong credentials' do
before { delete :destroy, auth_token: '' }
it 'returns http 401' do
response.response_code.should == 401
end
end
context 'normal auth token param' do
before { delete :destroy, auth_token: user.authentication_token }
subject { JSON.parse response.body }
it { should include 'user_id' }
it 'returns http 200' do
response.response_code.should == 200
end
end
end
end
We want to limit controller access to JSON only, so we force the format of all
request
s to json
, and respond_to
json
only. This behavior would be
common across all our apis, so let's group them in an Api::BaseController
.
app/controllers/api/base_controller.rb
:
module Api
class BaseController < ApplicationController
respond_to :json
before_action :default_json
protected
def default_json
request.format = :json if params[:format].nil?
end
end
end
Our SessionsController
will extend from our BaseController
.
Create a scaffold app/controllers/api/sessions_controller.rb
:
module Api
class SessionsController < BaseController
def create
end
def destroy
end
end
end
Modify the routes. First we move the devise_for :users
declaration into
the :api
namespace
.
routes.rb
:
EmberAuthRailsDemo::Application.routes.draw do
namespace :api do
devise_for :users
end
end
We only want a subset of devise routes, so we'll default to generating no route from devise:
namespace :api do
devise_for :users, only: []
end
Add back our session routes.
namespace :api do
# ...
devise_scope :user do
post 'sign_in' => 'sessions#create'
delete 'sign_out' => 'sessions#destroy'
end
end
For reference, a full config/routes.rb
:
EmberAuthRailsDemo::Application.routes.draw do
namespace :api do
devise_for :users, only: []
devise_scope :user do
post 'sign_in' => 'sessions#create'
delete 'sign_out' => 'sessions#destroy'
end
end
end
First we need to find the user with the supplied credentials.
We will use User::find_for_database_authentication
in conjunction with
User#valid_password?
to authenticate the user.
def create
@user = user_from_credentials
end
private
def user_from_credentials
if user = User.find_for_database_authentication(email: params[:email])
if user.valid_password? params[:password]
user
end
end
end
end
We will then use User#ensure_authentication_token!
to generate an auth token
for the user if it doesn't already have one. Rendering a response:
def create
@user = user_from_credentials # prev code
@user.ensure_authentication_token!
data = {
user_id: @user.id,
auth_token: @user.authentication_token
}
render json: data, status: 201
end
The sign out method is simpler. We will use the standard
ActiveRecord
method of find_by_*
to retrieve our user,
and user.reset_authentication_token!
to clear the auth token.
def destroy
@user = User.find_by authentication_token: params[:auth_token]
@user.reset_authentication_token!
render json: { user_id: @user.id }, status: 200
end
Define helpers for our 400 Bad Request
and 401 Unauthorized
responses.
private
def missing_params
render json: {}, status: 400
end
def invalid_credentials
render json: {}, status: 401
end
Add guard clauses against missing params.
def create
return missing_params unless (params[:email] && params[:password])
# ...
end
def destroy
return missing_params unless params[:auth_token]
# ...
end
Add guard clauses against invalid params.
def create
# ...
@user = user_from_credentials # prev
return invalid_credentials unless @user
# ...
end
def destroy
# ...
@user = User.find_by authentication_token: params[:auth_token] # prev
return invalid_credentials unless @user
# ...
end
The completed SessionsController
:
module Api
class SessionsController < BaseController
def create
return missing_params unless (params[:email] && params[:password])
@user = user_from_credentials
return invalid_credentials unless @user
@user.ensure_authentication_token!
data = {
user_id: @user.id,
auth_token: @user.authentication_token
}
render json: data, status: 201
end
def destroy
return missing_params unless params[:auth_token]
@user = User.find_by authentication_token: params[:auth_token]
return invalid_credentials unless @user
@user.reset_authentication_token!
render json: { user_id: @user.id }, status: 200
end
private
def user_from_credentials
if user = User.find_for_database_authentication(email: params[:email])
if user.valid_password? params[:password]
user
end
end
end
def missing_params
render json: {}, status: 400
end
def invalid_credentials
render json: {}, status: 401
end
end
end
Tests should pass.
Continue to Model.