Devise Token Auth

Gem Version Build Status Code Climate Test Coverage Dependency Status

This gem provides simple, secure token based authentication.

This gem was designed to work with the venerable ng-token-auth module for angular.js.

Demo

Here is a demo of this app running with the ng-token-auth module.

The fully configured api used in the demo can be found here.

Dependencies

This project leverages the following gems:

Installation

Add the following to your Gemfile:

gem devise_token_auth

Then install the gem using bundle:

bundle install

Configuration TL;DR

You will need to create a user model, define routes, include concerns, and you may want to alter some of the default settings for this gem. Run the following command for an easy one-step installation:

rails g devise_token_auth:install [USER_CLASS] [MOUNT_PATH]

This generator accepts the following optional arguments:

Argument Default Description
USER_CLASS User The name of the class to use for user authentication.
MOUNT_PATH auth The path at which to mount the authentication routes. Read more.

The following events will take place when using the install generator:

  • An initializer will be created at config/initializers/devise_token_auth.rb. Read more.

  • A model will be created in the app/models directory. If the model already exists, a concern will be included at the top of the file. Read more.

  • Routes will be appended to file at config/routes.rb. Read more.

  • A concern will be included by your application controller at app/controllers/application_controller.rb. Read more.

  • A migration file will be created in the db/migrate directory. Inspect the migrations file, add additional columns if necessary, and then run the migration:

  rake db:migrate

You may also need to configure the following items:

  • OmniAuth providers when using 3rd party oauth2 authentication. Read more.
  • Cross Origin Request Settings when using cross-domain clients. Read more.
  • Email when using email registration. Read more.
  • Multiple model support may require additional steps. Read more.

Jump here for more configuration information.

Usage TL;DR

The following routes are available for use by your client. These routes live relative to the path at which this engine is mounted (/auth by default). These routes correspond to the defaults used by the ng-token-auth module for angular.js.

path method purpose
/ POST email registration. accepts email, password, and password_confirmation params.
/sign_in POST email authentication. accepts email and password as params.
/sign_out DELETE invalidate tokens (end session)
/:provider GET set this route as the destination for client authentication. ideally this will happen in an external window or popup. Read more.
/:provider/callback GET/POST destination for the oauth2 provider's callback uri. postMessage events containing the authenticated user's data will be sent back to the main client window from this page. Read more.
/validate_token POST use this route to validate tokens on return visits to the client. accepts uid and auth_token as params. these values should correspond to the columns in your User table of the same names.
/password POST send password email to users that registered by email. accepts email and redirect_url as params. The user matching the email param will be sent instructions on how to reset their password. redirect_url is the url to which the user will be redirected after visiting the link contained in the email.
/password PUT password change for users that registered by email. accepts password and password_confirmation as params.
/password/edit GET verify user by password reset token. must contain reset_password_token and redirect_url as params. These values will be set automatically by the confirmation email that is generated by the password reset request.

Jump here for more usage information.

Configuration cont.

Initializer settings

The following settings are available for configuration in config/initializers/devise_token_auth.rb:

Name Default Description
change_headers_on_each_request true By default the authorization headers will change after each request. The client is responsible for keeping track of the changing tokens. The ng-token-auth module for angular.js does this out of the box. While this implementation is more secure, it can be difficult to manage. Set this to false to prevent the Authorization header from changing after each request. Read more.
token_lifespan 2.weeks Set the length of your tokens' lifespans. Users will need to re-authenticate after this duration of time has passed since their last login.
batch_request_buffer_throttle 5.seconds Sometimes it's necessary to make several requests to the API at the same time. In this case, each request in the batch will need to share the same auth token. This setting determines how far apart the requests can be while still using the same auth token. Read more.
omniauth_prefix "/omniauth" This route will be the prefix for all oauth2 redirect callbacks. For example, using the default '/omniauth' setting, the github oauth2 provider will redirect successful authentications to '/omniauth/github/callback'. Read more.

OmniAuth authentication

If you wish to use omniauth authentication, add all of your desired authentication provider gems to your Gemfile.

OmniAuth example using github, facebook, and google:

gem 'omniauth-github'
gem 'omniauth-facebook'
gem 'omniauth-google-oauth2'

Then run bundle install.

List of oauth2 providers

OmniAuth provider settings

In config/initializers/omniauth.rb, add the settings for each of your providers.

These settings must be obtained from the providers themselves.

Example using github, facebook, and google:

# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :github,        ENV['GITHUB_KEY'],   ENV['GITHUB_SECRET'],   scope: 'email,profile'
  provider :facebook,      ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET']
  provider :google_oauth2, ENV['GOOGLE_KEY'],   ENV['GOOGLE_SECRET']
end

The above example assumes that your provider keys and secrets are stored in environmental variables. Use the figaro gem (or dotenv or secrets.yml or equivalent) to accomplish this.

OmniAuth callback settings

The "Callback URL" setting that you set with your provider must correspond to the omniauth prefix setting defined by this app. This will be different than the omniauth route that is used by your client application.

For example, the demo app uses the default omniauth_prefix setting /omniauth, so the "Authorization callback URL" for github must be set to "http://devise-token-auth-demo.herokuapp.com**/omniauth**/github/callback".

Github example for the demo site: password reset flow

The url for github authentication will be different for the client. The client should visit the API at /[MOUNT_PATH]/:provider for omniauth authentication.

For example, given that the app is mounted using the following settings:

# config/routes.rb
mount_devise_token_auth_for 'User', at: '/auth'

The client configuration for github should look like this:

Angular.js setting for authenticating using github:

angular.module('myApp', ['ng-token-auth'])
  .config(function($authProvider) {
    $authProvider.configure({
      apiUrl: 'http://api.example.com'
      authProviderPaths: {
        github: '/auth/github' // <-- note that this is different than what was set with github
      }
    });
  });

This incongruence is necessary to support multiple user classes and mounting points.

Note for pow and xip.io users

If you receive redirect-uri-mismatch errors from your provider when using pow or xip.io urls, set the following in your development config:

# config/environments/development.rb

# when using pow
OmniAuth.config.full_host = "http://app-name.dev"

# when using xip.io
OmniAuth.config.full_host = "http://xxx.xxx.xxx.app-name.xip.io"

Email authentication

If you wish to use email authentication, you must configure your Rails application to send email. Read here for more information.

I recommend using mailcatcher for development.

mailcatcher development example configuration:
# config/environments/development.rb
Rails.application.configure do
  config.action_mailer.default_url_options = { :host => 'your-dev-host.dev' }
  config.action_mailer.delivery_method = :smtp
  config.action_mailer.smtp_settings = { :address => 'your-dev-host.dev', :port => 1025 }
end

CORS

If your API and client live on different domains, you will need to configure your Rails API to allow cross origin requests. The rack-cors gem can be used to accomplish this.

The following dangerous example will allow cross domain requests from any domain. Make sure to whitelist only the needed domains.

Example rack-cors configuration:
# gemfile
gem 'rack-cors', :require => 'rack/cors'

# config/application.rb
module YourApp
  class Application < Rails::Application
    config.middleware.use Rack::Cors do
      allow do
        origins '*'
        resource '*',
          :headers => :any,
          :expose => ['Authorization'], # <-- important!
          :methods => [:get, :post, :options, :delete, :put]
      end
    end
  end
end

Make extra sure that the Access-Control-Expose-Headers includes Authorization (as is set in the example above by the:expose param). If your client experiences erroneous 401 responses, this is likely the cause.

CORS may not be possible with older browsers (IE8, IE9). I usually set up a proxy for those browsers. See the ng-token-auth readme for more information.

Usage cont.

Mounting Routes

The authentication routes must be mounted to your project. This gem includes a route helper for this purpose:

mount_devise_token_auth_for - similar to devise_for, this method is used to append the routes necessary for user authentication. This method accepts the following arguments:

Argument Type Default Description
class_name string 'User' The name of the class to use for authentication. This class must include the model concern described here.
options object '/auth' The routes to be used for authentication will be prefixed by the path specified in the at param of this object.

Example:

# config/routes.rb
mount_devise_token_auth_for 'User', at: '/auth'

Any model class can be used, but the class will need to include DeviseTokenAuth::Concerns::SetUserByToken for authentication to work properly.

You can mount this engine to any route that you like. /auth is used by default to conform with the defaults of the ng-token-auth module.

Controller Concerns

DeviseTokenAuth::Concerns::SetUserByToken

This gem includes a Rails concern called DeviseTokenAuth::Concerns::SetUserByToken. This concern can be used in controllers to identify users by their Authorization header.

This concern runs a before_action, setting the @user variable for use in your controllers. The user will be signed in via devise for the duration of the request.

The concern also runs an after_action that changes the auth token after each request.

It is recommended to include the concern in your base ApplicationController so that all children of that controller include the concern as well.

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include DeviseTokenAuth::Concerns::SetUserByToken
end

# app/controllers/test_controller.rb
class TestController < ApplicationController
  def members_only
    if @user
      render json: {
        data: {
          message: "Welcome #{@user.name}",
          user: @user
        }
      }, status: 200
    else
      render json: {
        errors: ["Authorized users only."]
      }, status: 401
    end
  end
end

The authentication information should be included by the client in the Authorization header of each request. The header should follow this format:

Authorization header example:
token=wwwww client=xxxxx expiry=yyyyy uid=zzzzz

The Authorization header is made up of the following components:

  • token: This serves as the user's password for each request. A hashed version of this value is stored in the database for later comparison. This value should be changed on each request.
  • client: This enables the use of multiple simultaneous sessions on different clients. (For example, a user may want to be authenticated on both their phone and their laptop at the same time.)
  • expiry: The date at which the current session will expire. This can be used by clients to invalidate expired tokens without the need for an API request.
  • uid: A unique value that is used to identify the user. This is necessary because searching the DB for users by their access token will open the API up to timing attacks.

The Authorization header required for each request will be available in the response from the previous request. If you are using the ng-token-auth module for angular.js, this functionality is already provided.

Model Concerns

DeviseTokenAuth::Concerns::SetUserByToken

Typical use of this gem will not require the use of any of the following model methods. All authentication should be handled invisibly by the controller concerns described above.

Models that include the DeviseTokenAuth::Concerns::SetUserByToken concern will have access to the following public methods (read the above section for context on token and client):

  • valid_token?: check if an authentication token is valid. Accepts a token and client as arguments. Returns a boolean.

Example:

  # extract token + client_id from auth header
  client_id = request.headers['Authorization'][/client=(.*?) /,1]
  token = request.headers['Authorization'][/token=(.*?) /,1]

  @user.valid_token?(token, client_id)
  • create_new_auth_token: creates a new auth token with all of the necessary metadata. Accepts client as an optional argument. Will generate a new client if none is provided. Returns the Authorization header that should be sent by the client as a string.

Example:

  # extract client_id from auth header
  client_id = request.headers['Authorization'][/client=(.*?) /,1]

  # update token, generate updated auth headers for response
  new_auth_header = @user.create_new_auth_token(client_id)

  # update response with the header that will be required by the next request
  response.headers["Authorization"] = new_auth_header
  • build_auth_header: generates the auth header that should be sent to the client with the next request. Accepts token and client as arguments. Returns a string.

Example:

  # create client id and token
  client_id = SecureRandom.urlsafe_base64(nil, false)
  token     = SecureRandom.urlsafe_base64(nil, false)

  # store client + token in user's token hash
  @user.tokens[client_id] = {
    token: BCrypt::Password.create(token),
    expiry: (Time.now + DeviseTokenAuth.token_lifespan).to_i
  }

  # generate auth headers for response
  new_auth_header = @user.build_auth_header(token, client_id)

  # update response with the header that will be required by the next request
  response.headers["Authorization"] = new_auth_header

Using multiple models

This gem supports the use of multiple user models. One possible use case is to authenticate visitors using a model called User, and to authenticate administrators with a model called Admin. Take the following steps to add another authentication model to your app:

  1. Run the install generator for the new model. ~~~ rails g devise_token_auth:install Admin admin_auth ~~~

This will create the Admin model and define the model's authentication routes with the base path /admin_auth.

  1. Define the routes to be used by the Admin user within a devise_scope.

Example:

  Rails.application.routes.draw do
    # when using multiple models, controllers will default to the first available
    # devise mapping. routes for subsequent devise mappings will need to defined
    # within a `devise_scope` block

    # define :users as the first devise mapping:
    mount_devise_token_auth_for 'User', at: '/auth'

    # define :admins as the second devise mapping. routes using this class will
    # need to be defined within a devise_scope as shown below
    mount_devise_token_auth_for "Admin", at: '/admin_auth'

    # this route will authorize requests using the User class
    get 'demo/members_only', to: 'demo#members_only'

    # routes within this block will authorize requests using the Admin class
    devise_scope :admin do
      get 'demo/admins_only', to: 'demo#admins_only'
    end
  end

Conceptual

None of the following information is required to use this gem, but read on if you're curious.

About token management

Tokens should be invalidated after each request to the API. The following diagram illustrates this concept:

password reset flow

During each request, a new token is generated. The Authorization header that should be used in the next request is returned in the Authorization header of the response to the previous request. The last request in the diagram fails because it tries to use a token that was invalidated by the previous request.

The only case where an expired token is allowed is during batch requests.

These measures are taken by default when using this gem.

About batch requests

By default, the API should update the auth token for each request (read more). But sometimes it's neccessary to make several concurrent requests to the API, for example:

Batch request example
$scope.getResourceData = function() {

  $http.get('/api/restricted_resource_1').success(function(resp) {
    // handle response
    $scope.resource1 = resp.data;
  });

  $http.get('/api/restricted_resource_2').success(function(resp) {
    // handle response
    $scope.resource2 = resp.data;
  });
};

In this case, it's impossible to update the Authorization header for the second request with the Authorization header of the first response because the second request will begin before the first one is complete. The server must allow these batches of concurrent requests to share the same auth token. This diagram illustrates how batch requests are identified by the server:

batch request overview

The "5 second" buffer in the diagram is the default used this gem.

The following diagram details the relationship between the client, server, and access tokens used over time when dealing with batch requests:

batch request detail

Note that when the server identifies that a request is part of a batch request, the user's auth token is not updated. The auth token will be updated for the first request in the batch, and then that same token will be returned in the responses for each subsequent request in the batch (as shown in the diagram).

This gem automatically manages batch requests. You can change the time buffer for what is considered a batch request using the batch_request_buffer_throttle parameter in config/initializers/devise_token_auth.rb.

Security

This gem takes the following steps to ensure security.

This gem uses auth tokens that are:

These measures were inspired by this stackoverflow post.

This gem further mitigates timing attacks by using this technique.

But the most important step is to use HTTPS. You are on the hook for that.

Contributing

Just send a pull request. I will grant you commit access if you send quality pull requests.

Guidelines will be posted if the need arises.

License

This project uses the WTFPL