Skip to content

Protecting API Endpoints

Dick Davis edited this page Jan 23, 2026 · 2 revisions

Protecting API Endpoints

TokenAuthority provides the TokenAuthentication concern for validating JWT access tokens in your API controllers.

Basic Usage

Include the concern to automatically validate access tokens on every request:

class Api::V1::ResourcesController < ActionController::API
  include TokenAuthority::TokenAuthentication

  def index
    render json: token_user.resources
  end

  def show
    resource = token_user.resources.find(params[:id])
    render json: resource
  end
end

Available Methods

The concern provides two methods for accessing token data:

Method Description
token_user Returns the authenticated user associated with the access token
token_scope Returns an array of scope tokens (e.g., ["read", "write"]), or [] if no scopes

How Token Validation Works

When you include TokenAuthentication, a before_action automatically validates the access token on every request. The validation process:

Step 1: Extract Token

Extracts the Bearer token from the Authorization header:

Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

Step 2: Decode and Validate JWT

Decodes the JWT and validates:

  • The signature matches using your configured secret_key
  • The token has not expired (exp claim)
  • The audience matches your configured audience_url (aud claim)
  • The issuer matches your configured issuer_url (iss claim)

Step 3: Verify Session

Checks that the token's associated session is still active:

  • The session exists in the database
  • The session has not been revoked
  • The session has not been refreshed (only the latest token is valid)

If validation fails, the concern automatically renders an appropriate JSON error response.

Error Handling

The concern automatically handles authentication errors and returns appropriate JSON responses.

Error Scenarios

Scenario HTTP Status Error Key
Missing or blank Authorization header 401 Unauthorized missing_auth_header
Malformed or invalid JWT 401 Unauthorized invalid_token
Expired token or inactive session 401 Unauthorized unauthorized_token

Example Error Responses

Missing Authorization Header:

{
  "error": "Authorization header not found or value is blank"
}

Invalid Token:

{
  "error": "The access token is invalid or malformed"
}

Expired or Revoked Token:

{
  "error": "The access token is expired or unauthorized"
}

Custom Error Handling

If you need custom error handling, override the response methods:

class Api::V1::BaseController < ActionController::API
  include TokenAuthority::TokenAuthentication

  private

  def missing_auth_header_response
    render json: { code: "auth_required", message: "Please provide an access token" }, status: :unauthorized
  end

  def invalid_token_response
    render json: { code: "invalid_token", message: "The provided token is invalid" }, status: :unauthorized
  end

  def unauthorized_token_response
    render json: { code: "token_expired", message: "Your session has expired" }, status: :unauthorized
  end
end

Optional Authentication

For endpoints that work with or without authentication, rescue from the authentication errors:

class Api::V1::ArticlesController < ActionController::API
  include TokenAuthority::TokenAuthentication

  # Skip automatic validation for specific actions
  skip_before_action :decode_token, only: [:index]

  def index
    # Manually attempt authentication
    if authenticated?
      render json: token_user.personalized_articles
    else
      render json: Article.public_articles
    end
  end

  def show
    # This action still requires authentication
    render json: token_user.articles.find(params[:id])
  end

  private

  def authenticated?
    decode_token
    true
  rescue TokenAuthority::MissingAuthorizationHeaderError,
         TokenAuthority::InvalidAccessTokenError,
         TokenAuthority::UnauthorizedAccessTokenError
    false
  end
end

Validating Scopes

Use token_scope to check that the token includes required scopes:

class Api::V1::AdminController < ActionController::API
  include TokenAuthority::TokenAuthentication

  before_action :require_admin_scope

  def dashboard
    render json: { stats: Admin.dashboard_stats }
  end

  private

  def require_admin_scope
    unless token_scope.include?("admin")
      render json: { error: "Insufficient scope" }, status: :forbidden
    end
  end
end

Scope Helper Pattern

For more complex scope requirements, consider a helper module:

module ScopeAuthorization
  extend ActiveSupport::Concern

  private

  def require_scope(*required_scopes)
    missing = required_scopes - token_scope

    if missing.any?
      render json: {
        error: "insufficient_scope",
        required: required_scopes,
        granted: token_scope
      }, status: :forbidden
    end
  end
end

class Api::V1::DocumentsController < ActionController::API
  include TokenAuthority::TokenAuthentication
  include ScopeAuthorization

  def index
    require_scope("read")
    render json: token_user.documents
  end

  def create
    require_scope("write")
    document = token_user.documents.create!(document_params)
    render json: document, status: :created
  end

  def destroy
    require_scope("delete")
    token_user.documents.find(params[:id]).destroy
    head :no_content
  end
end

Token Claims

Access tokens include the following JWT claims:

Claim Description
sub Subject - the user's ID
aud Audience - your configured audience_url (or resource URIs if RFC 8707 is used)
iss Issuer - your configured issuer_url
exp Expiration time
iat Issued at time
jti JWT ID - unique token identifier
scope Space-delimited scope tokens (only present if scopes were requested)

See Also

Clone this wiki locally