Skip to content

Instantly share code, notes, and snippets.

@brunofacca
Last active October 13, 2023 00:55
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save brunofacca/6b385318dd38c4d6d17d3aad2917904b to your computer and use it in GitHub Desktop.
Save brunofacca/6b385318dd38c4d6d17d3aad2917904b to your computer and use it in GitHub Desktop.
Rails API token authentication with Tiddle gem in apps with web views

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.

Installation and configuration

  1. Add to the Gemfile:
    gem 'tiddle'

  2. Run bundle install

  3. Set up a model to store the authentication tokens:

    1. Generate the migration:
      rails g model AuthenticationToken body:string user:references last_used_at:datetime ip_address:string user_agent:string

    2. 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.

  4. Customize the Sessions Controller to handle token authentication for the API:

    1. 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.

    2. 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).

  5. 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.

  6. 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 the do_not_set_cookie method definition are placed at the bottom of the controller to avoid making other (unrelated) methods private.

Testing it with curl

# 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>"
@Jatapiaro
Copy link

Hey, Does that works for you?, I'm getting acces even if i don't have the right email and token

@edwardmp
Copy link

Thanks, works great here! Stumbled upon 2 small issues though (wrong logout path in CURL example, and the devise controller command should take the route name (e.g. users, not User): https://gist.github.com/edwardmp/24e50df3511280c3216467e231550f45

@redoc123
Copy link

This is sensational little gem. devise_token_auth is a buggy gem. I dont like the way it stores token in a single column with nested tree structure.

Thanks for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment