CrackedRuby CrackedRuby

Overview

JSON Web Tokens (JWT) provide a compact, URL-safe method for representing claims between two parties. A JWT encodes JSON data and signs it cryptographically, allowing the recipient to verify the token's authenticity and integrity without maintaining server-side session state.

The token format consists of three Base64URL-encoded sections separated by periods: header, payload, and signature. The header specifies the signing algorithm, the payload contains claims (statements about an entity), and the signature verifies that the token hasn't been tampered with.

# A typical JWT structure
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"

JWTs originated from the need for stateless authentication in distributed systems. Traditional session-based authentication requires server-side storage and lookup for each request. JWTs eliminate this requirement by embedding all necessary information within the token itself. The server validates the token's signature rather than querying a database.

This approach fits well with RESTful API design principles, microservices architectures, and single-page applications where maintaining session state across multiple services becomes complex. Mobile applications particularly benefit from JWTs because they avoid cookie-based authentication mechanisms that don't translate well to native mobile environments.

JWTs serve multiple purposes beyond authentication. They function as information exchange mechanisms where the signature guarantees data integrity. They enable single sign-on (SSO) across multiple domains by providing a portable identity credential. They support authorization by embedding permission claims directly in the token.

Key Principles

A JWT consists of three components encoded as Base64URL strings and concatenated with periods. The header contains metadata about the token type and signing algorithm. The payload contains claims, which are statements about the entity and additional metadata. The signature provides cryptographic verification of the token's authenticity.

The header typically specifies two fields: typ indicating the token type (JWT) and alg specifying the signing algorithm. Supported algorithms include HMAC with SHA-256 (HS256), RSA with SHA-256 (RS256), and Elliptic Curve Digital Signature Algorithm (ECDSA). The choice of algorithm determines the signature generation and verification process.

# Header example (before Base64URL encoding)
{
  "alg": "HS256",
  "typ": "JWT"
}

The payload contains claims, divided into three categories. Registered claims are predefined standard fields like iss (issuer), sub (subject), aud (audience), exp (expiration time), nbf (not before), and iat (issued at). Public claims are custom fields defined in the IANA JWT Claims Registry or using collision-resistant names. Private claims are custom fields agreed upon between parties.

# Payload example (before Base64URL encoding)
{
  "sub": "user123",
  "name": "Jane Developer",
  "role": "admin",
  "exp": 1735689600,
  "iat": 1735603200
}

The signature creation process involves concatenating the encoded header and payload with a period, then applying the specified algorithm with a secret key. For HMAC algorithms, the same secret key signs and verifies the token. For RSA and ECDSA algorithms, a private key signs the token and the corresponding public key verifies it.

signature = HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

JWTs operate on a stateless verification model. The server doesn't maintain a record of issued tokens. Instead, it verifies each token's signature using the known secret or public key. If the signature validates and the claims satisfy the application's requirements (expiration, audience, etc.), the server accepts the token.

This stateless nature creates both advantages and constraints. The server scales horizontally without shared session storage requirements. Token validation remains fast because it involves only cryptographic operations, not database queries. However, the server cannot invalidate individual tokens before their expiration time without implementing additional infrastructure like a token blacklist.

The Base64URL encoding differs from standard Base64 by replacing + with -, / with _, and removing padding = characters. This modification makes tokens URL-safe and suitable for use in HTTP headers, URL parameters, and HTML form values without requiring additional encoding.

Claims-based authentication shifts the security model from "who has access" to "what the token proves." The token becomes a portable proof of identity and permissions. The application examines token claims to make authorization decisions without querying a user database on every request.

Ruby Implementation

Ruby's jwt gem provides a straightforward interface for encoding and decoding JWTs. The gem supports multiple signing algorithms including HMAC, RSA, and ECDSA variants.

require 'jwt'

# Encoding a token with HS256 algorithm
payload = {
  user_id: 123,
  username: 'alice',
  role: 'editor',
  exp: Time.now.to_i + 3600 # Expires in 1 hour
}

secret = 'your-256-bit-secret'
token = JWT.encode(payload, secret, 'HS256')

# => "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxMjMsInVzZXJuYW1lIjoiYWxpY2UiLCJyb2xlIjoiZWRpdG9yIiwiZXhwIjoxNzM1NjA2ODAwfQ.qL9hVVNKhQPXu8e-vk8..."

Decoding validates the signature and returns the payload. The decode method raises exceptions for invalid tokens, allowing the application to handle verification failures appropriately.

begin
  decoded = JWT.decode(token, secret, true, { algorithm: 'HS256' })
  payload = decoded[0]
  header = decoded[1]
  
  puts payload['user_id']  # => 123
  puts payload['role']     # => "editor"
rescue JWT::DecodeError => e
  puts "Invalid token: #{e.message}"
end

The third parameter (true) enables signature verification. Setting it to false skips verification, which should only occur in specific scenarios like inspecting token contents without validation. The fourth parameter specifies verification options including expected algorithm, required claims, and custom validators.

RSA-based signing uses asymmetric cryptography where a private key signs tokens and public keys verify them. This approach suits distributed systems where multiple services need to verify tokens but shouldn't have the ability to create them.

require 'openssl'

# Generate RSA key pair
private_key = OpenSSL::PKey::RSA.generate(2048)
public_key = private_key.public_key

# Encode with private key
payload = { user_id: 456, exp: Time.now.to_i + 3600 }
token = JWT.encode(payload, private_key, 'RS256')

# Decode with public key
decoded = JWT.decode(token, public_key, true, { algorithm: 'RS256' })
# => [{"user_id"=>456, "exp"=>1735606800}, {"alg"=>"RS256"}]

For production systems, store the private key securely in environment variables or key management systems rather than hardcoding it. The public key can be distributed more freely since it only verifies signatures.

# Loading keys from environment
private_key = OpenSSL::PKey::RSA.new(ENV['JWT_PRIVATE_KEY'])
public_key = OpenSSL::PKey::RSA.new(ENV['JWT_PUBLIC_KEY'])

The jwt gem supports custom claim validators through the verify_claims method or by passing verification options. Common validations include expiration time, issued-at time, not-before time, issuer, and audience.

# Comprehensive token verification
options = {
  algorithm: 'HS256',
  verify_expiration: true,
  verify_iat: true,
  exp_leeway: 30,        # 30 second clock skew tolerance
  iss: 'auth.example.com',
  verify_iss: true,
  aud: 'api.example.com',
  verify_aud: true
}

begin
  decoded = JWT.decode(token, secret, true, options)
rescue JWT::ExpiredSignature
  # Handle expired token
rescue JWT::InvalidIssuerError
  # Handle wrong issuer
rescue JWT::InvalidAudError
  # Handle wrong audience
end

Rails applications commonly implement JWT authentication in a dedicated service class or concern. This encapsulation provides a clean interface for token operations throughout the application.

class JwtService
  SECRET = Rails.application.credentials.jwt_secret
  ALGORITHM = 'HS256'
  
  def self.encode(payload, exp: 24.hours.from_now)
    payload[:exp] = exp.to_i
    JWT.encode(payload, SECRET, ALGORITHM)
  end
  
  def self.decode(token)
    body = JWT.decode(token, SECRET, true, { algorithm: ALGORITHM })[0]
    HashWithIndifferentAccess.new(body)
  rescue JWT::DecodeError => e
    raise AuthenticationError, e.message
  end
end

# Usage in controller
class Api::AuthenticationController < ApplicationController
  def login
    user = User.find_by(email: params[:email])
    
    if user&.authenticate(params[:password])
      token = JwtService.encode(user_id: user.id, role: user.role)
      render json: { token: token, user: user }
    else
      render json: { error: 'Invalid credentials' }, status: :unauthorized
    end
  end
end

Middleware or before_action filters extract and validate tokens from incoming requests. The typical pattern retrieves the token from the Authorization header, validates it, and loads the associated user.

class ApplicationController < ActionController::API
  before_action :authenticate_request
  attr_reader :current_user
  
  private
  
  def authenticate_request
    header = request.headers['Authorization']
    token = header.split(' ').last if header
    
    decoded = JwtService.decode(token)
    @current_user = User.find(decoded[:user_id])
  rescue ActiveRecord::RecordNotFound, AuthenticationError
    render json: { error: 'Unauthorized' }, status: :unauthorized
  end
end

Security Implications

JWTs face multiple security vulnerabilities when implemented incorrectly. The most critical issues involve algorithm confusion, weak secrets, information disclosure, and insufficient validation.

Algorithm confusion attacks exploit the flexibility in the JWT header's algorithm field. An attacker modifies the algorithm from RS256 (RSA) to HS256 (HMAC), then signs the token using the public key as the HMAC secret. If the server doesn't enforce expected algorithms, it accepts the forged token.

# Vulnerable code - accepts any algorithm
decoded = JWT.decode(token, public_key, true)

# Secure code - explicitly requires RS256
decoded = JWT.decode(token, public_key, true, { algorithm: 'RS256' })

The "none" algorithm represents another confusion vector. Some JWT libraries accept tokens with alg: none, bypassing signature verification entirely. Attackers set the algorithm to "none" and remove the signature, creating an unsigned token that vulnerable implementations accept.

# Explicitly whitelist allowed algorithms
ALLOWED_ALGORITHMS = ['RS256'].freeze

def decode_token(token)
  JWT.decode(token, public_key, true, { algorithm: ALLOWED_ALGORITHMS })
rescue JWT::IncorrectAlgorithm
  raise SecurityError, 'Invalid signing algorithm'
end

Weak secrets in HMAC-signed tokens enable brute force attacks. Short or predictable secrets allow attackers to discover the secret through dictionary attacks or exhaustive search. The secret must contain sufficient entropy to resist brute force attempts.

# Weak secret - easily guessable
secret = 'secret123'

# Strong secret - 256 bits of entropy
secret = SecureRandom.hex(32)  # => 64 character hex string

# Or use Rails credentials
secret = Rails.application.credentials.jwt_secret

JWTs carry their payload in plain text, merely Base64URL-encoded. Anyone with access to the token can decode and read its contents without knowing the secret. Store only non-sensitive information in JWT payloads. Never include passwords, credit card numbers, or other confidential data.

# Bad - sensitive data in token
payload = {
  user_id: 123,
  password_hash: '$2a$12$...',  # Never include password hashes
  ssn: '123-45-6789'             # Never include sensitive personal data
}

# Good - only identification and claims
payload = {
  user_id: 123,
  role: 'admin',
  permissions: ['read', 'write']
}

Token expiration mitigates the risk of stolen tokens. Short-lived tokens reduce the window of opportunity for attackers. The standard practice combines short-lived access tokens with longer-lived refresh tokens. The application uses refresh tokens to obtain new access tokens without requiring reauthentication.

class TokenService
  ACCESS_TOKEN_EXPIRY = 15.minutes
  REFRESH_TOKEN_EXPIRY = 7.days
  
  def self.generate_tokens(user)
    access_payload = {
      user_id: user.id,
      role: user.role,
      type: 'access',
      exp: ACCESS_TOKEN_EXPIRY.from_now.to_i
    }
    
    refresh_payload = {
      user_id: user.id,
      type: 'refresh',
      jti: SecureRandom.uuid,  # Unique token ID
      exp: REFRESH_TOKEN_EXPIRY.from_now.to_i
    }
    
    {
      access_token: JWT.encode(access_payload, SECRET, 'HS256'),
      refresh_token: JWT.encode(refresh_payload, SECRET, 'HS256')
    }
  end
end

Refresh tokens require additional security measures. Store refresh token identifiers (jti claim) in a database with revocation capability. When a user logs out or security events occur, mark their refresh tokens as revoked.

Cross-site request forgery (CSRF) protection requires careful consideration with JWTs. Storing tokens in localStorage makes them immune to CSRF but vulnerable to XSS attacks. Storing tokens in httpOnly cookies protects against XSS but requires CSRF tokens. The choice depends on the application's threat model.

# Using httpOnly cookies with CSRF protection
class SessionsController < ApplicationController
  def create
    user = authenticate_user(params)
    token = JwtService.encode(user_id: user.id)
    
    cookies.encrypted[:jwt] = {
      value: token,
      httponly: true,
      secure: Rails.env.production?,
      same_site: :strict
    }
    
    render json: { user: user }
  end
end

Token storage location affects security posture. localStorage provides easy access but exposes tokens to XSS attacks. sessionStorage offers the same accessibility with browser-tab-scoped lifetime. httpOnly cookies prevent JavaScript access but require CSRF protection. Each approach trades convenience for security.

Man-in-the-middle attacks intercept tokens during transmission. HTTPS encrypts all communication, preventing token interception. Never transmit JWTs over unencrypted HTTP connections in production environments.

Audience and issuer validation prevent token misuse across different services. The issuer claim identifies who created the token. The audience claim specifies intended recipients. Validate both to ensure tokens aren't used in unintended contexts.

def validate_token(token)
  options = {
    algorithm: 'RS256',
    iss: 'https://auth.example.com',
    verify_iss: true,
    aud: 'https://api.example.com',
    verify_aud: true
  }
  
  JWT.decode(token, public_key, true, options)
rescue JWT::InvalidIssuerError
  raise SecurityError, 'Token from untrusted issuer'
rescue JWT::InvalidAudError
  raise SecurityError, 'Token not intended for this service'
end

Practical Examples

A typical authentication flow starts with user login, token generation, and subsequent authenticated requests using the token.

# User registration and login
class AuthenticationController < ApplicationController
  skip_before_action :authenticate_request, only: [:login, :register]
  
  def register
    user = User.new(user_params)
    
    if user.save
      tokens = TokenService.generate_tokens(user)
      render json: { user: UserSerializer.new(user), **tokens }, status: :created
    else
      render json: { errors: user.errors }, status: :unprocessable_entity
    end
  end
  
  def login
    user = User.find_by(email: params[:email])
    
    if user&.authenticate(params[:password])
      tokens = TokenService.generate_tokens(user)
      render json: { user: UserSerializer.new(user), **tokens }
    else
      render json: { error: 'Invalid credentials' }, status: :unauthorized
    end
  end
  
  private
  
  def user_params
    params.require(:user).permit(:email, :password, :password_confirmation, :name)
  end
end

Client applications store the token and include it in subsequent requests. The server validates the token and extracts user information from the claims.

# Making authenticated requests
class Api::ArticlesController < ApplicationController
  before_action :authenticate_request
  
  def index
    articles = Article.accessible_by(current_user)
    render json: articles
  end
  
  def create
    article = current_user.articles.build(article_params)
    
    if article.save
      render json: article, status: :created
    else
      render json: { errors: article.errors }, status: :unprocessable_entity
    end
  end
  
  private
  
  def article_params
    params.require(:article).permit(:title, :body, :published)
  end
end

Role-based authorization extends basic authentication by examining role claims in the token payload. Define authorization policies that check user roles and permissions.

class ApplicationController < ActionController::API
  def authenticate_request
    token = extract_token
    decoded = JwtService.decode(token)
    @current_user = User.find(decoded[:user_id])
  rescue ActiveRecord::RecordNotFound, JWT::DecodeError
    render json: { error: 'Unauthorized' }, status: :unauthorized
  end
  
  def require_role(*roles)
    unless roles.include?(current_user&.role)
      render json: { error: 'Forbidden' }, status: :forbidden
    end
  end
  
  def extract_token
    header = request.headers['Authorization']
    header.split(' ').last if header&.start_with?('Bearer ')
  end
end

class Admin::UsersController < ApplicationController
  before_action :authenticate_request
  before_action -> { require_role('admin', 'superadmin') }
  
  def index
    users = User.all
    render json: users
  end
end

Refresh token implementation maintains long-term sessions while minimizing access token exposure time. When the access token expires, clients request a new one using the refresh token.

class TokensController < ApplicationController
  skip_before_action :authenticate_request, only: [:refresh]
  
  def refresh
    refresh_token = params[:refresh_token]
    decoded = JwtService.decode(refresh_token)
    
    # Verify token type
    unless decoded[:type] == 'refresh'
      return render json: { error: 'Invalid token type' }, status: :unauthorized
    end
    
    # Check if token is revoked
    if RefreshToken.revoked?(decoded[:jti])
      return render json: { error: 'Token revoked' }, status: :unauthorized
    end
    
    user = User.find(decoded[:user_id])
    tokens = TokenService.generate_tokens(user)
    
    render json: tokens
  rescue JWT::ExpiredSignature
    render json: { error: 'Refresh token expired' }, status: :unauthorized
  rescue ActiveRecord::RecordNotFound
    render json: { error: 'User not found' }, status: :unauthorized
  end
  
  def revoke
    authenticate_request
    RefreshToken.revoke_all_for_user(current_user.id)
    render json: { message: 'All tokens revoked' }
  end
end

# Model for tracking refresh tokens
class RefreshToken < ApplicationRecord
  belongs_to :user
  
  def self.revoked?(jti)
    exists?(jti: jti, revoked: true)
  end
  
  def self.revoke_all_for_user(user_id)
    where(user_id: user_id).update_all(revoked: true)
  end
end

Microservices architectures use JWTs for service-to-service authentication. A central authentication service issues tokens, and downstream services validate them using the public key.

# Authentication service
class AuthService
  PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['JWT_PRIVATE_KEY'])
  
  def self.issue_service_token(service_name)
    payload = {
      sub: service_name,
      iss: 'auth.example.com',
      aud: '*.example.com',
      iat: Time.now.to_i,
      exp: 1.hour.from_now.to_i,
      scope: determine_scope(service_name)
    }
    
    JWT.encode(payload, PRIVATE_KEY, 'RS256')
  end
  
  def self.determine_scope(service_name)
    # Define service-specific permissions
    case service_name
    when 'user-service'
      ['users:read', 'users:write']
    when 'billing-service'
      ['invoices:read', 'invoices:write']
    else
      []
    end
  end
end

# Downstream service
class ServiceClient
  PUBLIC_KEY = OpenSSL::PKey::RSA.new(ENV['JWT_PUBLIC_KEY'])
  
  def self.verify_request(token)
    decoded = JWT.decode(token, PUBLIC_KEY, true, {
      algorithm: 'RS256',
      iss: 'auth.example.com',
      verify_iss: true,
      aud: '*.example.com',
      verify_aud: true
    })[0]
    
    decoded
  rescue JWT::DecodeError => e
    raise AuthorizationError, e.message
  end
end

Common Pitfalls

Storing sensitive information in JWT payloads creates security vulnerabilities. Developers sometimes include password hashes, API keys, or personal identifiable information, assuming the signature provides encryption. The signature only verifies integrity, not confidentiality. Anyone with access to the token can decode and read its contents.

# Wrong - sensitive data exposed
payload = {
  user_id: 123,
  email: 'user@example.com',
  api_key: 'sk_live_abc123',  # Exposed to anyone with the token
  password_hash: '$2a$12$...'   # Never include password data
}

# Correct - minimal claims only
payload = {
  user_id: 123,
  role: 'member',
  permissions: ['read']
}

Missing algorithm validation allows attackers to exploit algorithm confusion vulnerabilities. Omitting the algorithm parameter in JWT.decode or accepting any algorithm creates attack vectors.

# Vulnerable - accepts any algorithm including 'none'
decoded = JWT.decode(token, secret, true)

# Secure - explicitly specifies expected algorithm
decoded = JWT.decode(token, secret, true, { algorithm: 'HS256' })

# Even better - use constant
ALLOWED_ALGORITHMS = ['HS256'].freeze
decoded = JWT.decode(token, secret, true, { algorithm: ALLOWED_ALGORITHMS })

Disabling signature verification for debugging purposes and forgetting to re-enable it causes critical security flaws. The second parameter in JWT.decode controls signature verification.

# Dangerous - bypasses all security
decoded = JWT.decode(token, nil, false)  # Never deploy this

# Correct - always verify signatures
decoded = JWT.decode(token, secret, true, { algorithm: 'HS256' })

Ignoring token expiration creates indefinite token validity. Without expiration checks, stolen tokens remain valid forever. Always set and validate expiration claims.

# Missing expiration
payload = { user_id: 123 }  # No exp claim
token = JWT.encode(payload, secret, 'HS256')

# Proper expiration
payload = {
  user_id: 123,
  exp: 15.minutes.from_now.to_i
}
token = JWT.encode(payload, secret, 'HS256')

# Verify expiration during decode
options = {
  algorithm: 'HS256',
  verify_expiration: true,
  exp_leeway: 30  # Allow 30 seconds clock skew
}
decoded = JWT.decode(token, secret, true, options)

Using the same secret across multiple environments or applications compromises security. A leaked secret in development environments exposes production systems when secrets aren't separated.

# Bad - same secret everywhere
SECRET = 'shared_secret_across_all_environments'

# Good - environment-specific secrets
class JwtService
  SECRET = case Rails.env
  when 'production'
    Rails.application.credentials.jwt_secret_production
  when 'staging'
    Rails.application.credentials.jwt_secret_staging
  else
    'development_secret_not_used_in_production'
  end
end

Failing to implement token revocation leaves no mechanism to invalidate compromised tokens. While stateless validation offers performance benefits, critical scenarios require revocation capability.

# Implement selective revocation with Redis
class TokenBlacklist
  REDIS = Redis.new
  
  def self.revoke(token)
    decoded = JWT.decode(token, secret, false)[0]  # Don't verify to get exp
    ttl = decoded['exp'] - Time.now.to_i
    return if ttl <= 0
    
    jti = decoded['jti'] || Digest::SHA256.hexdigest(token)
    REDIS.setex("blacklist:#{jti}", ttl, '1')
  end
  
  def self.revoked?(token)
    decoded = JWT.decode(token, secret, false)[0]
    jti = decoded['jti'] || Digest::SHA256.hexdigest(token)
    REDIS.exists?("blacklist:#{jti}")
  end
end

# Check blacklist during authentication
def authenticate_request
  token = extract_token
  
  if TokenBlacklist.revoked?(token)
    return render json: { error: 'Token revoked' }, status: :unauthorized
  end
  
  decoded = JwtService.decode(token)
  @current_user = User.find(decoded[:user_id])
end

Including unnecessary claims increases token size and information exposure. Large tokens impact network performance and increase the attack surface.

# Bloated token with unnecessary data
payload = {
  user_id: 123,
  email: 'user@example.com',
  name: 'John Doe',
  address: '123 Main St',
  phone: '555-0100',
  preferences: { theme: 'dark', language: 'en' },
  metadata: { created_at: '2024-01-01', last_login: '2024-06-15' }
}

# Lean token with essential claims only
payload = {
  sub: '123',  # User ID
  role: 'member',
  exp: 15.minutes.from_now.to_i,
  iat: Time.now.to_i
}

Not validating issuer and audience claims allows tokens from one system to work on another. This creates security boundaries violations in multi-service architectures.

# Service A issues token
token_a = JWT.encode({ user_id: 123, aud: 'service-a.example.com' }, secret, 'HS256')

# Service B must verify audience
def verify_token(token)
  options = {
    algorithm: 'HS256',
    aud: 'service-b.example.com',  # Only accept tokens for this service
    verify_aud: true
  }
  
  JWT.decode(token, secret, true, options)
rescue JWT::InvalidAudError
  raise AuthorizationError, 'Token not valid for this service'
end

Forgetting clock skew tolerance causes valid tokens to fail near expiration boundaries. Network latency and minor time differences between servers require leeway in time-based validations.

# Strict validation may reject valid tokens
options = {
  algorithm: 'HS256',
  verify_expiration: true
}

# Better - allow reasonable clock skew
options = {
  algorithm: 'HS256',
  verify_expiration: true,
  exp_leeway: 60,  # 60 seconds leeway for expiration
  nbf_leeway: 30   # 30 seconds leeway for not-before
}

Testing Approaches

Testing JWT implementations requires validating both successful authentication flows and security boundary conditions. Unit tests verify token generation, validation logic, and error handling. Integration tests confirm end-to-end authentication workflows.

require 'rails_helper'

RSpec.describe JwtService do
  let(:user) { create(:user, id: 123, role: 'admin') }
  let(:secret) { 'test_secret_key' }
  
  before do
    allow(JwtService).to receive(:secret).and_return(secret)
  end
  
  describe '.encode' do
    it 'creates a valid token with user claims' do
      token = JwtService.encode(user_id: user.id, role: user.role)
      decoded = JWT.decode(token, secret, true, { algorithm: 'HS256' })[0]
      
      expect(decoded['user_id']).to eq(123)
      expect(decoded['role']).to eq('admin')
      expect(decoded['exp']).to be_present
    end
    
    it 'includes custom expiration time' do
      exp_time = 1.hour.from_now
      token = JwtService.encode({ user_id: user.id }, exp: exp_time)
      decoded = JWT.decode(token, secret, true, { algorithm: 'HS256' })[0]
      
      expect(decoded['exp']).to eq(exp_time.to_i)
    end
  end
  
  describe '.decode' do
    it 'decodes valid tokens' do
      token = JwtService.encode(user_id: user.id, role: user.role)
      decoded = JwtService.decode(token)
      
      expect(decoded[:user_id]).to eq(123)
      expect(decoded[:role]).to eq('admin')
    end
    
    it 'raises error for invalid tokens' do
      expect {
        JwtService.decode('invalid.token.here')
      }.to raise_error(JWT::DecodeError)
    end
    
    it 'raises error for expired tokens' do
      token = JwtService.encode({ user_id: user.id }, exp: 1.hour.ago)
      
      expect {
        JwtService.decode(token)
      }.to raise_error(JWT::ExpiredSignature)
    end
    
    it 'raises error for tokens with wrong algorithm' do
      other_secret = 'different_secret'
      token = JWT.encode({ user_id: user.id }, other_secret, 'HS512')
      
      expect {
        JwtService.decode(token)
      }.to raise_error(JWT::IncorrectAlgorithm)
    end
  end
end

Controller tests verify authentication middleware and authorization logic across different scenarios.

require 'rails_helper'

RSpec.describe Api::ArticlesController, type: :controller do
  let(:user) { create(:user, role: 'member') }
  let(:admin) { create(:user, role: 'admin') }
  
  describe 'GET #index' do
    context 'with valid token' do
      before do
        token = JwtService.encode(user_id: user.id, role: user.role)
        request.headers['Authorization'] = "Bearer #{token}"
      end
      
      it 'returns articles' do
        create_list(:article, 3)
        get :index
        
        expect(response).to have_http_status(:ok)
        expect(JSON.parse(response.body).length).to eq(3)
      end
    end
    
    context 'without token' do
      it 'returns unauthorized' do
        get :index
        expect(response).to have_http_status(:unauthorized)
      end
    end
    
    context 'with expired token' do
      before do
        token = JwtService.encode({ user_id: user.id }, exp: 1.hour.ago)
        request.headers['Authorization'] = "Bearer #{token}"
      end
      
      it 'returns unauthorized' do
        get :index
        expect(response).to have_http_status(:unauthorized)
      end
    end
    
    context 'with malformed token' do
      before do
        request.headers['Authorization'] = "Bearer invalid.token"
      end
      
      it 'returns unauthorized' do
        get :index
        expect(response).to have_http_status(:unauthorized)
      end
    end
  end
  
  describe 'POST #create' do
    context 'with member role' do
      before do
        token = JwtService.encode(user_id: user.id, role: user.role)
        request.headers['Authorization'] = "Bearer #{token}"
      end
      
      it 'creates article' do
        expect {
          post :create, params: { article: { title: 'Test', body: 'Content' } }
        }.to change(Article, :count).by(1)
      end
    end
    
    context 'with admin role' do
      before do
        token = JwtService.encode(user_id: admin.id, role: admin.role)
        request.headers['Authorization'] = "Bearer #{token}"
      end
      
      it 'creates article with admin privileges' do
        post :create, params: { 
          article: { title: 'Test', body: 'Content', featured: true } 
        }
        
        expect(Article.last.featured).to be true
      end
    end
  end
end

Security-focused tests verify protection against common vulnerabilities like algorithm confusion and token tampering.

RSpec.describe 'JWT Security' do
  let(:secret) { 'secure_secret_key' }
  let(:public_key) { OpenSSL::PKey::RSA.generate(2048).public_key }
  
  it 'rejects tokens with algorithm none' do
    payload = { user_id: 123 }
    header = { alg: 'none' }
    
    # Manually construct token with 'none' algorithm
    segments = [
      Base64.urlsafe_encode64(header.to_json, padding: false),
      Base64.urlsafe_encode64(payload.to_json, padding: false),
      ''
    ]
    malicious_token = segments.join('.')
    
    expect {
      JWT.decode(malicious_token, secret, true, { algorithm: 'HS256' })
    }.to raise_error(JWT::IncorrectAlgorithm)
  end
  
  it 'rejects tokens signed with different secret' do
    token = JWT.encode({ user_id: 123 }, 'wrong_secret', 'HS256')
    
    expect {
      JWT.decode(token, secret, true, { algorithm: 'HS256' })
    }.to raise_error(JWT::VerificationError)
  end
  
  it 'rejects tampered payloads' do
    token = JWT.encode({ user_id: 123, role: 'member' }, secret, 'HS256')
    
    # Tamper with payload
    parts = token.split('.')
    tampered_payload = Base64.urlsafe_encode64(
      { user_id: 123, role: 'admin' }.to_json,
      padding: false
    )
    tampered_token = [parts[0], tampered_payload, parts[2]].join('.')
    
    expect {
      JWT.decode(tampered_token, secret, true, { algorithm: 'HS256' })
    }.to raise_error(JWT::VerificationError)
  end
  
  it 'validates issuer claim' do
    token = JWT.encode(
      { user_id: 123, iss: 'evil.example.com' },
      secret,
      'HS256'
    )
    
    expect {
      JWT.decode(token, secret, true, {
        algorithm: 'HS256',
        iss: 'auth.example.com',
        verify_iss: true
      })
    }.to raise_error(JWT::InvalidIssuerError)
  end
  
  it 'validates audience claim' do
    token = JWT.encode(
      { user_id: 123, aud: 'other-service.example.com' },
      secret,
      'HS256'
    )
    
    expect {
      JWT.decode(token, secret, true, {
        algorithm: 'HS256',
        aud: 'api.example.com',
        verify_aud: true
      })
    }.to raise_error(JWT::InvalidAudError)
  end
end

Integration tests confirm complete authentication workflows including login, token usage, and refresh token operations.

RSpec.describe 'Authentication flow', type: :request do
  let(:user) { create(:user, email: 'test@example.com', password: 'password123') }
  
  describe 'POST /auth/login' do
    it 'returns tokens for valid credentials' do
      post '/auth/login', params: {
        email: user.email,
        password: 'password123'
      }
      
      expect(response).to have_http_status(:ok)
      json = JSON.parse(response.body)
      expect(json['access_token']).to be_present
      expect(json['refresh_token']).to be_present
    end
    
    it 'rejects invalid credentials' do
      post '/auth/login', params: {
        email: user.email,
        password: 'wrong_password'
      }
      
      expect(response).to have_http_status(:unauthorized)
    end
  end
  
  describe 'Token refresh flow' do
    let(:tokens) { TokenService.generate_tokens(user) }
    
    it 'issues new access token with valid refresh token' do
      post '/auth/refresh', params: {
        refresh_token: tokens[:refresh_token]
      }
      
      expect(response).to have_http_status(:ok)
      json = JSON.parse(response.body)
      expect(json['access_token']).to be_present
      expect(json['access_token']).not_to eq(tokens[:access_token])
    end
    
    it 'rejects expired refresh tokens' do
      expired_token = JWT.encode(
        { user_id: user.id, type: 'refresh', exp: 1.day.ago.to_i },
        TokenService::SECRET,
        'HS256'
      )
      
      post '/auth/refresh', params: { refresh_token: expired_token }
      expect(response).to have_http_status(:unauthorized)
    end
  end
end

Reference

Standard Claims

Claim Name Type Description
iss Issuer String Identifies principal that issued the token
sub Subject String Identifies principal that is the subject of the token
aud Audience String or Array Identifies recipients that the token is intended for
exp Expiration Time NumericDate Time after which the token must not be accepted
nbf Not Before NumericDate Time before which the token must not be accepted
iat Issued At NumericDate Time at which the token was issued
jti JWT ID String Unique identifier for the token

Signing Algorithms

Algorithm Type Key Type Description
HS256 HMAC Symmetric HMAC using SHA-256 hash algorithm
HS384 HMAC Symmetric HMAC using SHA-384 hash algorithm
HS512 HMAC Symmetric HMAC using SHA-512 hash algorithm
RS256 RSA Asymmetric RSASSA-PKCS1-v1_5 using SHA-256
RS384 RSA Asymmetric RSASSA-PKCS1-v1_5 using SHA-384
RS512 RSA Asymmetric RSASSA-PKCS1-v1_5 using SHA-512
ES256 ECDSA Asymmetric ECDSA using P-256 curve and SHA-256
ES384 ECDSA Asymmetric ECDSA using P-384 curve and SHA-384
ES512 ECDSA Asymmetric ECDSA using P-521 curve and SHA-512

JWT.encode Parameters

Parameter Type Required Description
payload Hash Yes Claims and data to encode in token
key String or Key Object Yes Secret key or private key for signing
algorithm String Yes Signing algorithm (HS256, RS256, etc)
header_fields Hash No Additional header fields to include

JWT.decode Parameters

Parameter Type Required Description
token String Yes JWT token to decode and verify
key String or Key Object Yes Secret key or public key for verification
verify Boolean Yes Whether to verify signature (always use true)
options Hash No Verification options and claim validators

Verification Options

Option Type Default Description
algorithm String or Array none Required algorithm(s) for verification
verify_expiration Boolean true Verify expiration claim
verify_not_before Boolean true Verify not-before claim
verify_iss Boolean false Verify issuer claim
verify_aud Boolean false Verify audience claim
verify_iat Boolean false Verify issued-at claim
verify_jti Proc nil Custom JWT ID validator
iss String nil Expected issuer value
aud String or Array nil Expected audience value(s)
exp_leeway Integer 0 Seconds of leeway for expiration check
nbf_leeway Integer 0 Seconds of leeway for not-before check

Common Error Classes

Error Class Raised When
JWT::DecodeError Base class for all JWT decode errors
JWT::VerificationError Signature verification fails
JWT::ExpiredSignature Token expiration time has passed
JWT::IncorrectAlgorithm Algorithm doesn't match expected algorithm
JWT::InvalidIssuerError Issuer claim doesn't match expected value
JWT::InvalidAudError Audience claim doesn't match expected value
JWT::InvalidSubError Subject claim doesn't match expected value
JWT::InvalidJtiError JWT ID claim validation fails
JWT::InvalidIatError Issued-at claim validation fails
JWT::ImmatureSignature Current time is before not-before time

Token Lifecycle Best Practices

Stage Recommendation
Generation Use strong secrets (32+ bytes), set expiration, include minimal claims
Storage Use httpOnly cookies or localStorage with appropriate security model
Transmission Always use HTTPS, include in Authorization header
Validation Verify signature, check expiration, validate issuer and audience
Refresh Implement refresh tokens with rotation, store in database with revocation
Revocation Maintain blacklist or whitelist for critical security events

Security Checklist

Check Status
Algorithm explicitly specified in decode
Signature verification enabled
Expiration claim present and validated
Strong secret with sufficient entropy
No sensitive data in payload
HTTPS enforced for token transmission
Issuer and audience validated
Token revocation mechanism implemented
Refresh tokens stored securely
Clock skew tolerance configured