class JWT::JWKS

Overview

JWKS (JSON Web Key Set) helper for JWT validation with support for OIDC discovery

Supports:

Example:

# Initialize with optional local keys
jwks = JWT::JWKS.new(
  local_keys: {"local_key_id" => "secret"},
  local_algorithm: JWT::Algorithm::HS256
)

# Validate a token and check scopes
payload = jwks.validate(token, issuer: "https://example.com", audience: "my-app")
if payload
  scopes = JWT::JWKS.extract_scopes(payload)
  if scopes.includes?("read")
    # User has required scope
  end
end

Defined in:

jwt/jwks.cr
jwt/jwks/jwk.cr
jwt/jwks/oidc_metadata.cr

Constant Summary

ALLOWED_ALGORITHMS = {"RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "ES256", "ES384", "ES512", "EdDSA"}

Allowed algorithms for JWKS validation (interoperable with real-world JWKS endpoints) "none" is explicitly excluded for security

DEFAULT_CACHE_TTL = 10.minutes

Default cache TTL (10 minutes)

DEFAULT_LEEWAY = 60.seconds

Default leeway for time-based claims (60 seconds)

Constructors

Class Method Summary

Instance Method Summary

Constructor Detail

def self.new(local_keys : Hash(String, String) | Nil = nil, local_algorithm : Algorithm | Nil = nil, cache_ttl : Time::Span = DEFAULT_CACHE_TTL, leeway : Time::Span = DEFAULT_LEEWAY) #

Initialize JWKS validator

@param local_keys Optional hash of kid => key for local JWT validation @param local_algorithm Algorithm to use for local keys @param cache_ttl Cache TTL for OIDC metadata and JWKS (default: 10 minutes) @param leeway Clock skew leeway for time-based claims (default: 60 seconds)


[View source]

Class Method Detail

def self.extract_roles(payload : JSON::Any) : Array(String) #

Extract roles from JWT payload

Checks "roles" (Azure AD), "realm_access.roles" (Keycloak realm roles), "resource_access" (Keycloak client roles), and "groups" (Okta) claims


[View source]
def self.extract_scopes(payload : JSON::Any) : Array(String) #

Extract scopes from JWT payload

Checks "scp" (Entra/Azure AD), "scope" (standard), and "permissions" (Auth0) claims Handles both space-delimited strings and arrays


[View source]
def self.validate_roles(payload : JSON::Any, required_roles : Array(String)) : Bool #

Validate roles in a JWT payload

@param payload JWT payload @param required_roles Required roles (checks for "roles" claim) @return true if all required roles are present


[View source]
def self.validate_scopes(payload : JSON::Any, required_scopes : Array(String)) : Bool #

Validate scopes in a JWT payload

@param payload JWT payload @param required_scopes Required scopes (checks for "scp" claim) @return true if all required scopes are present


[View source]

Instance Method Detail

def cache_ttl : Time::Span #

Cache TTL


[View source]
def clear_cache : Nil #

Clear all caches


[View source]
def fetch_jwks(jwks_uri : String, force_refresh : Bool = false) : JWKSet #

Fetch JWKS from a jwks_uri

@param jwks_uri JWKS URI @param force_refresh Force refresh even if cached (used for key rotation) @return JWKS key set


[View source]
def fetch_oidc_metadata(issuer : String) : OIDCMetadata #

Fetch OIDC metadata for an issuer

@param issuer Issuer URL (e.g., "https://login.microsoftonline.com/{tenant}/v2.0") @return OIDC metadata


[View source]
def find_key(jwks : JWKSet, kid : String) : JWK | Nil #

Find a JWK by kid in a JWKS


[View source]
def leeway : Time::Span #

Clock skew leeway for time-based claims (exp, nbf, iat)


[View source]
def leeway=(leeway : Time::Span) #

Clock skew leeway for time-based claims (exp, nbf, iat)


[View source]
def local_algorithm : Algorithm | Nil #

[View source]
def local_keys : Hash(String, String) | Nil #

Local keys for service-to-service JWT validation


[View source]
def validate(token : String, issuer : String | Nil = nil, audience : String | Array(String) | Nil = nil, validate_claims : Bool = true) : JSON::Any | Nil #

Validate a JWT token

This method will:

  1. Try to validate using local keys if provided
  2. Fall back to JWKS validation if not a local token

@param token JWT token string @param issuer Expected issuer (for OIDC metadata lookup) @param audience Expected audience(s) for validation @param validate_claims Whether to validate standard claims (exp, nbf, etc.) @return Validated payload or nil if validation fails

Example:

payload = jwks.validate(token, issuer: "https://example.com", audience: "my-app")
if payload
  # Check scopes
  scopes = JWT::JWKS.extract_scopes(payload)
  if scopes.includes?("read")
    # Token is valid with required scope
  end
end

[View source]