CrackedRuby CrackedRuby

Overview

OAuth 2.0 provides an authorization framework that applications use to obtain limited access to user accounts on HTTP services. The protocol defines methods for third-party applications to access server resources on behalf of a resource owner without exposing credentials. OAuth replaced OAuth 1.0, addressing security vulnerabilities and implementation complexity.

OpenID Connect (OIDC) extends OAuth 2.0 to add an identity layer. While OAuth handles authorization (what a user can do), OpenID Connect handles authentication (who the user is). OpenID Connect returns standardized identity information as JSON Web Tokens (JWTs), allowing clients to verify user identity and obtain basic profile information.

The protocols separate concerns across distinct roles: resource owners (users), clients (applications), authorization servers (issue tokens), and resource servers (host protected resources). This separation enables flexible integration patterns while maintaining security boundaries.

OAuth and OpenID Connect dominate modern web authentication. Social login buttons, API access tokens, mobile app authentication, and single sign-on systems rely on these protocols. The specification's flexibility accommodates web applications, mobile apps, server-to-server communication, and IoT devices.

# Basic OAuth 2.0 authorization code flow
require 'oauth2'

client = OAuth2::Client.new(
  'client_id',
  'client_secret',
  site: 'https://provider.com',
  authorize_url: '/oauth/authorize',
  token_url: '/oauth/token'
)

# Redirect user to authorization URL
auth_url = client.auth_code.authorize_url(
  redirect_uri: 'https://app.com/callback',
  scope: 'read_profile'
)

# After user authorizes, exchange code for token
token = client.auth_code.get_token(
  params[:code],
  redirect_uri: 'https://app.com/callback'
)
# => #<OAuth2::AccessToken>

Key Principles

OAuth 2.0 operates on token-based authorization. Instead of sharing passwords, resource owners grant limited access through tokens. Access tokens carry authorization data with defined scopes and expiration times. Refresh tokens enable obtaining new access tokens without repeated user interaction.

The authorization server issues tokens after verifying the resource owner's consent. Tokens contain no credential information but reference authorization grants stored server-side. Resource servers validate tokens before granting access, checking both authenticity and scope permissions.

Scopes define permission boundaries. A scope like read:profile grants read access to profile data, while write:posts allows creating posts. Applications request specific scopes during authorization. Users see requested permissions and approve or deny access. The authorization server issues tokens limited to approved scopes.

Grant types determine how clients obtain tokens. The authorization code grant suits web applications with server-side components. The implicit grant served browser-based applications but faces security concerns. The client credentials grant enables server-to-server communication. The resource owner password credentials grant applies when other flows cannot work but requires high trust levels.

OpenID Connect introduces ID tokens alongside access tokens. ID tokens contain claims about the authenticated user encoded as JWTs. Standard claims include sub (subject identifier), iss (issuer), aud (audience), and exp (expiration). Custom claims carry additional user attributes.

# OpenID Connect adds ID token to OAuth flow
require 'openid_connect'

client = OpenIDConnect::Client.new(
  identifier: 'client_id',
  secret: 'client_secret',
  authorization_endpoint: 'https://provider.com/authorize',
  token_endpoint: 'https://provider.com/token',
  userinfo_endpoint: 'https://provider.com/userinfo'
)

# Authorization includes openid scope
authorization_uri = client.authorization_uri(
  scope: [:openid, :profile, :email],
  state: SecureRandom.hex(16),
  nonce: SecureRandom.hex(16)
)

# Token response includes id_token
client.authorization_code = params[:code]
access_token = client.access_token!
# => Returns both access_token and id_token

# Decode and verify ID token
id_token = OpenIDConnect::ResponseObject::IdToken.decode(
  access_token.id_token,
  client.secret
)
# => #<OpenIDConnect::ResponseObject::IdToken>

Token validation prevents security vulnerabilities. ID tokens require signature verification using the provider's public key. Clients check the iss claim matches the expected issuer and the aud claim includes the client's identifier. The exp claim must exceed current time. The nonce claim must match the value sent in the authorization request.

State parameters protect against CSRF attacks. Clients generate random state values before redirecting to authorization endpoints. After authorization, clients verify the returned state matches the original value. Attackers cannot forge valid state parameters without intercepting the initial request.

Discovery documents simplify provider integration. OpenID Connect providers publish configuration at /.well-known/openid-configuration. The document lists endpoint URLs, supported grant types, signing algorithms, and available claims. Clients fetch this document to configure themselves dynamically.

# OpenID Connect discovery
discovery = OpenIDConnect::Discovery::Provider::Config.discover!(
  'https://accounts.google.com'
)

discovery.authorization_endpoint
# => "https://accounts.google.com/o/oauth2/v2/auth"

discovery.token_endpoint
# => "https://accounts.google.com/oauth2/v4/token"

discovery.jwks_uri
# => "https://www.googleapis.com/oauth2/v3/certs"

Implementation Approaches

Authorization Code Flow provides the strongest security for web applications. The client redirects users to the authorization server, which authenticates the user and displays a consent screen. After approval, the authorization server redirects back with an authorization code. The client exchanges this code for tokens via a back-channel request. This approach prevents token exposure in browser history or logs.

The flow requires server-side code to keep client secrets confidential. Single-page applications cannot safely use this flow without a backend proxy. The authorization code itself provides minimal risk if intercepted since it requires the client secret for token exchange.

# Authorization code flow implementation
class OAuthController < ApplicationController
  def authorize
    client = oauth_client
    session[:oauth_state] = SecureRandom.hex(16)
    
    redirect_to client.auth_code.authorize_url(
      redirect_uri: callback_url,
      scope: 'read write',
      state: session[:oauth_state]
    ), allow_other_host: true
  end
  
  def callback
    # Verify state parameter
    if params[:state] != session[:oauth_state]
      return render json: { error: 'Invalid state' }, status: 400
    end
    
    # Exchange code for token
    client = oauth_client
    token = client.auth_code.get_token(
      params[:code],
      redirect_uri: callback_url
    )
    
    session[:access_token] = token.token
    session[:refresh_token] = token.refresh_token
    
    redirect_to dashboard_path
  end
  
  private
  
  def oauth_client
    OAuth2::Client.new(
      ENV['OAUTH_CLIENT_ID'],
      ENV['OAUTH_CLIENT_SECRET'],
      site: ENV['OAUTH_PROVIDER_URL']
    )
  end
end

Authorization Code Flow with PKCE adapts the authorization code flow for public clients. PKCE (Proof Key for Code Exchange) eliminates the client secret requirement. The client generates a random code verifier, then creates a code challenge by hashing the verifier. The authorization request includes the code challenge. The token request includes the original code verifier. The server verifies the verifier produces the challenge, proving the same client made both requests.

Mobile applications and single-page applications use PKCE to prevent authorization code interception attacks. The approach maintains authorization code flow security without requiring client secrets in public clients.

# PKCE flow for public clients
require 'digest'
require 'base64'

# Generate code verifier
code_verifier = SecureRandom.urlsafe_base64(32)
session[:code_verifier] = code_verifier

# Create code challenge
code_challenge = Base64.urlsafe_encode64(
  Digest::SHA256.digest(code_verifier),
  padding: false
)

# Authorization request includes challenge
auth_url = client.auth_code.authorize_url(
  redirect_uri: callback_url,
  scope: 'read',
  code_challenge: code_challenge,
  code_challenge_method: 'S256'
)

# Token request includes verifier
token = client.auth_code.get_token(
  params[:code],
  redirect_uri: callback_url,
  code_verifier: session[:code_verifier]
)

Client Credentials Flow enables server-to-server authentication without user involvement. The client authenticates directly with the authorization server using its credentials. The server issues an access token for the client's own resources or resources it manages. This flow suits background jobs, API integrations, and service accounts.

The approach assumes the client securely stores credentials. Tokens represent the client's identity rather than a user's identity. Scopes limit the client's capabilities but no user consent occurs.

Implicit Flow allowed browser-based applications to receive tokens directly without an authorization code exchange. The authorization server returns access tokens in the redirect URI fragment. Security concerns around token exposure in browser history and lack of client authentication led to PKCE replacing this flow for most use cases. The OAuth 2.0 Security Best Current Practice recommends against using implicit flow.

Ruby Implementation

Ruby applications integrate OAuth and OpenID Connect through several gems. The oauth2 gem provides low-level OAuth 2.0 protocol support. The omniauth gem offers a standardized interface for multiple authentication providers. The openid_connect gem implements OpenID Connect specifications.

OAuth2 Gem handles token requests, token refresh, and authenticated API calls. The gem defines client objects representing OAuth providers and token objects representing authorized sessions.

# Complete OAuth 2.0 implementation with error handling
require 'oauth2'

class OAuthService
  TOKEN_OPTIONS = {
    mode: :body,
    header_format: 'Bearer %s'
  }.freeze
  
  def initialize(provider_config)
    @client = OAuth2::Client.new(
      provider_config[:client_id],
      provider_config[:client_secret],
      site: provider_config[:site],
      token_url: provider_config[:token_url],
      authorize_url: provider_config[:authorize_url],
      token_method: :post
    )
  end
  
  def authorization_url(redirect_uri, scope, state)
    @client.auth_code.authorize_url(
      redirect_uri: redirect_uri,
      scope: scope,
      state: state
    )
  end
  
  def exchange_code(code, redirect_uri)
    @client.auth_code.get_token(
      code,
      redirect_uri: redirect_uri
    )
  rescue OAuth2::Error => e
    handle_oauth_error(e)
  end
  
  def refresh_token(refresh_token_string)
    token = OAuth2::AccessToken.from_hash(
      @client,
      refresh_token: refresh_token_string
    )
    token.refresh!
  rescue OAuth2::Error => e
    handle_oauth_error(e)
  end
  
  def make_api_call(access_token_string, endpoint)
    token = OAuth2::AccessToken.new(@client, access_token_string)
    response = token.get(endpoint)
    JSON.parse(response.body)
  rescue OAuth2::Error => e
    handle_oauth_error(e)
  end
  
  private
  
  def handle_oauth_error(error)
    case error.code
    when 'invalid_grant'
      raise AuthorizationExpiredError, 'Token expired or invalid'
    when 'invalid_client'
      raise AuthenticationError, 'Invalid client credentials'
    when 'access_denied'
      raise AccessDeniedError, 'User denied access'
    else
      raise OAuthError, "OAuth error: #{error.message}"
    end
  end
end

OmniAuth abstracts provider differences behind a common interface. Rails applications mount OmniAuth as Rack middleware. After successful authentication, OmniAuth populates request.env['omniauth.auth'] with standardized user data.

# OmniAuth configuration for multiple providers
# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :google_oauth2,
    ENV['GOOGLE_CLIENT_ID'],
    ENV['GOOGLE_CLIENT_SECRET'],
    scope: 'email,profile',
    prompt: 'select_account'
  
  provider :github,
    ENV['GITHUB_CLIENT_ID'],
    ENV['GITHUB_CLIENT_SECRET'],
    scope: 'user:email'
  
  provider :oauth2,
    ENV['CUSTOM_CLIENT_ID'],
    ENV['CUSTOM_CLIENT_SECRET'],
    client_options: {
      site: ENV['CUSTOM_PROVIDER_URL'],
      authorize_url: '/oauth/authorize',
      token_url: '/oauth/token'
    }
end

# Controller handling OmniAuth callback
class SessionsController < ApplicationController
  def create
    auth_hash = request.env['omniauth.auth']
    
    user = User.find_or_create_by(
      provider: auth_hash['provider'],
      uid: auth_hash['uid']
    ) do |u|
      u.email = auth_hash.dig('info', 'email')
      u.name = auth_hash.dig('info', 'name')
      u.oauth_token = auth_hash.dig('credentials', 'token')
      u.oauth_refresh_token = auth_hash.dig('credentials', 'refresh_token')
      u.oauth_expires_at = Time.at(auth_hash.dig('credentials', 'expires_at'))
    end
    
    session[:user_id] = user.id
    redirect_to root_path
  end
  
  def failure
    flash[:error] = params[:message]
    redirect_to login_path
  end
end

OpenID Connect Gem implements ID token validation, userinfo endpoint requests, and discovery protocol support. The gem handles JWT signature verification and claim validation automatically.

# OpenID Connect implementation with ID token validation
require 'openid_connect'
require 'json/jwt'

class OpenIDService
  def initialize
    @discovery = OpenIDConnect::Discovery::Provider::Config.discover!(
      ENV['OIDC_ISSUER']
    )
    
    @client = OpenIDConnect::Client.new(
      identifier: ENV['OIDC_CLIENT_ID'],
      secret: ENV['OIDC_CLIENT_SECRET'],
      authorization_endpoint: @discovery.authorization_endpoint,
      token_endpoint: @discovery.token_endpoint,
      userinfo_endpoint: @discovery.userinfo_endpoint
    )
  end
  
  def authorization_url(redirect_uri, state, nonce)
    @client.authorization_uri(
      scope: [:openid, :profile, :email],
      state: state,
      nonce: nonce,
      redirect_uri: redirect_uri
    )
  end
  
  def handle_callback(code, redirect_uri, expected_nonce)
    @client.authorization_code = code
    @client.redirect_uri = redirect_uri
    
    token_response = @client.access_token!
    
    # Decode ID token
    id_token = decode_id_token(
      token_response.id_token,
      expected_nonce
    )
    
    # Optionally fetch additional user info
    userinfo = token_response.userinfo!
    
    {
      id_token: id_token,
      userinfo: userinfo,
      access_token: token_response.access_token,
      refresh_token: token_response.refresh_token
    }
  end
  
  def decode_id_token(token_string, expected_nonce)
    # Fetch public keys from JWKS endpoint
    jwks = JSON::JWK::Set.new(
      JSON.parse(Net::HTTP.get(URI(@discovery.jwks_uri)))
    )
    
    # Decode and verify token
    id_token = JSON::JWT.decode(token_string, jwks)
    
    # Validate claims
    validate_id_token_claims(id_token, expected_nonce)
    
    id_token
  end
  
  private
  
  def validate_id_token_claims(id_token, expected_nonce)
    # Verify issuer
    unless id_token['iss'] == @discovery.issuer
      raise 'Invalid issuer'
    end
    
    # Verify audience
    unless id_token['aud'] == ENV['OIDC_CLIENT_ID']
      raise 'Invalid audience'
    end
    
    # Verify expiration
    if Time.at(id_token['exp']) < Time.now
      raise 'Token expired'
    end
    
    # Verify nonce
    unless id_token['nonce'] == expected_nonce
      raise 'Invalid nonce'
    end
  end
end

Token storage requires careful consideration. Session storage works for short-lived access tokens. Database storage suits applications needing tokens across sessions. Encrypted database columns protect tokens at rest. Never log tokens or include them in error reports.

# Secure token storage model
class OAuthCredential < ApplicationRecord
  belongs_to :user
  
  encrypts :access_token
  encrypts :refresh_token
  
  validates :provider, presence: true
  validates :access_token, presence: true
  
  def expired?
    expires_at && expires_at < Time.current
  end
  
  def refresh!
    service = OAuthService.new(provider_configuration)
    new_token = service.refresh_token(refresh_token)
    
    update!(
      access_token: new_token.token,
      refresh_token: new_token.refresh_token || refresh_token,
      expires_at: Time.at(new_token.expires_at)
    )
  end
  
  def provider_configuration
    # Load from config based on provider
    Rails.application.credentials.dig(:oauth, provider.to_sym)
  end
end

Security Implications

Token Leakage poses the primary OAuth security risk. Access tokens function like temporary passwords. Attackers gaining token access can impersonate users within the token's scope. Applications must protect tokens with the same rigor as passwords.

Transport security requires HTTPS for all OAuth communications. Authorization redirects, token exchanges, and API requests must use TLS. Mixed content warnings indicate security vulnerabilities. Certificate validation must remain enabled.

Redirect URI Validation prevents authorization code interception. Authorization servers must validate redirect URIs match pre-registered values exactly. Partial matches or substring validation create vulnerabilities. Open redirectors in OAuth clients enable attackers to receive authorization codes.

# Secure redirect URI validation
class OAuthProvider
  ALLOWED_REDIRECT_URIS = {
    'client_123' => [
      'https://app.example.com/callback',
      'https://app.example.com/oauth/callback'
    ],
    'client_456' => [
      'https://other.example.com/auth/complete'
    ]
  }.freeze
  
  def validate_redirect_uri(client_id, redirect_uri)
    allowed_uris = ALLOWED_REDIRECT_URIS[client_id]
    return false unless allowed_uris
    
    # Exact match required, no wildcards or patterns
    allowed_uris.include?(redirect_uri)
  end
  
  def authorize(client_id, redirect_uri, scope, state)
    unless validate_redirect_uri(client_id, redirect_uri)
      render json: { error: 'invalid_redirect_uri' }, status: 400
      return
    end
    
    # Continue authorization flow
  end
end

State Parameter CSRF Protection defends against cross-site request forgery. Attackers trick victims into clicking authorization URLs without state validation. The attacker's account becomes linked to the victim's OAuth credentials. Applications must generate cryptographically random state values and verify them on callback.

PKCE mitigates authorization code interception attacks. Mobile applications and public clients face code interception risks. PKCE ensures only the client that started the flow can exchange the code for tokens. All public clients should implement PKCE.

Token Expiration limits damage from leaked tokens. Short-lived access tokens reduce the window of exposure. Refresh tokens enable obtaining new access tokens without user interaction. Applications should use 15-60 minute access token lifetimes. Refresh tokens may live days or months depending on security requirements.

# Token expiration checking middleware
class TokenExpirationMiddleware
  def initialize(app)
    @app = app
  end
  
  def call(env)
    request = Rack::Request.new(env)
    user = current_user(request)
    
    if user && user.oauth_credential&.expired?
      begin
        user.oauth_credential.refresh!
      rescue AuthorizationExpiredError
        # Refresh token also expired, require re-authentication
        return [401, {}, ['Token expired, re-authentication required']]
      end
    end
    
    @app.call(env)
  end
  
  private
  
  def current_user(request)
    return nil unless request.session[:user_id]
    User.find_by(id: request.session[:user_id])
  end
end

Scope Minimization reduces risk exposure. Applications should request only required scopes. Users more readily approve minimal permission requests. Compromised tokens with limited scopes restrict attacker capabilities. Review scope requirements regularly.

Client Secret Protection applies to confidential clients. Secrets must never appear in client-side code, version control, or logs. Environment variables or secret management systems provide better storage. Rotate secrets after exposure or on a regular schedule.

ID Token Validation prevents token substitution attacks. Applications must verify signature, issuer, audience, and expiration. Nonce validation ensures the token was issued in response to the current authentication request. Skipping validation allows attackers to use tokens issued to different clients.

# Complete ID token validation
class IDTokenValidator
  def initialize(discovery_config, client_id)
    @discovery = discovery_config
    @client_id = client_id
    @jwks = fetch_jwks
  end
  
  def validate(token_string, expected_nonce)
    # Decode token
    id_token = decode_token(token_string)
    
    # Perform all validations
    validate_issuer(id_token)
    validate_audience(id_token)
    validate_expiration(id_token)
    validate_issued_at(id_token)
    validate_nonce(id_token, expected_nonce)
    
    id_token
  end
  
  private
  
  def decode_token(token_string)
    JSON::JWT.decode(token_string, @jwks)
  rescue JSON::JWT::Exception => e
    raise ValidationError, "Token decode failed: #{e.message}"
  end
  
  def validate_issuer(token)
    unless token['iss'] == @discovery.issuer
      raise ValidationError, 
        "Invalid issuer: expected #{@discovery.issuer}, got #{token['iss']}"
    end
  end
  
  def validate_audience(token)
    audiences = Array(token['aud'])
    unless audiences.include?(@client_id)
      raise ValidationError, "Token not intended for this client"
    end
  end
  
  def validate_expiration(token)
    exp = Time.at(token['exp'])
    if exp < Time.now
      raise ValidationError, "Token expired at #{exp}"
    end
  end
  
  def validate_issued_at(token)
    # Token should not be issued in the future
    iat = Time.at(token['iat'])
    if iat > Time.now + 60 # Allow 60 second clock skew
      raise ValidationError, "Token issued in the future"
    end
  end
  
  def validate_nonce(token, expected_nonce)
    unless token['nonce'] == expected_nonce
      raise ValidationError, "Nonce mismatch"
    end
  end
  
  def fetch_jwks
    jwks_response = Net::HTTP.get(URI(@discovery.jwks_uri))
    JSON::JWK::Set.new(JSON.parse(jwks_response))
  end
end

Token Storage requires encryption. Database-stored tokens should use field-level encryption. Session cookies containing tokens need secure and httponly flags. Never store tokens in localStorage due to XSS vulnerability. sessionStorage provides slightly better security but remains vulnerable to XSS.

Practical Examples

Social Login Implementation demonstrates complete OAuth integration for user authentication. Applications redirect users to a social provider, receive authorization codes, exchange codes for tokens, and create user accounts.

# Complete social login implementation
class SocialAuthController < ApplicationController
  skip_before_action :verify_authenticity_token, only: [:callback]
  
  def initiate
    provider_name = params[:provider]
    provider = oauth_provider(provider_name)
    
    state = SecureRandom.hex(16)
    session[:oauth_state] = state
    session[:oauth_provider] = provider_name
    
    auth_url = provider.authorization_url(
      callback_url,
      'email profile',
      state
    )
    
    redirect_to auth_url, allow_other_host: true
  end
  
  def callback
    # Validate state
    unless params[:state] == session[:oauth_state]
      flash[:error] = 'Invalid state parameter'
      return redirect_to login_path
    end
    
    # Handle errors
    if params[:error]
      flash[:error] = "Authentication failed: #{params[:error]}"
      return redirect_to login_path
    end
    
    # Exchange code for token
    provider = oauth_provider(session[:oauth_provider])
    token = provider.exchange_code(params[:code], callback_url)
    
    # Fetch user profile
    user_data = fetch_user_profile(token, session[:oauth_provider])
    
    # Find or create user
    user = User.find_or_initialize_by(
      provider: session[:oauth_provider],
      provider_uid: user_data[:uid]
    )
    
    user.assign_attributes(
      email: user_data[:email],
      name: user_data[:name],
      avatar_url: user_data[:avatar_url]
    )
    
    if user.save
      # Store tokens
      user.create_oauth_credential!(
        provider: session[:oauth_provider],
        access_token: token.token,
        refresh_token: token.refresh_token,
        expires_at: Time.at(token.expires_at)
      )
      
      session[:user_id] = user.id
      redirect_to dashboard_path
    else
      flash[:error] = 'Failed to create user account'
      redirect_to login_path
    end
  end
  
  private
  
  def oauth_provider(provider_name)
    config = Rails.application.credentials.dig(:oauth, provider_name.to_sym)
    OAuthService.new(config)
  end
  
  def fetch_user_profile(token, provider_name)
    case provider_name
    when 'google'
      fetch_google_profile(token)
    when 'github'
      fetch_github_profile(token)
    else
      raise "Unknown provider: #{provider_name}"
    end
  end
  
  def fetch_google_profile(token)
    response = token.get('https://www.googleapis.com/oauth2/v2/userinfo')
    data = JSON.parse(response.body)
    
    {
      uid: data['id'],
      email: data['email'],
      name: data['name'],
      avatar_url: data['picture']
    }
  end
  
  def fetch_github_profile(token)
    response = token.get('https://api.github.com/user')
    data = JSON.parse(response.body)
    
    {
      uid: data['id'].to_s,
      email: data['email'],
      name: data['name'] || data['login'],
      avatar_url: data['avatar_url']
    }
  end
  
  def callback_url
    oauth_callback_url(protocol: 'https')
  end
end

API Integration shows server-to-server OAuth using client credentials flow. Background jobs authenticate as service accounts to access external APIs.

# Service-to-service API integration
class ExternalAPIClient
  def initialize
    @client = OAuth2::Client.new(
      ENV['API_CLIENT_ID'],
      ENV['API_CLIENT_SECRET'],
      site: 'https://api.example.com',
      token_url: '/oauth/token'
    )
    
    @token = nil
  end
  
  def fetch_data(resource_path)
    ensure_valid_token!
    
    response = @token.get(resource_path)
    JSON.parse(response.body)
  rescue OAuth2::Error => e
    if e.code == 'invalid_token'
      # Token expired, refresh and retry
      @token = nil
      ensure_valid_token!
      retry
    else
      raise
    end
  end
  
  def create_resource(resource_path, data)
    ensure_valid_token!
    
    response = @token.post(resource_path) do |request|
      request.headers['Content-Type'] = 'application/json'
      request.body = data.to_json
    end
    
    JSON.parse(response.body)
  end
  
  private
  
  def ensure_valid_token!
    return if @token && !token_expired?
    
    @token = @client.client_credentials.get_token(
      scope: 'api:read api:write'
    )
    
    @token_expires_at = Time.now + @token.expires_in
  end
  
  def token_expired?
    @token_expires_at && @token_expires_at < Time.now + 60
  end
end

# Usage in background job
class DataSyncJob < ApplicationJob
  def perform(account_id)
    client = ExternalAPIClient.new
    
    # Fetch data from external service
    data = client.fetch_data("/accounts/#{account_id}/data")
    
    # Process and store locally
    Account.find(account_id).update!(
      external_data: data,
      last_synced_at: Time.current
    )
  end
end

Mobile App Authentication implements PKCE flow for native mobile applications without client secrets.

# PKCE flow for mobile app backend
class MobileAuthController < ApplicationController
  skip_before_action :verify_authenticity_token
  
  def initiate
    # Mobile app provides code challenge
    code_challenge = params[:code_challenge]
    code_challenge_method = params[:code_challenge_method]
    
    unless code_challenge && code_challenge_method == 'S256'
      return render json: { error: 'Missing or invalid PKCE parameters' },
        status: 400
    end
    
    # Store challenge for verification
    auth_request = AuthorizationRequest.create!(
      code_challenge: code_challenge,
      state: SecureRandom.hex(16),
      expires_at: 10.minutes.from_now
    )
    
    render json: {
      authorization_url: authorization_url(auth_request),
      state: auth_request.state
    }
  end
  
  def exchange
    code = params[:code]
    code_verifier = params[:code_verifier]
    state = params[:state]
    
    # Find authorization request
    auth_request = AuthorizationRequest.find_by(state: state)
    
    unless auth_request && !auth_request.expired?
      return render json: { error: 'Invalid or expired state' },
        status: 400
    end
    
    # Verify code verifier matches challenge
    computed_challenge = Base64.urlsafe_encode64(
      Digest::SHA256.digest(code_verifier),
      padding: false
    )
    
    unless computed_challenge == auth_request.code_challenge
      return render json: { error: 'Invalid code verifier' },
        status: 400
    end
    
    # Exchange code for tokens
    client = oauth_client
    token = client.auth_code.get_token(
      code,
      redirect_uri: mobile_callback_url,
      code_verifier: code_verifier
    )
    
    # Create session for mobile user
    user = find_or_create_user_from_token(token)
    
    render json: {
      access_token: token.token,
      refresh_token: token.refresh_token,
      expires_in: token.expires_in,
      user: user.as_json(only: [:id, :email, :name])
    }
  end
  
  private
  
  def authorization_url(auth_request)
    client = oauth_client
    client.auth_code.authorize_url(
      redirect_uri: mobile_callback_url,
      state: auth_request.state,
      code_challenge: auth_request.code_challenge,
      code_challenge_method: 'S256',
      scope: 'openid profile email'
    )
  end
end

Common Pitfalls

Insufficient Redirect URI Validation creates authorization code interception vulnerabilities. Applications accepting any redirect URI or performing substring matching allow attackers to register similar domains and capture codes. Always validate redirect URIs exactly against pre-registered values. Never use pattern matching or wildcards.

Missing State Parameter Validation enables CSRF attacks. Attackers craft authorization URLs without state parameters or with predictable state values. Victims clicking these links authorize the attacker's application. Applications must generate cryptographically random state values and verify them on callback. Never skip state validation even if the OAuth provider makes it optional.

# Common state validation mistakes
class AuthController < ApplicationController
  # WRONG: No state generation
  def authorize_bad
    redirect_to client.auth_code.authorize_url(
      redirect_uri: callback_url
      # Missing state parameter
    )
  end
  
  # WRONG: Predictable state
  def authorize_bad_predictable
    state = "user_#{current_user.id}"  # Predictable
    redirect_to client.auth_code.authorize_url(
      redirect_uri: callback_url,
      state: state
    )
  end
  
  # WRONG: No state verification
  def callback_bad
    token = client.auth_code.get_token(params[:code])
    # Should verify params[:state] matches session
  end
  
  # CORRECT: Random state with verification
  def authorize_correct
    state = SecureRandom.hex(32)
    session[:oauth_state] = state
    
    redirect_to client.auth_code.authorize_url(
      redirect_uri: callback_url,
      state: state
    )
  end
  
  def callback_correct
    unless params[:state] == session.delete(:oauth_state)
      return render json: { error: 'Invalid state' }, status: 400
    end
    
    token = client.auth_code.get_token(params[:code])
  end
end

Token Storage in Browser Storage exposes tokens to XSS attacks. LocalStorage and sessionStorage remain accessible to any JavaScript executing on the page. XSS vulnerabilities allow attackers to steal tokens. Store tokens in httponly, secure cookies or server-side sessions. Never expose tokens to client-side JavaScript.

Implicit Flow in SPAs creates token leakage risks. The implicit flow returns tokens in URL fragments visible in browser history, logs, and referrer headers. Authorization servers adopted PKCE with authorization code flow for single-page applications. Migrate applications from implicit flow to authorization code flow with PKCE.

Ignoring Token Expiration allows indefinite token usage. Applications that never check token expiration continue using expired tokens until the resource server rejects them. Check expires_at values and proactively refresh tokens before expiration. Handle refresh token expiration by redirecting to re-authentication.

Overly Broad Scopes violate the principle of least privilege. Requesting excessive permissions reduces user trust and increases damage from token compromise. Request only the scopes required for current operations. Applications can request additional scopes later when needed. Review scope requirements regularly.

# Scope management best practices
class APIController < ApplicationController
  REQUIRED_SCOPES = {
    profile: ['read:profile'],
    posts: ['read:posts', 'write:posts'],
    admin: ['read:users', 'write:users', 'delete:users']
  }.freeze
  
  before_action :verify_scope
  
  def profile
    # Requires read:profile scope
  end
  
  def create_post
    # Requires write:posts scope
  end
  
  private
  
  def verify_scope
    required = REQUIRED_SCOPES[action_name.to_sym]
    return true unless required
    
    token_scopes = current_token.scopes
    
    unless required.all? { |scope| token_scopes.include?(scope) }
      render json: { error: 'Insufficient scope' }, status: 403
    end
  end
  
  def current_token
    # Extract and validate token from Authorization header
    token_string = request.headers['Authorization']&.sub('Bearer ', '')
    TokenValidator.validate(token_string)
  end
end

Exposing Client Secrets in public clients compromises all users. Mobile apps and single-page applications cannot protect client secrets. Decompiling mobile apps or inspecting browser code reveals embedded secrets. Use PKCE for public clients instead of client secrets. Never embed secrets in client-side code.

Skipping ID Token Validation allows token substitution attacks. Applications accepting ID tokens without signature verification let attackers submit tokens issued to different clients. Applications skipping issuer validation accept tokens from malicious providers. Perform all validation steps: signature, issuer, audience, expiration, and nonce. Never trust tokens without full validation.

Insecure Token Transmission leaks tokens through network interception. Applications using HTTP instead of HTTPS expose tokens during transmission. Mixed content warnings indicate partial HTTPS implementation. Enforce HTTPS for all OAuth endpoints and API calls. Configure HSTS headers to prevent protocol downgrade attacks.

Refresh Token Misuse creates persistent access vulnerabilities. Applications storing refresh tokens client-side enable long-term token theft. Refresh tokens grant new access tokens without user interaction. Store refresh tokens server-side with encryption. Implement refresh token rotation where each refresh request returns a new refresh token and invalidates the old one.

Reference

OAuth 2.0 Grant Types

Grant Type Use Case Client Type User Involvement Token Types
Authorization Code Web applications with backend Confidential Yes Access + Refresh
Authorization Code + PKCE Mobile apps, SPAs Public Yes Access + Refresh
Client Credentials Service-to-service Confidential No Access only
Device Code Limited input devices Public Yes Access + Refresh
Resource Owner Password Legacy migration only Confidential Yes Access + Refresh

OpenID Connect Scopes

Scope Purpose Claims Returned
openid Required for OIDC sub
profile User profile data name, family_name, given_name, picture, etc.
email Email address email, email_verified
address Physical address address object
phone Phone number phone_number, phone_number_verified

Standard ID Token Claims

Claim Type Description Required
iss String Issuer identifier Yes
sub String Subject identifier Yes
aud String or Array Audience (client ID) Yes
exp Number Expiration time (Unix timestamp) Yes
iat Number Issued at time Yes
auth_time Number Authentication time If requested
nonce String Request nonce If sent in request
acr String Authentication context class reference Optional
amr Array Authentication methods references Optional
azp String Authorized party If multiple audiences

Token Response Fields

Field Description Token Type
access_token The access token string All grants
token_type Usually "Bearer" All grants
expires_in Seconds until expiration All grants
refresh_token Refresh token string Some grants
scope Granted scopes (space-separated) All grants
id_token JWT identity token OIDC only

Error Response Codes

Error Code Description Typical Cause
invalid_request Malformed request Missing required parameter
invalid_client Client authentication failed Wrong client credentials
invalid_grant Authorization grant invalid Expired or used code
unauthorized_client Client not authorized for grant type Configuration mismatch
unsupported_grant_type Grant type not supported Requesting disabled grant
invalid_scope Requested scope invalid Unknown or unauthorized scope
access_denied User denied authorization User clicked "Deny"
temporarily_unavailable Server temporarily unavailable Retry later

PKCE Parameters

Parameter Location Description Format
code_challenge Authorization request Hashed verifier Base64URL(SHA256(verifier))
code_challenge_method Authorization request Hash algorithm S256 or plain
code_verifier Token request Original random string 43-128 characters

Common OAuth Endpoints

Endpoint Method Purpose Authentication
/oauth/authorize GET Start authorization flow User login
/oauth/token POST Exchange code for tokens Client credentials
/oauth/revoke POST Revoke token Client credentials
/oauth/introspect POST Check token validity Client credentials
/.well-known/openid-configuration GET Discovery document None

HTTP Headers

Header Value Purpose
Authorization Bearer ACCESS_TOKEN Authenticate API requests
Content-Type application/x-www-form-urlencoded Token request format
Content-Type application/json API request/response format
Cache-Control no-store Prevent token caching
Pragma no-cache HTTP/1.0 cache prevention

Security Checklist

Item Description Priority
HTTPS only All OAuth communications use TLS Critical
Validate redirect URIs Exact match against registered URIs Critical
State parameter Random state in all flows Critical
PKCE for public clients Use code challenge/verifier Critical
Verify ID token signature Validate JWT signature Critical
Check token expiration Validate exp claim High
Validate issuer Check iss claim High
Validate audience Check aud claim High
Verify nonce Match nonce in ID token High
Short access token lifetime 15-60 minutes recommended Medium
Encrypt stored tokens Field-level encryption Medium
Implement token rotation New refresh token per refresh Medium
Scope minimization Request minimum required scopes Medium
Secure client secrets Environment variables or vault High