JWT Ruby
Library
JWT Ruby
Overview
JWT Ruby is a Ruby implementation of JSON Web Token (JWT) that complies with RFC 7519 standard, providing secure authentication and token management.
Details
JWT Ruby gem is a comprehensive library for implementing JWT authentication in Ruby/Rails applications. It supports HMAC, RSASSA, ECDSA, RSASSA-PSS, and EdDSA algorithms with native implementation through the OpenSSL library. Supporting up to Ruby 3.3, it delivers high performance and security. The library provides token generation, verification, and decoding capabilities, with automatic custom claim processing and expiration validation. It integrates easily with Rails applications and is utilized by many third-party libraries. Actively developed in the jwt/ruby-jwt GitHub repository, it's a reliable library suitable for enterprise-grade authentication systems.
Pros and Cons
Pros
- RFC 7519 Compliant: Full compliance with standard JWT specifications
- Rich Algorithms: Support for HMAC, RSA, ECDSA, EdDSA
- High Performance: Fast processing with OpenSSL native implementation
- Rails Integration: Natural integration with Ruby on Rails
- Extensibility: Support for custom algorithms and extensions
- Mature Library: Long history of development and maintenance
Cons
- Configuration Complexity: Complex security configuration and learning curve
- Ruby-Specific: Cannot be used in non-Ruby runtime environments
- Dependencies: Dependency on OpenSSL library
- Algorithm Selection: Need to choose appropriate encryption algorithms
- Legacy Ruby: Ended support for Ruby 2.5 and below
Main Links
- GitHub - jwt/ruby-jwt
- RubyGems.org - jwt
- JWT Official Site
- RFC 7519 - JWT Specification
- JWT.io
- Ruby Official Site
Code Examples
Basic Token Generation and Verification
require 'jwt'
# Secret key
secret = 'my_secret_key'
# Payload
payload = {
user_id: 123,
email: '[email protected]',
exp: Time.now.to_i + 3600 # Expires in 1 hour
}
# Generate token
token = JWT.encode(payload, secret, 'HS256')
puts "Generated token: #{token}"
# Decode and verify token
begin
decoded_payload = JWT.decode(token, secret, true, algorithm: 'HS256')
puts "Decoded payload: #{decoded_payload[0]}"
puts "Header: #{decoded_payload[1]}"
rescue JWT::DecodeError => e
puts "Decode error: #{e.message}"
end
RSA Private/Public Key Signing
require 'jwt'
require 'openssl'
# Generate RSA key pair
rsa_private = OpenSSL::PKey::RSA.generate(2048)
rsa_public = rsa_private.public_key
# Payload
payload = {
user_id: 456,
role: 'admin',
iat: Time.now.to_i,
exp: Time.now.to_i + 3600
}
# Sign with RSA-256
token = JWT.encode(payload, rsa_private, 'RS256')
puts "RSA signed token: #{token}"
# Verify with public key
begin
decoded = JWT.decode(token, rsa_public, true, algorithm: 'RS256')
puts "Successfully verified with public key"
puts "User ID: #{decoded[0]['user_id']}"
puts "Role: #{decoded[0]['role']}"
rescue JWT::VerificationError => e
puts "Verification failed: #{e.message}"
end
Rails Authentication System Implementation
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :authenticate_request
private
def authenticate_request
header = request.headers['Authorization']
if header.present?
token = header.split(' ').last
begin
decoded = JWT.decode(token, Rails.application.secrets.secret_key_base, true, algorithm: 'HS256')
@current_user_id = decoded[0]['user_id']
rescue JWT::DecodeError => e
render json: { error: 'Invalid token' }, status: :unauthorized
end
else
render json: { error: 'Missing token' }, status: :unauthorized
end
end
def current_user
@current_user ||= User.find(@current_user_id) if @current_user_id
end
end
# app/controllers/auth_controller.rb
class AuthController < ApplicationController
skip_before_action :authenticate_request, only: [:login]
def login
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
payload = {
user_id: user.id,
email: user.email,
role: user.role,
iat: Time.now.to_i,
exp: Time.now.to_i + 24.hours.to_i
}
token = JWT.encode(payload, Rails.application.secrets.secret_key_base, 'HS256')
render json: {
token: token,
user: {
id: user.id,
email: user.email,
name: user.name
}
}
else
render json: { error: 'Invalid credentials' }, status: :unauthorized
end
end
def me
render json: current_user
end
end
JWT Service Class
# app/services/jwt_service.rb
class JwtService
ALGORITHM = 'HS256'.freeze
class << self
def encode(payload, expiration = 24.hours.from_now)
payload[:exp] = expiration.to_i
payload[:iat] = Time.now.to_i
JWT.encode(payload, secret_key, ALGORITHM)
end
def decode(token)
decoded = JWT.decode(token, secret_key, true, algorithm: ALGORITHM)
decoded[0].with_indifferent_access
rescue JWT::DecodeError => e
Rails.logger.error "JWT Decode Error: #{e.message}"
nil
end
def verify_and_decode(token)
payload = decode(token)
return nil unless payload
# Additional custom validation
return nil if payload[:exp] < Time.now.to_i
payload
end
def refresh_token(old_token)
payload = decode(old_token)
return nil unless payload
# Check refresh availability (e.g., within 1 week of expiration)
expiry_time = Time.at(payload[:exp])
return nil if expiry_time < 1.week.ago
# Generate new token
new_payload = payload.except(:exp, :iat)
encode(new_payload)
end
private
def secret_key
Rails.application.secrets.secret_key_base
end
end
end
# Usage examples
token = JwtService.encode({ user_id: 1, role: 'user' })
payload = JwtService.verify_and_decode(token)
new_token = JwtService.refresh_token(token)
Authorization Management and Middleware
# app/controllers/concerns/authorization.rb
module Authorization
extend ActiveSupport::Concern
def authorize_admin
unless current_user&.admin?
render json: { error: 'Admin access required' }, status: :forbidden
end
end
def authorize_owner_or_admin(resource)
unless current_user&.admin? || resource.user_id == current_user.id
render json: { error: 'Access denied' }, status: :forbidden
end
end
end
# app/middleware/jwt_middleware.rb
class JwtMiddleware
def initialize(app)
@app = app
end
def call(env)
request = Rack::Request.new(env)
# Skip paths that don't require authentication
unless requires_authentication?(request.path)
return @app.call(env)
end
auth_header = request.get_header('HTTP_AUTHORIZATION')
if auth_header&.start_with?('Bearer ')
token = auth_header[7..-1]
payload = JwtService.verify_and_decode(token)
if payload
env['jwt.payload'] = payload
env['jwt.user_id'] = payload['user_id']
else
return unauthorized_response
end
else
return unauthorized_response
end
@app.call(env)
end
private
def requires_authentication?(path)
# Define paths that don't require authentication
excluded_paths = ['/auth/login', '/auth/register', '/health']
!excluded_paths.any? { |excluded| path.start_with?(excluded) }
end
def unauthorized_response
[401, { 'Content-Type' => 'application/json' },
[{ error: 'Unauthorized' }.to_json]]
end
end
Custom Claims and Validation
# app/models/jwt_token.rb
class JwtToken
include ActiveModel::Model
include ActiveModel::Validations
attr_accessor :user_id, :email, :role, :permissions, :iat, :exp, :jti
validates :user_id, presence: true
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :role, inclusion: { in: %w[admin user guest] }
def self.from_token(token)
payload = JwtService.decode(token)
return nil unless payload
new(payload)
end
def to_token
return nil unless valid?
payload = {
user_id: user_id,
email: email,
role: role,
permissions: permissions,
jti: jti || SecureRandom.uuid
}
JwtService.encode(payload)
end
def expired?
return true unless exp
Time.at(exp) < Time.now
end
def admin?
role == 'admin'
end
def can?(permission)
return true if admin?
permissions&.include?(permission.to_s)
end
end
# Usage example
jwt_token = JwtToken.new(
user_id: 1,
email: '[email protected]',
role: 'user',
permissions: ['read_posts', 'write_comments']
)
if jwt_token.valid?
token = jwt_token.to_token
# Restore from token
restored_token = JwtToken.from_token(token)
puts restored_token.can?('read_posts') # => true
puts restored_token.can?('delete_users') # => false
end
Testing Usage Examples
# spec/support/jwt_helpers.rb
module JwtHelpers
def generate_jwt_token(user, additional_claims = {})
payload = {
user_id: user.id,
email: user.email,
role: user.role
}.merge(additional_claims)
JwtService.encode(payload)
end
def auth_headers(user)
token = generate_jwt_token(user)
{ 'Authorization' => "Bearer #{token}" }
end
end
# spec/requests/api/posts_spec.rb
RSpec.describe 'API::Posts', type: :request do
include JwtHelpers
let(:user) { create(:user) }
let(:admin) { create(:user, :admin) }
describe 'GET /api/posts' do
it 'returns posts for authenticated user' do
get '/api/posts', headers: auth_headers(user)
expect(response).to have_http_status(:ok)
expect(JSON.parse(response.body)).to be_an(Array)
end
it 'returns unauthorized for invalid token' do
get '/api/posts', headers: { 'Authorization' => 'Bearer invalid_token' }
expect(response).to have_http_status(:unauthorized)
end
end
describe 'DELETE /api/posts/:id' do
let(:post) { create(:post, user: user) }
it 'allows owner to delete their post' do
delete "/api/posts/#{post.id}", headers: auth_headers(user)
expect(response).to have_http_status(:no_content)
end
it 'allows admin to delete any post' do
delete "/api/posts/#{post.id}", headers: auth_headers(admin)
expect(response).to have_http_status(:no_content)
end
end
end