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 end
Note that we have:
- Added
has_many :authentication_tokens
- Added
:token_authenticatable
in 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 User
Note 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 end
Our custom controller will have specific routes, which will only be used for JSON (API) requests. The default routes created by
devise_for
will 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 :users
Note 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 end
Make sure the
private
keyword (which is actually a method) and thedo_not_set_cookie
method 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>"
Hey, Does that works for you?, I'm getting acces even if i don't have the right email and token