~/src/www.mokhan.ca/xlgmokha [main]
cat authentication-authorization-guide.md
authentication-authorization-guide.md 52439 bytes | 2019-01-01 12:00
symlink: /dev/eng/authentication-authorization-guide.md

Authentication & Authorization Guide

This is a comprehensive collection of notes covering modern authentication and authorization protocols.

OAuth 2.0

The OAuth 2.0 authorization framework enables a third-party application to obtain limited access to an HTTP service, either on behalf of a resource owner by orchestrating an approval interaction between the resource owner and the HTTP service, or by allowing the third-party application to obtain access on its own behalf. - RFC-6749

Core RFC Documents

OAuth 2.0 Roles

  1. Resource Owner: An entity capable of granting access to a protected resource (typically the end-user)
  2. Resource Server: The server hosting protected resources, capable of accepting and responding to protected resource requests using access tokens
  3. Client: An application making protected resource requests on behalf of the resource owner
  4. Authorization Server: The server issuing access tokens to the client after successfully authenticating the resource owner

Authorization Flows

Authorization Code Flow

The most secure flow for web applications. Here’s a production implementation from my OAuth2 server:

# Authorization endpoint - validates client and redirects
class Oauth::AuthorizationsController < ApplicationController
  def show
    redirect_to(error_url_for('unsupported_response_type')) && return unless %w[code token].include?(response_type)
    redirect_to(error_url_for('invalid_client')) && return unless client&.active?
    redirect_to(error_url_for('invalid_request')) && return unless client.redirect_uri_matches?(redirect_uri)

    session[:oauth] = secure_params.to_h
  end

  def create
    oauth = session[:oauth]&.with_indifferent_access
    authorization = current_user.authorizations.create!(
      client: client,
      response_type: oauth[:response_type],
      redirect_uri: oauth[:redirect_uri],
      scope: oauth[:scope],
      state: oauth[:state]
    )
    redirect_to authorization.redirect_url_for(client), allow_other_host: true
  end
end

Token exchange endpoint:

# Token endpoint - exchanges authorization code for access token
def authorization_code_grant(code, code_verifier)
  authorization = current_client.authorizations.active.find_by!(code: code)
  return { error: 'invalid_grant' } unless authorization.valid_verifier?(code_verifier)
  
  authorization.issue_tokens_to(current_client)
end

Client Credentials Flow

For server-to-server communication. Implementation from production:

# Simple but secure client credentials grant
def client_credentials_grant
  current_client.issue_tokens_to(current_client)
end

# Client authentication using HTTP Basic Auth
def authenticate_client!
  @current_client = authenticate_with_http_basic do |client_id, client_secret|
    Client.find_by(client_id: client_id)&.authenticate(client_secret)
  end
  
  head :unauthorized unless @current_client
end

Token Exchange (RFC-8693)

Enables secure token exchange scenarios for complex distributed systems.

JWT Token Management

Production JWT token implementation from my OAuth2 server:

# app/models/token.rb
class Token < ApplicationRecord
  enum token_type: { access: 0, refresh: 1 }
  
  belongs_to :authorization
  belongs_to :subject, polymorphic: true
  belongs_to :audience, polymorphic: true

  def revoke!
    update!(revoked_at: Time.current)
    Rails.cache.write("revoked:#{jti}", true, expires_in: expires_in)
  end

  def claims
    {
      iss: Rails.application.routes.url_helpers.root_url,
      sub: subject.to_param,
      aud: audience.entity_id,
      exp: expires_at.to_i,
      nbf: created_at.to_i,
      iat: created_at.to_i,
      jti: jti,
      scope: authorization.scope
    }
  end

  def to_jwt
    JWT.encode(claims, Saml::Kit.configuration.private_keys.first, 'RS256')
  end

  def self.issue_tokens_to(subject, client)
    authorization = Authorization.find_or_create_by(subject: subject, client: client)
    
    tokens = {
      access_token: authorization.tokens.create!(
        token_type: :access,
        subject: subject,
        audience: client,
        expires_at: 1.hour.from_now
      ),
      refresh_token: authorization.tokens.create!(
        token_type: :refresh,
        subject: subject,
        audience: client,
        expires_at: 1.day.from_now
      )
    }

    {
      access_token: tokens[:access_token].to_jwt,
      refresh_token: tokens[:refresh_token].to_jwt,
      token_type: 'Bearer',
      expires_in: 3600
    }
  end

  def self.authenticate(token)
    claims = JWT.decode(token, public_key, true, algorithm: 'RS256').first
    return nil if revoked?(claims['jti'])
    
    find(claims['jti'])
  rescue JWT::DecodeError
    nil
  end

  def self.revoked?(jti)
    Rails.cache.exist?("revoked:#{jti}")
  end
end

Multi-Factor Authentication (MFA)

TOTP-based MFA implementation using the rotp gem:

# app/models/mfa.rb
class Mfa < ApplicationRecord
  belongs_to :user

  def setup?
    secret.present?
  end

  def build_secret
    self.secret = ROTP::Base32.random
  end

  def provisioning_uri(email)
    totp.provisioning_uri(email)
  end

  def disable!(current_password)
    return false unless user.authenticate(current_password)
    
    destroy!
  end

  def authenticate(code)
    return false unless setup?
    
    totp.verify(code, drift_behind: 15, drift_ahead: 15)
  end

  def valid_session?(session_token)
    return true unless setup?
    
    session_token.present? && 
      authenticate(session_token) && 
      !used_recently?(session_token)
  end

  private

  def totp
    @totp ||= ROTP::TOTP.new(secret, issuer: 'saml-kit')
  end

  def used_recently?(code)
    # Prevent replay attacks by tracking used codes
    Rails.cache.exist?("mfa:used:#{user.id}:#{code}")
  end
end

OAuth2 Client Management

Production OAuth2 client registration and management:

# app/models/client.rb
class Client < ApplicationRecord
  has_many :authorizations, dependent: :destroy
  has_many :tokens, through: :authorizations

  validates :client_name, presence: true
  validates :redirect_uris, presence: true

  def grant_types
    %w[authorization_code refresh_token client_credentials]
  end

  def response_types
    %w[code token]
  end

  def redirect_uri_matches?(uri)
    redirect_uris.include?(uri)
  end

  def authenticate(client_secret)
    ActiveSupport::SecurityUtils.secure_compare(
      self.client_secret,
      client_secret
    )
  end

  def issue_tokens_to(subject)
    Token.issue_tokens_to(subject, self)
  end

  def redirect_url_for(authorization)
    case authorization.response_type
    when 'code'
      "#{authorization.redirect_uri}?code=#{authorization.code}&state=#{authorization.state}"
    when 'token'
      access_token = authorization.issue_tokens_to(self)
      "#{authorization.redirect_uri}#access_token=#{access_token[:access_token]}&token_type=Bearer&expires_in=3600&state=#{authorization.state}"
    end
  end
end

Authorization Code and PKCE Implementation

Production PKCE (Proof Key for Code Exchange) validation:

# app/models/authorization.rb
class Authorization < ApplicationRecord
  belongs_to :user
  belongs_to :client
  has_many :tokens, dependent: :destroy

  scope :active, -> { where(revoked_at: nil).where('expires_at > ?', Time.current) }
  scope :expired, -> { where('expires_at <= ?', Time.current) }

  before_create :set_expiration

  def valid_verifier?(code_verifier)
    return true if code_challenge.blank?
    
    case code_challenge_method
    when 'S256'
      expected = Base64.urlsafe_encode64(
        Digest::SHA256.digest(code_verifier), 
        padding: false
      )
      ActiveSupport::SecurityUtils.secure_compare(code_challenge, expected)
    when 'plain'
      ActiveSupport::SecurityUtils.secure_compare(code_challenge, code_verifier)
    else
      false
    end
  end

  def issue_tokens_to(client)
    Token.issue_tokens_to(user, client)
  end

  def revoke!
    update!(revoked_at: Time.current)
    tokens.each(&:revoke!)
  end

  def redirect_url_for(client)
    client.redirect_url_for(self)
  end

  private

  def set_expiration
    self.expires_at = 10.minutes.from_now
  end
end

Security Considerations

  • Always use HTTPS for all OAuth flows
  • Implement PKCE for public clients
  • Validate redirect URIs strictly
  • Use short-lived access tokens with refresh tokens
  • Implement proper token storage and handling

Complete Implementation

The examples above are from a complete, working OAuth2 and SAML authentication server. You can explore the full implementation:

GitHub Repository: https://github.com/xlgmokha/proof

Key files to examine:

Clone and run locally:

git clone https://github.com/xlgmokha/proof.git
cd proof
bundle install
rails db:setup
rails server

This provides a complete reference implementation of modern authentication protocols in Ruby on Rails, including OAuth2, SAML, JWT, and MFA.

SAML 2.0

The Security Assertion Markup Language is an XML-based protocol for completing authentication.

SAML Participants

Each SAML transaction includes at least 3 parties:

  • Service Provider (SP): The application requesting authentication
  • User Agent: The user’s browser or client
  • Identity Provider (IDP): The authentication service

SAML Benefits

  • Single Sign-On (SSO) across multiple applications
  • Centralized identity management
  • Standards-based approach
  • Federation capabilities between organizations

SAML Flows

SP-Initiated Flow

@startuml
sp -> user_agent: AuthnRequest
user_agent -> idp: AuthnRequest
idp --> user_agent: Response
user_agent --> sp: Response
@enduml

  1. User accesses Service Provider
  2. SP generates SAML AuthnRequest
  3. SP redirects user to Identity Provider
  4. User authenticates with IDP
  5. IDP generates SAML Response
  6. IDP redirects user back to SP with Response
  7. SP validates Response and grants access

IDP-Initiated Flow

@startuml
idp -> user_agent: Response
user_agent -> sp: Response
@enduml

  1. User accesses Identity Provider directly
  2. User authenticates with IDP
  3. User selects target Service Provider
  4. IDP generates SAML Response
  5. IDP redirects user to SP with Response
  6. SP validates Response and grants access

SAML Metadata

SAML metadata describes the capabilities and configuration of SAML entities.

Service Provider Metadata

<?xml version="1.0"?>
<EntityDescriptor entityID="https://www.example.com/metadata" xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_9fd49a06-ee79-49a2-ba29-ddd4e3950bd2" >
  <SPSSODescriptor AuthnRequestsSigned="true" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
    <KeyDescriptor use="signing">
      <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
        <X509Data>
          <X509Certificate>x509 certificate</X509Certificate>
        </X509Data>
      </KeyInfo>
    </KeyDescriptor>
    <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat>
    <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://www.example.com/assertions" index="0" isDefault="true"/>
  </SPSSODescriptor>
</EntityDescriptor>

Identity Provider Metadata

<?xml version="1.0"?>
<EntityDescriptor entityID="https://www.example.org/metadata" xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_afa7d243-a1af-44a9-9a9b-83ed35f537ba">
  <IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
    <KeyDescriptor use="signing">
      <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
        <X509Data>
          <X509Certificate>x509 certificate</X509Certificate>
        </X509Data>
      </KeyInfo>
    </KeyDescriptor>
    <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat>
    <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://www.example.org/session/new"/>
    <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://www.example.org/session/new"/>
  </IDPSSODescriptor>
</EntityDescriptor>

SAML Messages

AuthnRequest

<?xml version="1.0"?>
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_2ff118b4-a178-46ac-82b7-374ee6314e02" Version="2.0" IssueInstant="2019-02-23T18:10:03Z" Destination="https://www.example.org/session/new" AssertionConsumerServiceURL="https://www.example.com/assertions">
  <saml:Issuer>https://www.example.com/metadata</saml:Issuer>
  <samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"/>
</samlp:AuthnRequest>

SAML Response

<?xml version="1.0"?>
<Response xmlns="urn:oasis:names:tc:SAML:2.0:protocol" ID="_8d623313-87ca-40a5-b299-694e8aaa5998" Version="2.0" IssueInstant="2019-02-23T18:10:25Z" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" Destination="https://www.example.com/assertions" InResponseTo="_2ff118b4-a178-46ac-82b7-374ee6314e02">
  <Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">https://www.example.org/metadata</Issuer>
  <Status>
    <StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
  </Status>
  <Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="_0d856f2f-fe06-4411-8bc8-163b17142e5c" IssueInstant="2019-02-23T18:10:25Z" Version="2.0">
    <Issuer>https://www.example.org/metadata</Issuer>
    <Subject>
      <NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">22446ce2-710a-4a81-bad0-f0e3c56d1c46</NameID>
      <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <SubjectConfirmationData InResponseTo="_2ff118b4-a178-46ac-82b7-374ee6314e02" Recipient="https://www.example.com/assertions" NotOnOrAfter="2019-02-23T18:15:25Z"/>
      </SubjectConfirmation>
    </Subject>
    <Conditions NotBefore="2019-02-23T18:10:25Z" NotOnOrAfter="2019-02-23T21:10:25Z">
      <AudienceRestriction>
        <Audience>https://www.example.com/metadata</Audience>
      </AudienceRestriction>
    </Conditions>
    <AuthnStatement AuthnInstant="2019-02-23T18:10:25Z" SessionIndex="_0d856f2f-fe06-4411-8bc8-163b17142e5c">
      <AuthnContext>
        <AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef>
      </AuthnContext>
    </AuthnStatement>
  </Assertion>
</Response>

SAML Bindings

  • HTTP Redirect Binding: Parameters in URL query string
  • HTTP POST Binding: Parameters in form POST body
  • HTTP Artifact Binding: Reference to assertion stored at IDP

OpenID Connect

OpenID Connect Core 1.0 is an identity layer on top of OAuth 2.0.

OpenID Connect Flow

The OpenID Connect protocol, in abstract, follows these steps:

  1. The RP (Client) sends a request to the OpenID Provider (OP)
  2. The OP authenticates the End-User and obtains authorization
  3. The OP responds with an ID Token and usually an Access Token
  4. The RP can send a request with the Access Token to the UserInfo Endpoint
  5. The UserInfo Endpoint returns Claims about the End-User

ID Token Structure

The ID Token is a JWT containing claims about the authentication event:

{
  "iss": "https://auth.example.com",
  "sub": "248289761001",
  "aud": "s6BhdRkqt3",
  "nonce": "n-0S6_WzA2Mj",
  "exp": 1311281970,
  "iat": 1311280970,
  "auth_time": 1311280969,
  "acr": "urn:mace:incommon:iap:silver"
}

Standard Claims

  • sub: Subject identifier
  • name: Full name
  • given_name: Given name
  • family_name: Surname
  • email: Email address
  • email_verified: Email verification status
  • picture: Profile picture URL

Discovery and Registration

OpenID Connect supports dynamic discovery of OP configuration and dynamic client registration:

  • Discovery: /.well-known/openid_configuration
  • Registration: Dynamic client registration endpoint
  • JWKs: JSON Web Key Set for token verification

Authentication Methods

  • Authorization Code Flow: Most secure for web applications
  • Implicit Flow: Deprecated, use Authorization Code with PKCE instead
  • Hybrid Flow: Combination of code and implicit flows

Implementation Best Practices

Security Guidelines

  1. Always use HTTPS in production
  2. Validate all tokens before accepting them
  3. Implement proper session management
  4. Use secure storage for sensitive data
  5. Regular security audits and updates

Token Management

  • Use short-lived access tokens (15-60 minutes)
  • Implement refresh token rotation
  • Secure token storage (httpOnly cookies, secure storage)
  • Proper token revocation mechanisms

Error Handling

  • Don’t leak sensitive information in error messages
  • Implement proper logging for security events
  • Use standard error codes and responses
  • Graceful degradation for authentication failures

Performance Considerations

  • Cache metadata and configuration
  • Use connection pooling for HTTP requests
  • Implement proper timeout handling
  • Consider token caching strategies

Common Integration Patterns

API Gateway Integration

Centralize authentication and authorization at the API gateway level:

  1. Gateway validates tokens
  2. Gateway forwards authenticated requests
  3. Downstream services trust gateway
  4. Centralized policy enforcement

Microservices Authentication

Patterns for distributed authentication:

  • Token Relay: Pass tokens between services
  • Token Exchange: Convert tokens for different contexts
  • Service Mesh: Centralized security at infrastructure level
  • Sidecar Pattern: Authentication proxy per service

Mobile Application Integration

Considerations for mobile apps:

  • Use Authorization Code flow with PKCE
  • Implement secure token storage
  • Handle network connectivity issues
  • Consider offline authentication scenarios