JWT Ruby

AuthenticationJWTRubySecurityTokenRFC7519

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

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