devise

How do I use JWT tokens with Devise?

Devise doesn’t include JWT support by default, but you can integrate JWT authentication using the devise-jwt gem. This is particularly useful for API-only applications or mobile app backends.

Installation

require 'bundler/inline'

gemfile do
  source 'https://rubygems.org'
  gem 'devise'
  gem 'devise-jwt'
  gem 'rails'
end

Or add to your Gemfile:

gem 'devise'
gem 'devise-jwt'

Basic Configuration

1. Configure Devise JWT in config/initializers/devise.rb:

Devise.setup do |config|
  config.jwt do |jwt|
    jwt.secret = Rails.application.credentials.devise_jwt_secret_key
    jwt.dispatch_requests = [
      ['POST', %r{^/login$}]
    ]
    jwt.revocation_requests = [
      ['DELETE', %r{^/logout$}]
    ]
    jwt.expiration_time = 1.day.to_i
  end
end

2. Create a JWT revocation strategy (recommended for security):

Create a model to track JWT tokens:

rails generate model JwtDenylist jti:string:index exp:datetime
rails db:migrate

3. Configure the JwtDenylist model:

class JwtDenylist < ApplicationRecord
  include Devise::JWT::RevocationStrategies::Denylist

  self.table_name = 'jwt_denylist'
end

4. Update your User model:

class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :jwt_authenticatable, jwt_revocation_strategy: JwtDenylist
end

API Controllers

Sessions Controller for login:

class Api::SessionsController < Devise::SessionsController
  respond_to :json

  private

  def respond_with(resource, _opts = {})
    render json: {
      message: 'Logged in successfully',
      user: resource
    }, status: :ok
  end

  def respond_to_on_destroy
    if current_user
      render json: {
        message: 'Logged out successfully'
      }, status: :ok
    else
      render json: {
        message: 'User not found'
      }, status: :unauthorized
    end
  end
end

Registrations Controller for signup:

class Api::RegistrationsController < Devise::RegistrationsController
  respond_to :json

  private

  def respond_with(resource, _opts = {})
    if resource.persisted?
      render json: {
        message: 'Signed up successfully',
        user: resource
      }, status: :ok
    else
      render json: {
        message: 'Sign up failed',
        errors: resource.errors.full_messages
      }, status: :unprocessable_entity
    end
  end
end

Routes Configuration

Rails.application.routes.draw do
  devise_for :users,
    path: '',
    path_names: {
      sign_in: 'login',
      sign_out: 'logout',
      registration: 'signup'
    },
    controllers: {
      sessions: 'api/sessions',
      registrations: 'api/registrations'
    }
end

Making Authenticated Requests

The JWT token is automatically included in the Authorization header on login. Clients should include it in subsequent requests:

# Client example
headers = {
  'Authorization' => "Bearer #{jwt_token}",
  'Content-Type' => 'application/json'
}

Protecting Controllers

Use authenticate_user! to require authentication:

class Api::ProtectedController < ApplicationController
  before_action :authenticate_user!

  def index
    render json: {
      message: 'This is protected data',
      user: current_user
    }
  end
end

Token Revocation Strategies

Denylist Strategy (recommended): Stores revoked tokens in database. More secure but requires database queries.

Allowlist Strategy: Stores all valid tokens. Most secure but highest database overhead.

# Allowlist example
class User < ApplicationRecord
  devise :jwt_authenticatable,
         jwt_revocation_strategy: Devise::JWT::RevocationStrategies::Allowlist
end

Null Strategy: No revocation checking. Fastest but tokens remain valid until expiration.

class User < ApplicationRecord
  devise :jwt_authenticatable,
         jwt_revocation_strategy: Devise::JWT::RevocationStrategies::Null
end

Custom Claims

Add custom data to JWT payload:

class User < ApplicationRecord
  devise :jwt_authenticatable, jwt_revocation_strategy: JwtDenylist

  def jwt_payload
    super.merge('custom_field' => custom_value)
  end
end

Important Considerations

  • Always use HTTPS in production to protect tokens in transit
  • Store the JWT secret securely using Rails credentials or environment variables
  • Set appropriate expiration times (1 day to 1 week is common)
  • Implement token refresh mechanisms for long-lived sessions
  • Use denylist or allowlist strategies for production applications
  • Consider rate limiting on authentication endpoints