Devise Part 10: Devise Token Auth

In part ten of the Devise series, we start a new project from scratch to explore how the devise-token-auth gem can be setup with a Rails 7 application.

On this page

This post will explore how we can use devise_token_auth to setup opaque tokens with our applications.

This is a great solution for SPAs or mobile apps that require authentication.

Each request will refresh the token and expires them in a short time. It also maintains sessions for each client which enables us to have multi-session capabilities for the same user on their different devices.

Part 10 will naturally move onto part 11 which will be around setting up simple authentication tokens with Doorkeeper to enable you to share a public API for your users.

Final source code can be found here.

Prerequisites

  1. Basic familiarity with setting up a new Rails project.

Getting started

We will use Rails to initialize the project devise-token-auth-example:

# Create a new rails project
$ rails new devise-token-auth-example
$ cd devise-token-auth-example

We need to add a number of gems for us to use devise_token_auth.

gem 'devise'
gem 'devise_token_auth', '>= 1.2.0', git: "https://github.com/lynndylanhurley/devise_token_auth"

gem 'omniauth'

The latest version on Git plays well with Rails 7, but at the time of writing a new version of the gem is yet to be released.

In the terminal, we now need to install these gems as well as generate the install helpers given from devise and devise_token_auth.

# Install gems
$ bundle

# Generate and setup files
$ bin/rails g devise:install
$ bin/rails g devise_token_auth:install User auth

# Create and migrate DBs
$ bin/rails db:create db:migrate

At this point, we have created a new User model and set it so the /auth endpoints will be where we authenticate.

Updating our application controller

As I will be using Postman for demonstration purposes, we will update our settings to skip forgery.

Update app/controllers/application_controller.rb to the following:

class ApplicationController < ActionController::Base
  skip_forgery_protection
  include DeviseTokenAuth::Concerns::SetUserByToken
end

Creating a new controller to test against

We will be requiring a user authentication for the /api/v1/home route, so let's create a controller to demonstrate this.

# Generate controller
$ bin/rails g controller api/v1/home

Update app/controllers/api/v1/home_controller.rb:

class Api::V1::HomeController < ApplicationController
  before_action :authenticate_user!

  def index
    render json: { message: 'Welcome to the API' }
  end
end

Updating our routes

We need to update our config/routes.rb file to include our home controller as well as setting up devise_token_auth.

Update the file to the following:

Rails.application.routes.draw do
  mount_devise_token_auth_for 'User', at: 'auth'
  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

  # Defines the root path route ("/")
  # root "articles#index"
  namespace :api do
    namespace :v1 do
      resources :home, only: [:index]
    end
  end
end

Setting up the mount_devise_token_auth_for method will provide us with a number of useful endpoint.

If you look through bin/rails routes, you'll find the following related to the /auth endpoints:

        new_user_session GET      /auth/sign_in(.:format)                                                                           devise_token_auth/sessions#new
            user_session POST     /auth/sign_in(.:format)                                                                           devise_token_auth/sessions#create
    destroy_user_session DELETE   /auth/sign_out(.:format)                                                                          devise_token_auth/sessions#destroy
        new_user_password GET      /auth/password/new(.:format)                                                                      devise_token_auth/passwords#new
      edit_user_password GET      /auth/password/edit(.:format)                                                                     devise_token_auth/passwords#edit
            user_password PATCH    /auth/password(.:format)                                                                          devise_token_auth/passwords#update
                          PUT      /auth/password(.:format)                                                                          devise_token_auth/passwords#update
                          POST     /auth/password(.:format)                                                                          devise_token_auth/passwords#create
cancel_user_registration GET      /auth/cancel(.:format)                                                                            devise_token_auth/registrations#cancel
    new_user_registration GET      /auth/sign_up(.:format)                                                                           devise_token_auth/registrations#new
  edit_user_registration GET      /auth/edit(.:format)                                                                              devise_token_auth/registrations#edit
        user_registration PATCH    /auth(.:format)                                                                                   devise_token_auth/registrations#update
                          PUT      /auth(.:format)                                                                                   devise_token_auth/registrations#update
                          DELETE   /auth(.:format)                                                                                   devise_token_auth/registrations#destroy
                          POST     /auth(.:format)                                                                                   devise_token_auth/registrations#create
      auth_validate_token GET      /auth/validate_token(.:format)                                                                    devise_token_auth/token_validations#validate_token
            auth_failure GET      /auth/failure(.:format)                                                                           devise_token_auth/omniauth_callbacks#omniauth_failure
                          GET      /auth/:provider/callback(.:format)                                                                devise_token_auth/omniauth_callbacks

We can use the POST /auth/sign_in route to authenticate our user in our clients, as well as a POST request to /auth to sign up new users.

Signing our can be done DELETE /auth/sign_out.

Testing our authentication with Postman

We can sign up a new user by create a POST request to /auth with email, password and password_confirmation fields:

Signing up
Signing up

From the response, we need to grab three of the headers for our authentication.

  1. client
  2. uid
  3. access-token
Response headers
Response headers

If we make a request to our /api/v1/home route without providing the required headers, you will receive an unauthorized response.

No token provided
No token provided

We can remedy this by putting in all three required headers:

Providing the token
Providing the token

After using the token in a request, a new one will be given back and you need to track it between requests.

New token returned after a request
New token returned after a request

After usage of a token, it will expire soon-after.

Expired token being used
Expired token being used

Amazing! We have gone through a workflow of what accessing protected endpoints will look like.

Summary

Today's post demonstrated how to authenticate a user using devise_token_auth. This method makes use of opaque tokens and can be highly secure in how it works.

In the next part, we will extend this application by using Doorkeeper to create a v2 of the API that will be for public usage.

Resources and further reading

Photo credit: tods