Crystal JWT

repository·master·Indexed 42 days ago

https://github.com/crystal-community/jwt

A library for encoding, decoding, and validating JSON Web Tokens (JWT) in Crystal. It supports HMAC, RSA, RSA-PSS, ECDSA, EdDSA, and None algorithms. Includes a specialized `JWT::JWKS` helper for remote token validation via OpenID Connect (OIDC) providers such as Microsoft Entra ID, Auth0, Okta, Keycloak, Google, and AWS Cognito, featuring built-in support for scope and role extraction, HTTPS enforcement, and JWKS caching.

Tokens
3.6K
Snippets
13
Records
19
Agent score
3%

What's inside Crystal JWT

  1. How JWT::JWKS works and its validation flow

    master

    The JWT::JWKS class provides a unified interface for both local token validation (using shared secrets for service-to-service JWTs) and remote token validation (using JWKS from external identity providers).

    When validating a remote token, the library follows this flow:

    1. Extracts the header (kid, alg, typ) and validates the type (JWT or at+jwt).
    2. If not a local validation, it extracts the iss (issuer) claim.
    3. Fetches OIDC metadata via HTTPS (cached).
    4. Validates that the token's iss matches the metadata's issuer.
    5. Fetches the JWKS (cached, HTTPS only).
    6. Finds the correct key using kid or x5t.
    7. Validates the algorithm against the JWK properties.
    8. Converts the JWK to PEM and validates the signature.
    9. Validates claims (exp, nbf, iss, aud) using a configurable leeway for clock skew.
  2. Use reserved claim names in JWT payloads

    master

    You can include standard RFC 7519 claims in your payload to control token behavior during decoding.

    Common Claims:

    • exp (Expiration Time): The time after which the token MUST NOT be accepted. Raises JWT::ExpiredSignatureError if expired.
    • nbf (Not Before Time): The time before which the token MUST NOT be accepted. Raises JWT::ImmatureSignatureError if used too early.
    • iat (Issued At): The time the token was issued.
    • aud (Audience): Identifies the intended recipients. Use the aud: option in JWT.decode to validate. Raises JWT::InvalidAudienceError on mismatch.
    • iss (Issuer): Identifies the principal that issued the JWT. Use the iss: option in JWT.decode to validate. Raises JWT::InvalidIssuerError on mismatch.
    • sub (Subject): Identifies the subject of the JWT. Use the sub: option in JWT.decode to validate. Raises JWT::InvalidSubjectError on mismatch.
    • jti (JWT ID): A unique identifier for the token to prevent replay attacks.
    • typ (Type): Used to disambiguate the object type (defaults to "JWT").

    Example: Expiration and Audience validation

    # Create token with expiration and audience
    exp = Time.utc.to_unix + 60
    payload = { "foo" => "bar", "exp" => exp, "aud" => ["sergey", "julia"] }
    token = JWT.encode(payload, "SecretKey", JWT::Algorithm::HS256)
    
    # Valid decoding
    payload, header = JWT.decode(token, "SecretKey", JWT::Algorithm::HS256, aud: "sergey")
    
    # Invalid audience (raises JWT::InvalidAudienceError)
    payload, header = JWT.decode(token, "SecretKey", JWT::Algorithm::HS256, aud: "max")
  3. Security features and protections in JWT::JWKS

    master

    The JWT::JWKS implementation includes several built-in security protections:

    • Algorithm Allow-List: Only specific algorithms are accepted (RS256/384/512, PS256/384/512, ES256/384/512, and EdDSA). It explicitly rejects none and HS algorithms for JWKS.
    • JWK Validation: Cross-checks header alg against JWK alg, validates kty compatibility (e.g., RSA to RS*/PS*), and ensures EC curves match the algorithm.
    • HTTPS Enforcement: All issuer URLs and JWKS URIs must use HTTPS to prevent downgrade attacks and SSRF.
    • Network Hardening: Includes connection timeouts (3s) and read timeouts (5s).
    • HTTP Caching: Respects Cache-Control max-age, supports ETag and conditional GET (If-None-Match), and falls back to a configured TTL.
    • Key Rotation: Automatically refreshes the JWKS cache when a kid is not found and supports x5t matching for kid-less tokens.
  4. Validate local HMAC tokens with JWKS

    master

    You can use the same JWKS instance to validate both remote (RSA/ECDSA) and local (HMAC) tokens. If a token's kid matches a key provided in the local_keys configuration, the library will attempt local validation first before falling back to remote JWKS fetching.

    local_keys = {
      "local-service-key-id" => "your-secret-key"
    }
    
    jwks = JWT::JWKS.new(
      local_keys: local_keys,
      local_algorithm: JWT::Algorithm::HS256
    )
    
    # This will try local validation first, then fall back to JWKS
    payload = jwks.validate(token)
  5. Configure JWKS validator options

    master

    When initializing JWT::JWKS.new, you can configure several security and performance options:

    • leeway: Integer/Duration. Configurable clock skew tolerance for time-based claims (exp, nbf, iat). Default is 60.seconds.
    • cache_ttl: Duration. How long to cache OIDC metadata and JWKS.
    • local_keys: Hash. A mapping of kid to secret keys for local HMAC validation (e.g., {"key-id" => "secret"}).
    • local_algorithm: JWT::Algorithm. The algorithm to use for local tokens (e.g., JWT::Algorithm::HS256).
    # Example with custom configuration
    local_keys = {
      "local-service-key-id" => "your-secret-key"
    }
    
    jwks = JWT::JWKS.new(
      local_keys: local_keys,
      local_algorithm: JWT::Algorithm::HS256,
      cache_ttl: 15.minutes,
      leeway: 60.seconds
    )
  6. Handle JWT exceptions

    master

    When working with JWTs, you should be prepared to catch the following exceptions:

    • JWT::Error: Base error class
      • JWT::DecodeError: Errors during decoding
        • JWT::VerificationError: Signature mismatch
        • JWT::ExpiredSignatureError: Token has expired (exp claim)
        • JWT::ImmatureSignatureError: Token is not yet valid (nbf claim)
        • JWT::InvalidAudienceError: Audience mismatch (aud claim)
        • JWT::InvalidIssuerError: Issuer mismatch (iss claim)
        • JWT::InvalidSubjectError: Subject mismatch (sub claim)
    • UnsupportedAlgorithmError: The specified algorithm is not supported
  7. Basic JWKS token validation

    master

    Use JWT::JWKS.new to initialize a validator. The .validate method performs signature verification, checks the issuer (iss), and checks the audience (aud). It returns the token payload on success, or nil if validation fails.

    require "jwt"
    require "jwt/jwks"
    
    # Initialize JWKS validator
    jwks = JWT::JWKS.new
    
    # Validate a token
    issuer = "https://login.microsoftonline.com/{tenant}/v2.0"
    audience = "api://your-app-client-id"
    token = "eyJ..."
    
    payload = jwks.validate(token, issuer: issuer, audience: audience)
    
    if payload
      puts "Token is valid!"
      puts "User: #{payload["sub"]}"
    else
      puts "Token is invalid"
    end
    require "jwt"
    require "jwt/jwks"
    
    # Initialize JWKS validator
    jwks = JWT::JWKS.new
    
    # Validate a token from Entra ID
    issuer = "https://login.microsoftonline.com/{tenant}/v2.0"
    audience = "api://your-app-client-id"
    token = "eyJ..." # JWT token from Authorization header
    
    payload = jwks.validate(token, issuer: issuer, audience: audience)
    
    if payload
      puts "Token is valid!"
      puts "User: #{payload["sub"]}"
      puts "Name: #{payload["name"]}"
    else
      puts "Token is invalid"
    end
  8. Manage JWKS cache manually

    master

    If you need to handle key rotation or force updates, you can interact with the cache directly:

    • jwks.clear_cache: Clears all cached metadata and JWKS.
    • jwks.fetch_oidc_metadata(issuer): Manually fetches OIDC metadata for a specific issuer.
    • jwks.fetch_jwks(jwks_uri, force_refresh: true): Manually fetches JWKS from a URI, with an option to force a refresh.
    # Clear cache manually
    jwks.clear_cache
    
    # Manually fetch OIDC metadata
    metadata = jwks.fetch_oidc_metadata(issuer)
    
    # Manually fetch JWKS (with force refresh)
    jwks_data = jwks.fetch_jwks(metadata.jwks_uri, force_refresh: true)
  9. Extract and validate roles

    master

    The JWT::JWKS.extract_roles method retrieves roles from multiple provider structures:

    • Azure AD: roles claim.
    • Keycloak: realm_access.roles and resource_access[client].roles.
    • Okta: groups claim.

    You can use JWT::JWKS.validate_roles(payload, required_roles) to check if the payload contains the necessary roles.

    # Extract roles from payload
    roles = JWT::JWKS.extract_roles(payload)  # => ["admin", "user"]
    
    # Validate required roles
    JWT::JWKS.validate_roles(payload, ["admin"])  # => true/false