gose/jwt

JSON Web Token (JWT) - RFC 7519

This module provides JWT functionality built on top of JWS for signing and verification. JWTs are a compact, URL-safe means of representing claims to be transferred between two parties.

Phantom Types

JWT uses phantom types to enforce compile-time safety:

Example

import gleam/dynamic/decode
import gleam/time/duration
import gleam/time/timestamp
import gose/jwa
import gose/jwk
import gose/jwt

let key = jwk.generate_hmac_key(jwa.HmacSha256)
let now = timestamp.system_time()

// Create claims and sign
let claims = jwt.claims()
  |> jwt.with_subject("user123")
  |> jwt.with_issuer("my-app")
  |> jwt.with_expiration(timestamp.add(now, duration.hours(1)))

let assert Ok(signed) = jwt.sign(jwa.JwsHmac(jwa.HmacSha256), claims, key)
let token = jwt.serialize(signed)

// Verify and validate using Verifier (enforces algorithm pinning)
let assert Ok(verifier) = jwt.verifier(jwa.JwsHmac(jwa.HmacSha256), [key], jwt.default_validation())
let assert Ok(verified) = jwt.verify_and_validate(verifier, token, now)

// Decode verified claims
let decoder = {
  use sub <- decode.field("sub", decode.string)
  decode.success(sub)
}
let assert Ok(subject) = jwt.decode(verified, decoder)

Types

JWT claims set.

Contains the registered claims from RFC 7519 and supports custom claims.

pub opaque type Claims

A JSON Web Token with phantom type for state tracking.

pub opaque type Jwt(state)

JWT error type with structured variants for domain-specific errors.

Used by both signed JWTs (jwt module) and encrypted JWTs (encrypted_jwt module). Provides rich variants for validation errors (expiration, audience, issuer, etc.) and wraps underlying JOSE layer errors via JoseError.

Example

case jwt.verify_and_validate(verifier, token, now) {
  Ok(verified) -> io.println("Success!")
  Error(jwt.TokenExpired(at)) -> io.println("Token expired")
  Error(jwt.InvalidSignature) -> io.println("Bad signature")
  Error(jwt.JoseError(gose_err)) -> io.println("JOSE error: " <> gose.error_message(gose_err))
  Error(e) -> io.println("Error: " <> string.inspect(e))
}
pub type JwtError {
  InvalidSignature
  DecryptionFailed(reason: String)
  TokenExpired(expired_at: timestamp.Timestamp)
  TokenNotYetValid(valid_from: timestamp.Timestamp)
  MissingExpiration
  MissingIssuedAt
  IssuedInFuture(issued_at: timestamp.Timestamp)
  TokenTooOld(issued_at: timestamp.Timestamp, max_age: Int)
  InvalidJti(jti: String)
  IssuerMismatch(expected: String, actual: option.Option(String))
  AudienceMismatch(
    expected: String,
    actual: option.Option(List(String)),
  )
  JwsAlgorithmMismatch(expected: jwa.JwsAlg, actual: jwa.JwsAlg)
  JweAlgorithmMismatch(
    expected_alg: jwa.JweAlg,
    expected_enc: jwa.Enc,
    actual_alg: jwa.JweAlg,
    actual_enc: jwa.Enc,
  )
  MissingKid
  UnknownKid(kid: String)
  MalformedToken(reason: String)
  ClaimDecodingFailed(reason: String)
  InsecureUnprotectedHeader(header: String)
  InvalidClaim(reason: String)
  JoseError(error: gose.GoseError)
}

Constructors

  • InvalidSignature

    The JWS signature did not verify against any of the provided keys.

  • DecryptionFailed(reason: String)

    JWE decryption failed (wrong key, corrupted ciphertext, etc.).

  • TokenExpired(expired_at: timestamp.Timestamp)

    The exp claim is in the past.

  • TokenNotYetValid(valid_from: timestamp.Timestamp)

    The nbf claim is in the future.

  • MissingExpiration

    The exp claim is required by the verifier but absent.

  • MissingIssuedAt

    The iat claim is required by the verifier but absent.

  • IssuedInFuture(issued_at: timestamp.Timestamp)

    The iat claim is in the future.

  • TokenTooOld(issued_at: timestamp.Timestamp, max_age: Int)

    The token age (now − iat) exceeds the configured max_age in seconds.

  • InvalidJti(jti: String)

    The jti claim is empty or otherwise invalid.

  • IssuerMismatch(expected: String, actual: option.Option(String))

    The iss claim does not match the expected issuer.

  • AudienceMismatch(
      expected: String,
      actual: option.Option(List(String)),
    )

    The aud claim does not contain the expected audience.

  • JwsAlgorithmMismatch(expected: jwa.JwsAlg, actual: jwa.JwsAlg)

    The token’s JWS algorithm does not match the expected algorithm.

  • JweAlgorithmMismatch(
      expected_alg: jwa.JweAlg,
      expected_enc: jwa.Enc,
      actual_alg: jwa.JweAlg,
      actual_enc: jwa.Enc,
    )

    The token’s JWE algorithm or encryption does not match expected values.

  • MissingKid

    A kid header is required for key lookup but absent from the token.

  • UnknownKid(kid: String)

    The token’s kid does not match any key in the provided set.

  • MalformedToken(reason: String)

    The token could not be parsed (invalid compact serialization, bad base64, malformed header JSON, etc.).

  • ClaimDecodingFailed(reason: String)

    The claims payload is valid JSON but a required field is missing or has an unexpected type.

  • InsecureUnprotectedHeader(header: String)

    A security-sensitive header (e.g. alg) appears in the unprotected header, which is not integrity-protected.

  • InvalidClaim(reason: String)

    A claim value is invalid (empty audience list, reserved claim name, etc.).

  • JoseError(error: gose.GoseError)

    An error from the underlying JOSE layer (JWS, JWE, or JWK).

Options for JWT validation.

pub type JwtValidationOptions {
  JwtValidationOptions(
    issuer: option.Option(String),
    audience: option.Option(String),
    clock_skew: Int,
    require_exp: Bool,
    max_token_age: option.Option(Int),
    jti_validator: option.Option(fn(String) -> Bool),
    kid_policy: KidPolicy,
  )
}

Constructors

  • JwtValidationOptions(
      issuer: option.Option(String),
      audience: option.Option(String),
      clock_skew: Int,
      require_exp: Bool,
      max_token_age: option.Option(Int),
      jti_validator: option.Option(fn(String) -> Bool),
      kid_policy: KidPolicy,
    )

    Arguments

    issuer

    Expected iss claim. If Some, the token’s issuer must match exactly or IssuerMismatch is returned. None skips the check.

    audience

    Expected aud claim. If Some, the token’s audience list must contain this value or AudienceMismatch is returned. None skips the check.

    clock_skew

    Tolerance in seconds for time-based checks (exp, nbf, iat). Accounts for clock drift between issuer and verifier.

    require_exp

    Whether the exp claim must be present. If True and absent, MissingExpiration is returned.

    max_token_age

    Maximum allowed token age in seconds (now − iat). If Some, tokens older than this are rejected with TokenTooOld. Requires iat to be present; returns MissingIssuedAt if absent.

    jti_validator

    Custom validation function for the jti (JWT ID) claim. Receives the jti value; return True to accept, False to reject with InvalidJti. Useful for replay detection.

    kid_policy

    Controls how the kid header is handled for key selection. See KidPolicy for the available modes.

Policy for kid (Key ID) header validation during JWT verification.

pub type KidPolicy {
  NoKidRequirement
  RequireKid
  RequireKidMatch
}

Constructors

  • NoKidRequirement

    No kid requirement - prioritize matching keys but try all (default)

  • RequireKid

    Token must have a kid header, but it doesn’t need to match a configured key

  • RequireKidMatch

    Token must have a kid header AND it must match a configured key’s kid

Phantom type for unverified JWT.

pub type Unverified

Phantom type for verified JWT.

pub type Verified

A JWT verifier that enforces algorithm pinning and validates key compatibility.

Create with verifier(). The verifier validates that:

  • All keys are compatible with the algorithm
  • Each key’s use field (if set) is Signing
  • Each key’s key_ops field (if set) includes Verify
pub opaque type Verifier

Values

pub fn alg(jwt: Jwt(state)) -> jwa.JwsAlg

Get the algorithm (alg) from a JWT.

Parameters

  • jwt - The JWT to read the algorithm from.

Returns

The JwsAlg used to sign the token.

pub fn claims() -> Claims

Create an empty claims set with no registered or custom claims. Use the with_* functions to populate claims before signing.

Returns

An empty Claims value with no registered or custom claims set.

pub fn dangerously_decode_unverified(
  jwt: Jwt(Unverified),
  decoder: decode.Decoder(a),
) -> Result(a, JwtError)

Decode an unverified JWT’s claims using a custom decoder.

Warning: These claims have not been verified. Do not trust them until the JWT has been verified with verify_and_validate.

Example

let assert Ok(parsed) = jwt.parse(token)
let decoder = {
  use iss <- decode.field("iss", decode.string)
  decode.success(iss)
}
let assert Ok(issuer) = jwt.dangerously_decode_unverified(parsed, decoder)
// issuer is untrusted - only use for routing/lookup, not authorization

Parameters

  • jwt - An unverified JWT obtained from parse().
  • decoder - A gleam/dynamic/decode decoder for extracting claims from the raw JSON payload.

Returns

Ok(a) with the decoded value from the unverified claims, or Error(JwtError) if the payload cannot be decoded.

pub fn decode(
  jwt: Jwt(Verified),
  decoder: decode.Decoder(a),
) -> Result(a, JwtError)

Decode a verified JWT’s claims using a custom decoder.

This allows extracting claims directly into your own types using gleam/dynamic/decode. The decoder receives the raw claims JSON.

Example

let decoder = {
  use sub <- decode.field("sub", decode.string)
  use role <- decode.field("role", decode.string)
  decode.success(User(sub:, role:))
}
let assert Ok(user) = jwt.decode(verified_jwt, decoder)

Parameters

  • jwt - A verified JWT obtained from verify_and_validate or sign.
  • decoder - A gleam/dynamic/decode decoder for extracting claims from the raw JSON payload.

Returns

Ok(a) with the decoded value from the verified claims, or Error(JwtError) if the claims cannot be decoded with the provided decoder.

pub fn default_validation() -> JwtValidationOptions

Create default validation options.

Default settings:

  • No issuer validation
  • No audience validation
  • 60 seconds clock skew tolerance
  • Expiration claim required
  • No max token age
  • No JWT ID validator
  • No kid requirement (prioritizes matching keys but tries all)

When an iat claim is present, it is always checked to ensure it is not in the future (beyond clock skew), regardless of whether max_token_age is configured.

pub fn kid(jwt: Jwt(state)) -> Result(String, Nil)

Get the key ID (kid) from a JWT header.

Security Warning: The kid value comes from the token and is untrusted input. If you use it to look up keys (from a database, filesystem, or key store), you must sanitize it first to prevent injection attacks.

Parameters

  • jwt - The JWT to read the key ID from.

Returns

Ok(String) with the key ID, or Error(Nil) if no kid is set.

pub fn parse(token: String) -> Result(Jwt(Unverified), JwtError)

Parse a JWT from compact format.

Returns an unverified JWT that needs to be verified with verify_and_validate or verify_and_dangerously_skip_validation.

Parameters

  • token - The JWT compact-serialized string to parse.

Returns

Ok(Jwt(Unverified)) with the parsed but unverified JWT, or Error(JwtError) if the token is malformed or uses unsupported JWT features.

pub fn serialize(jwt: Jwt(Verified)) -> String

Serialize a verified JWT to compact format.

Parameters

  • jwt - The verified JWT to serialize.

Returns

The JWT in compact format (header.payload.signature).

pub fn sign(
  alg: jwa.JwsAlg,
  claims: Claims,
  key: jwk.Jwk,
) -> Result(Jwt(Verified), JwtError)

Sign a JWT with the provided key.

Automatically sets typ: "JWT" in the header.

Example

let claims = jwt.claims()
  |> jwt.with_subject("user123")
  |> jwt.with_expiration(exp)

let assert Ok(signed) = jwt.sign(jwa.JwsHmac(jwa.HmacSha256), claims, key)
let token = jwt.serialize(signed)

Parameters

  • alg - The JWS algorithm to use for signing.
  • claims - The claims set to include in the JWT payload.
  • key - The signing key (must be compatible with the algorithm).

Returns

Ok(Jwt(Verified)) with the signed JWT ready to serialize with serialize(), or Error(JwtError) if signing fails due to key incompatibility or a crypto error. The token is marked Verified because locally-signed tokens are implicitly trusted.

pub fn verifier(
  alg: jwa.JwsAlg,
  keys keys: List(jwk.Jwk),
  options options: JwtValidationOptions,
) -> Result(Verifier, JwtError)

Create a verifier for JWT signature verification and claim validation.

Each verifier is pinned to a single algorithm. This prevents algorithm confusion attacks where an attacker changes the alg header to trick the verifier into using the wrong algorithm (see RFC 8725 Section 3.1). For multi-algorithm scenarios (e.g., algorithm migration), create one verifier per algorithm and try each in sequence:

let assert Ok(rs_verifier) = jwt.verifier(
  jwa.JwsRsaPkcs1(jwa.RsaPkcs1Sha256),
  keys: rsa_keys,
  options: jwt.default_validation(),
)
let assert Ok(ec_verifier) = jwt.verifier(
  jwa.JwsEcdsa(jwa.EcdsaP256),
  keys: ec_keys,
  options: jwt.default_validation(),
)

let result = case jwt.verify_and_validate(rs_verifier, token, now) {
  Ok(verified) -> Ok(verified)
  _ -> jwt.verify_and_validate(ec_verifier, token, now)
}

Accepts one or more keys for key rotation scenarios.

Key selection during verification:

  1. If token has kid header, prioritize keys with matching kid
  2. Try keys in order until one succeeds
  3. Fail if no key verifies the signature

Returns an error if:

  • The key list is empty
  • Any algorithm is incompatible with any key type
  • Any key’s use field is set but not Signing
  • Any key’s key_ops field is set but doesn’t include Verify

Parameters

  • alg - The expected JWS algorithm; tokens using a different algorithm are rejected.
  • keys - One or more keys to try during verification (supports key rotation).
  • options - Validation options controlling claim checks (expiration, issuer, etc.).

Returns

Ok(Verifier) ready for use with verify_and_validate, or Error(JwtError) if any key is incompatible with the algorithm or has incorrect usage constraints.

pub fn verify_and_dangerously_skip_validation(
  verifier: Verifier,
  token: String,
) -> Result(Jwt(Verified), JwtError)

Verify a JWT’s signature only, skipping all claim validation.

Warning: This skips expiration, not-before, issuer, and audience checks. Use only when you have a legitimate reason to bypass validation, such as inspecting claims before deciding on validation policy.

Still enforces algorithm pinning and kid_policy for security. When multiple keys are configured, keys with matching kid are tried first.

Parameters

  • verifier - A verifier created with verifier() that pins the algorithm and keys.
  • token - The JWT compact-serialized string to verify.

Returns

Ok(Jwt(Verified)) with the verified JWT and unchecked claims, or Error(JwtError) if signature verification fails or the algorithm doesn’t match.

pub fn verify_and_validate(
  verifier: Verifier,
  token: String,
  now: timestamp.Timestamp,
) -> Result(Jwt(Verified), JwtError)

Verify a JWT’s signature and validate its claims using a Verifier.

Checks:

  1. Token’s alg header matches the verifier’s expected algorithm
  2. Signature is valid for one of the verifier’s keys
  3. Claims pass validation (exp, nbf, iss, aud per options)

When multiple keys are configured:

  • Keys with matching kid are tried first (if token has kid header)
  • kid_policy controls kid header enforcement (see KidPolicy type)
  • With NoKidRequirement, all keys are tried with matching keys prioritized

Parameters

  • verifier - A verifier created with verifier() that pins the algorithm and keys.
  • token - The JWT compact-serialized string to verify.
  • now - The current timestamp, used for time-based claim validation.

Returns

Ok(Jwt(Verified)) with the verified JWT whose claims can be safely trusted, or Error(JwtError) if signature verification or claim validation fails.

pub fn with_audience(claims: Claims, aud: String) -> Claims

Set a single audience (aud) claim.

Parameters

  • claims - The claims set to update.
  • aud - The audience string.

Returns

The Claims with the aud claim set.

pub fn with_audiences(
  claims: Claims,
  aud: List(String),
) -> Result(Claims, JwtError)

Set multiple audiences (aud) claim.

Returns an error if the audience list is empty.

Parameters

  • claims - The claims set to update.
  • aud - The list of audience strings.

Returns

Ok(Claims) with the audience list set, or Error(JwtError) if the audience list is empty.

pub fn with_claim(
  claims: Claims,
  key: String,
  value: json.Json,
) -> Result(Claims, JwtError)

Set a custom claim.

Returns an error if the key is a reserved claim name. Use the dedicated setters for registered claims (e.g., with_issuer, with_subject).

Parameters

  • claims - The claims set to add the custom claim to.
  • key - The claim name (must not be a reserved claim like “iss”, “sub”, etc.).
  • value - The JSON value for the claim.

Returns

Ok(Claims) with the custom claim added, or Error(JwtError) if the key is a reserved claim name.

pub fn with_expiration(
  claims: Claims,
  exp: timestamp.Timestamp,
) -> Claims

Set the expiration time (exp) claim.

Parameters

  • claims - The claims set to update.
  • exp - The expiration timestamp.

Returns

The Claims with the exp claim set.

pub fn with_issued_at(
  claims: Claims,
  iat: timestamp.Timestamp,
) -> Claims

Set the issued at time (iat) claim.

Parameters

  • claims - The claims set to update.
  • iat - The issued-at timestamp.

Returns

The Claims with the iat claim set.

pub fn with_issuer(claims: Claims, iss: String) -> Claims

Set the issuer (iss) claim.

Parameters

  • claims - The claims set to update.
  • iss - The issuer string.

Returns

The Claims with the iss claim set.

pub fn with_jti_validator(
  options: JwtValidationOptions,
  validator: fn(String) -> Bool,
) -> JwtValidationOptions

Set a custom JWT ID (jti) validator.

The validator function receives the jti claim value and should return True if the ID is valid, False if it should be rejected.

Common use cases:

  • Check against a revocation list
  • Verify the ID hasn’t been seen before (replay prevention)
  • Validate format/structure of the ID

If the token has no jti claim, the validator is not called.

Parameters

  • options - The validation options to update.
  • validator - A function that receives the jti value and returns True if valid.

Returns

The updated JwtValidationOptions with the custom jti validator.

pub fn with_jwt_id(claims: Claims, jti: String) -> Claims

Set the JWT ID (jti) claim.

Parameters

  • claims - The claims set to update.
  • jti - The unique token identifier.

Returns

The Claims with the jti claim set.

pub fn with_max_token_age(
  options: JwtValidationOptions,
  max_age_seconds: Int,
) -> JwtValidationOptions

Set the maximum token age in seconds.

If set, tokens with an iat claim older than now - max_age_seconds will be rejected with TokenTooOld. Requires the iat claim to be present. Tokens without iat are rejected with MissingIssuedAt.

Parameters

  • options - The validation options to update.
  • max_age_seconds - The maximum allowed token age in seconds.

Returns

The updated JwtValidationOptions with the max token age set.

pub fn with_not_before(
  claims: Claims,
  nbf: timestamp.Timestamp,
) -> Claims

Set the not before time (nbf) claim.

Parameters

  • claims - The claims set to update.
  • nbf - The not-before timestamp.

Returns

The Claims with the nbf claim set.

pub fn with_subject(claims: Claims, sub: String) -> Claims

Set the subject (sub) claim.

Parameters

  • claims - The claims set to update.
  • sub - The subject string.

Returns

The Claims with the sub claim set.

Search Document