Categories
Programming

JWT Authentication

Implementing authentication for third-party access is no small feat, but is imperative in order to compete in a complex API economy and expand business capabilities. With every application, securing protected resources always poses a unique challenge, particularly considering how rapid technology evolves. New solutions come along and customers expect a level of consistency across apps, which is important to be mindful of in reducing friction (and generating revenue).

The OAuth 2.0 protocol is the industry standard for authorization. It focuses on client/developer simplicity, and enables secure access for desktop and mobile applications. Nearly everyone has come across this type of authentication with Single Sign On (SSO) options from companies like Google, Apple, or Linkedin, which keep you logged in across all of their products (view a full list of strategies).

OAuth Logo

For the purposes of this article, the focus will be on authenticating access to protected data through Rails 6 REST API endpoints, through means like axios and cURL. Axios is an excellent Promise based HTTP client which comes bundled with features that can intercept or transform data in the request/response cycle, sending authentication credentials with every connection.

Basic server-side authentication using sessions and cookies doesn’t require much initial setup, but when it comes to client-side authentication, there are many cross-origin security vulnerabilities to take into account. I’ve tried a combination of server/client-side authentication methods using Devise, but had greater success by committing to either or. It really depends on the type of product you are building, but a Single Page Application authenticated by way of JWT opens up more possibilities for microservices via public API.

Token Authentication - Rails Axios Devise Warden

It makes sense to implement token authorization at the API namespace level, and provide an entrypoint for a React front-end through a static controller and view. Following this method, the client-side view logic can be handled by restricting private routes using Higher Order Components (HOCs), and react-router to redirect to a login component if the user is not authorized. We’ll look at how to integrate Devise–one of the most popular plug-n-play authentication libraries for Rails–and how to make it work with JSON Web Tokens (JWT).

What is JWT?

For a user to gain limited access to a web server, there needs to be an authentication scheme which allows the sharing of resources without revealing user credentials. With OAuth, this is referred to as flows. The HTTP authentication scheme typically uses a Bearer or Authentication token, which is a cryptic string generated by a server response upon login:

Authorization: Bearer <token>

The token is comprised of 3 distinct parts separated by dots: header.payload.signature. All of the parts are then Base64Url encoded for simple transmission in HTML and HTTP environments. The 3 parts are as follows:

1. Header

The first part of the JWT is the signing algorithm and type of token:

{
  "alg": "HS256",
  "typ": "JWT"
}

2. Payload

Second, is the payload which holds claims–pieces of information asserted about the subject:

{
  "exp": "86400",
  "name": "Adam",
  "admin": true
}

3. Signature

Last, is the signature created by signing the previously encoded parts using the algorithm specified in the header:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  256-bit-secret 
)

The secret can be stored in rails credentials, and set in Devise’s configuration:

config.jwt do |jwt|
  jwt.secret = Rails.application.credentials.dig(:devise_jwt_secret_key)
end

The output will be a JSON Web Token signed with the secret key:

eyJhbGciOiJIUzI1NiJ9.
eyJlbWFpbCI6ImFkYW1uYWFtYW5pQGdtYWlsLmNvbSIsImFkbWluIjp0cnVlLCJzdWIiOiIxIiwic2NwIjoidXNlciIsImF1ZCI6bnVsbCwiaWF0IjoxNTkxMTI4MTg5LCJleHAiOjE1OTEyMTQ1ODksImp0aSI6IjFhNGVhNDE1LWYxNTktNDhkOC05ZjlhLTQzYzNhMjZhZTlhOCJ9.
XutPAVM0Lxoyx_qv9QefVtgmtFKcVEg0N-tERlZneKU

Enter Devise

Devise is based on Warden–a Rack-based authentication middleware for Ruby–offering a complete MVC solution based on Rails engines, and powerful options for authenticating users. Install the following gems in your Gemfile:

gem 'devise'
gem 'devise-jwt'
gem 'pundit'
gem 'js-routes'

Managing simple http requests is fairly straight-forward, and as simple as using Devise’s built-in generators:

$ rails generate devise:install
$ rails generate devise User
$ rails generate devise:views users
$ rails db:migrate

You would then add a before_action callback wherever you want protected routes:

before_action :authenticate_user!

One of the biggest snags I ran into was originally placing this method in the ApplicationController, so I moved it to a base controller that all API controllers inherit from. Doing so offloaded all authentication responsibility to the client, and prevented redirect hell. Next, configure the User model to include the strategy:

class User < ApplicationRecord
  devise :database_authenticatable,
         :registerable,
         :validatable,
         :trackable,
         :rememberable,
         :recoverable,
         :timeoutable,
         :lockable,
         :invitable,
         :omniauthable,
         :jwt_authenticatable,
         jwt_revocation_strategy: JwtBlacklist,
         omniauth_providers: %i[google_oauth2]

  def self.from_omniauth(auth)
    where(email: auth.info.email)
      .or(User.where(uid: auth.uid))
      .first_or_initialize
  end

  def jwt_payload
    { email: email, admin: admin }
  end

  def generate_jwt
    JWT.encode({ id: id, exp: 1.day.from_now.to_i }, DEVISE_SECRET_KEY)
  end

  def on_jwt_dispatch(_token, _payload)
    JwtBlacklist.where('exp < ?', Date.today).destroy_all
  end
end

Devise makes helpers available that pair well with Pundit, so you can not only authorize access to the app, but restrict what users can access within the app:

user_signed_in?
current_user
user_session

There’s a plethora of resources online that will delve into the specific configurations for Devise, but these were the ones that gave me the most trouble when not configured correctly:

config.skip_session_storage = %i[http_auth]
config.jwt do |jwt|
  jwt.secret = Rails.application.credentials.dig(:devise_jwt_secret_key)
  jwt.expiration_time = 1.day.to_i
  jwt.request_formats = { user: [:json] }
  jwt.dispatch_requests = [
    ['POST', %r{^/api/signup$}],
    ['POST', %r{^/api/login$}]
  ]
  jwt.revocation_requests = [
    ['DELETE', %r{^/api/signout$}]
  ]
end

This basically instructs Devise to dispatch a JWT when an authorized request is made through one of these endpoints. You also need to specify the routes for Devise:

Rails.application.routes.draw do
  defaults format: :json do
    namespace :api do
      devise_for :users, skip: %i[sessions registrations]
      devise_scope :user do
        post 'signup', to: 'registrations#create'
        post 'login', to: 'sessions#create'
      end
    end
  end
end

Setting Up the Client-Side

Now that the back-end is configured to issue tokens, we can set up the front-end. Use axios’ interceptors to set the defaults on every request/response:

import axios from 'axios'
import {
  setToken,
  getToken,
  removeToken
} from '../auth/token'

axios.defaults.headers.common.Accept = 'application/json'
axios.defaults.headers.common['Content-Type'] = 'application/json'
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
axios.defaults.responseType = 'json'
axios.defaults.withCredentials = true

axios.interceptors.request.use((request) => {
  if (getToken()) {
    axios.defaults.headers.common.Authorization = `Bearer ${token}`
  }
  return request
}, (error) => {
  handleError(error)
})
axios.interceptors.response.use((response) => {
  const { authorization } = response.headers
  if (authorization) {
    removeToken()
    setToken(authorization.split(' ')[1])
  }
  return response
}, (error) => {
  handleError(error)
})

When a request is made from the client to the api/login endpoint, the Bearer token will be returned in the response.

axios.post(Routes.api_login(), formData)
     .then((response) => response)
Token Authentication - Warden & Devise

Since I use React as my front-end framework, I created separate Authentication components apart from the views generated by Devise. The routing logic is handled with a react-router-dom Higher Order Component, that checks if the user is authenticated from the aforementioned server response, and injects the appropriate component.

import React from 'react'
import { Route, Redirect } from 'react-router-dom'
import { isAuthenticated } from '../../api/index'

const PrivateRoute = ({ component: Component, ...rest }) => (
  <Route
    {...rest}
    render={(props) => (isAuthenticated() ? (
      <Component {...props} />
    ) : (
      <Redirect
        to={{
          pathname: '/login',
          state: { from: props.location } }} />
    ))} />
)

export default PrivateRoute

By Adam Naamani

Real estate specialist, software engineer, and writer based in Vancouver, British Columbia.

Leave a Reply