class

JWT::JWKS

Inherits Reference < Object

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

Supports:

  • Fetching OIDC metadata from /.well-known/openid-configuration
  • Fetching and caching JWKS keys
  • Local JWT validation (for service-to-service tokens)
  • Remote JWT validation via JWKS

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

Constants

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

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)

Source

Class methods

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

Source
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

Source
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

Source
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

Source

Instance methods

cache_ttl

Cache TTL

Source
clear_cache

Clear all caches

Source
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

Source
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

Source
find_key(jwks : JWKSet, kid : String) : JWK | Nil

Find a JWK by kid in a JWKS

Source
leeway

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

Source
leeway=(leeway : Time::Span)

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

Source
local_algorithm
Source
local_keys

Local keys for service-to-service JWT validation

Source
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
Source

Nested types