This gist attempts to explain how to implement token authentication in Rails, using Devise and Tiddle. Tiddle was designed to provide token auth for API-only Rails apps. However, the following instructions will enable you to use it in Rails apps which have both APIs and web views.
##Why Tiddle?
Devise is the obvious choice for authentication on Rails. However, token authentication was deprecated in Devise 3.1. There are a few gems that provide token authentication for Rails apps with Devise:
- Simple Token Authentication: Looks good if you need very simple token auth. However, it lacked some of the features my project required, such as supporting multiple tokens per user.
- Tiddle: Supports multiple tokens per user and its simple to implement.
- Devise Token Auth: Looks a lot more robust than the above. If your project requires strong security and a full-featured token auth system, this is probably the best choice. However, the project I'm currently working on only requires simple token auth for a read-only API where security is not a major concern. I wanted to avoid unnecessary complexity, so this gem seemed like overkill.
##Prerequisites
This tutorial assumes the following items:
- You already have Devise configured to authenticate the web views of your Rails app. You want to use tokens to authenticate API (JSON) requests in the same Rails app.
-
Add to the Gemfile:
gem 'tiddle' -
Run
bundle install -
Set up a model to store the authentication tokens:
-
Generate the migration:
rails g model AuthenticationToken body:string user:references last_used_at:datetime ip_address:string user_agent:string -
Edit
app/models/user.rb, so it looks similar to:# app/models/user.rb class User < ActiveRecord::Base has_many :authentication_tokens devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :token_authenticatable endNote that we have:
- Added
has_many :authentication_tokens - Added
:token_authenticatablein the list of devise modules
User is the default name for the model created by Devise to store it's users. You may have named it differently when installing Devise.
- Added
-
-
Customize the Sessions Controller to handle token authentication for the API:
-
Generate custom controllers:
rails generate devise:controllers UserNote that Devise controllers are contained inside the Devise gem by default. The above command will generate a set of Devise controller files in
app/controllers/users/so we can customize them. -
Edit
app/controllers/users/sessions_controller.rb, so it looks like this:module Api module V1 class Users::SessionsController < Devise::SessionsController skip_before_action :verify_signed_out_user def create user = warden.authenticate!(:scope => :user) token = Tiddle.create_and_return_token(user, request) render json: { authentication_token: token } end def destroy if current_user && Tiddle.expire_token(current_user, request) head :ok else # Client tried to expire an invalid token render json: { error: 'invalid token' }, status: 401 end end end end endOur custom controller will have specific routes, which will only be used for JSON (API) requests. The default routes created by
devise_forwill remain unaltered, pointing to Devise's original session controller, which will keep being used by HTML requests (web view authentication).
-
-
Create routes to our custom controller by including the following lines in
config/routes.rb:# Devise routes for API clients (custom sessions controller) devise_scope :user do post 'api/v1/login', to: 'users/sessions#create' delete 'api/v1/logout', to: 'users/sessions#destroy' end # Devise routes for web clients (built-in sessions controller) devise_for :usersNote that
devise_for :users(which should already exist in routes.rb) must be below our custom routes. -
Include the following lines to
app/controllers/application_controller.rb.# Prevent CSRF attacks, except for JSON requests (API clients) protect_from_forgery unless: -> { request.format.json? } # Require authentication and do not set a session cookie for JSON requests (API clients) before_action :authenticate_user!, :do_not_set_cookie, if: -> { request.format.json? } private # Do not generate a session or session ID cookie # See https://github.com/rack/rack/blob/master/lib/rack/session/abstract/id.rb#L171 def do_not_set_cookie request.session_options[:skip] = true endMake sure the
privatekeyword (which is actually a method) and thedo_not_set_cookiemethod definition are placed at the bottom of the controller to avoid making other (unrelated) methods private.
# Get an auth token (send credentials in JSON format):
curl -X POST "http://localhost:3000/api/v1/login.json" \
-H "Content-Type:application/json" \
-d '{ "user": { "email": "<email>", "password": "<password>" }}'
# Use the token to authenticate a request (send credentials as headers)
curl -X GET "http://localhost:3000/api/v1/<resource name>.json" \
-H "Content-type: application/json" \
-H "X-User-Email: <email>" \
-H "X-User-Token: <token>"
# Log out (destroy the auth token):
curl -X DELETE "http://localhost:3000/api/v1/sign_out.json" \
-H "Content-Type:application/json" \
-H "X-User-Email: <email>" \
-H "X-User-Token: <token>"