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