Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Enhancements

heartsentwined edited this page · 7 revisions

Enhancements


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.


Remember me

Let's add a standard "remember me" feature to the authentication process. The user would have the option to opt-in to remember me, with the default being off. We would define the remember period as, let's say, 7 days.

First, we need server side API code to enforce these settings. As always, never rely on the client-side ember app to enforce any security.

Devise

rememberable should be already there (it's on by default); if not, enable it and migrate the database.

Review config/initializers/devise.rb. In our case, we would want to modify the remember period.

  config.remember_for = 7.days

API Overview

We would allow the sign in API end point to accept polymorphic params:

  • email and password, or
  • remember_token

It should also accept an optional param remember, which, when true, should remember the user's sign in and return a remember_token.

The sign out API end point should also clear any open remember session.

Remember token

Devise provides a serialize_into_cookie helper that returns the user ID and a "remember token" for cookies. It is in the form of:

[[user_id], token]

(the square brackets actually denote array, not "optional")

So we can string this into a remember token for our API. We will use "-" as the join character in our example app.

Specs

Have you earned bonus credit for DRYing up your specs yet? - as mentioned in the footnote of the Model chapter. The following specs have been DRYed up for readability. If you are not familiar with shared examples, you should read the rspec docs on shared examples.

Here we will use shared examples, otherwise the specs will become too long and repetitive for readiblity.

spec/support/auth_examples.rb:

shared_examples 'http code' do |code|
  it "returns http #{code}" do
    response.response_code.should == code
  end
end

shared_examples 'auth response' do
  context 'remember me on' do
    it_behaves_like 'auth response case' do let(:remember) { true } end
  end

  context 'remember me off' do
    it_behaves_like 'auth response case' do let(:remember) { false } end
  end

  context 'remember me unspecified' do
    it_behaves_like 'auth response case' do let(:remember) { nil } end
  end
end

shared_examples 'auth response case' do
  before do
    if [true, false].include? remember
      params[:remember] = remember
    end
    post 'create', params
  end
  subject { JSON.parse response.body }

  it { should include 'user_id' }
  it { should include 'auth_token' }
  it do
    if remember
      should include 'remember_token'
    else
      should_not include 'remember_token'
    end
  end

  it_behaves_like 'http code', 201
end

spec/controllers/sessions_controller_spec.rb:

require 'spec_helper'

describe SessionsController do
  let(:user) { Fabricate(:user) }

  before do
    @request.env['devise.mapping'] = Devise.mappings[:user];
    user.ensure_authentication_token!
  end

  describe 'POST create' do
    context 'no param' do
      before { post :create }

      it_behaves_like 'http code', 400 # DRYed up
    end

    context 'wrong credentials' do
      before { post :create, email: user.email, password: '' }

      it_behaves_like 'http code', 401 # DRYed up
    end

    context 'normal email + password auth' do
      it_behaves_like 'auth response' do # uses shared example here
        let(:params) { { email: user.email, password: user.password } }
      end
    end

    context 'remember token auth' do
      it_behaves_like 'auth response' do # uses shread example here
        let(:params) do
          user.remember_me!
          data = User.serialize_into_cookie(user)
          token = "#{data.first.first}-#{data.last}"
          { remember_token: token }
        end
      end
    end
  end

  describe 'DELETE destroy' do
    context 'no param' do
      before { delete :destroy }

      it_behaves_like 'http code', 400 # DRYed up
    end

    context 'wrong credentials' do
      before { delete :destroy, auth_token: '' }

      it_behaves_like 'http code', 401 # DRYed up
    end

    context 'normal auth token param' do
      before { delete :destroy, auth_token: user.authentication_token }
      subject { JSON.parse response.body }

      it 'includes user id' do
        should include 'user_id'
      end

      it_behaves_like 'http code', 200 # DRYed up
    end
  end
end

You may DRY up the other specs too, but we won't bother for this example app.

Sessions controller

We now accept polymorphic params, so change the initial param check clause

      return missing_params unless params[:email] && params[:password]

to

      unless (params[:email] && params[:password]) || (params[:remember_token])
        return missing_params
      end

Our user finder is likewise polymorphic, so change the line

      @user = user_from_credentials

to

      @user = if params[:remember_token]
                user_from_remember_token
              else
                user_from_credentials
              end

The actual user_from_remember_token method will utilize serialize_from_cookie from Devise.

    private

    def user_from_remember_token
      id, identifier = params[:remember_token].split '-'
      User.serialize_from_cookie id, identifier
    end

We want to remember the user, and return a remember_token on successful authentication. So add this to the data hash:

      data = { ... } # prev code, abbreviated
      if params[:remember]
        @user.remember_me!
        data[:remember_token] = remember_token
      end

The remember_token method:

    private

    def remember_token
      data = User.serialize_into_cookie @user
      "#{data.first.first}-#{data.last}"
    end

We also want to clear any open remember sessions on sign out. In the destroy method:

      @user.reset_authentication_token! # prev code
      @user.forget_me!

For reference, here is the complete app/controllers/api/sessions_controller.rb:

module Api
  class SessionsController < BaseController
    def create
      unless (params[:email] && params[:password]) || (params[:remember_token])
        return missing_params
      end

      @user = if params[:remember_token]
                user_from_remember_token
              else
                user_from_credentials
              end
      return invalid_credentials unless @user

      @user.ensure_authentication_token!

      data = {
        user_id: @user.id,
        auth_token: @user.authentication_token
      }
      if params[:remember]
        @user.remember_me!
        data[:remember_token] = remember_token
      end

      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!
      @user.forget_me!

      render json: { user_id: @user.id }, status: 200
    end

    private

    def remember_token
      data = User.serialize_into_cookie @user
      "#{data.first.first}-#{data.last}"
    end

    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 user_from_remember_token
      id, identifier = params[:remember_token].split '-'
      User.serialize_from_cookie id, identifier
    end

    def missing_params
      render json: {}, status: 400
    end

    def invalid_credentials
      render json: {}, status: 401
    end
  end
end

Tests should pass. On to the ember app now.

Config

Install the rememberable module, update bundle, and include the module.

Gemfile:

gem 'ember-auth-module-rememberable-rails', '~> 1.0' # remember me
$ bundle update

application.coffee:

# ...
#= require ember-auth
# ...
#= require ember-auth-module-rememberable
# ...

We will need to provide the key for the remember me token, in both the API response and its expected param, and the remember period.

auth.coffee:

EmberAuthRailsDemo.Auth = Em.Auth.create
  # ...
  modules: [
    # ...
    'rememberable'
  ]
  # ...
  rememberable:
    tokenKey: 'remember_token'
    period: 7 # days
    autoRecall: true

Sign in

Modify the auth/sign-in template and controller to include a remember property, and to add this remember property to the params passed to the token API call.

controllers/auth/sign-in.coffee:

EmberAuthRailsDemo.AuthSignInController = Em.Controller.extend
  email: null
  password: null
  remember: false # changed here

  actions:
    signIn: ->
      @auth.signIn
        data:
          email:    @get 'email'
          password: @get 'password'
          remember: @get 'remember' # and here

Add a checkbox, bound to our new remember property, to the template.

templates/auth/sign-in.emblem:

form
  label Email
  = input type='text' value=view.email
  label Password
  = input type='password' value=view.password

  =input type='checkbox' checked=remember
  label Remember me

  button Sign In

There is no need to make any change to userland sign out code.

ember-auth will now add hooks to sign in and out API calls, and set / unset the remember cookie accordingly. It will respect the opt-in remember me by setting the remember cookie only if the token creation API response includes a remember token (and we have configured our SessionsController not to do so unless params[:remember] is true).

That's all for remember me.

Something went wrong with that request. Please try again.