gose/jose/jwt
JSON Web Token (JWT) - RFC 7519
Claims-based tokens built on 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:
Jwt(Unverified)- A JWT that has been parsed but not yet verifiedJwt(Verified)- A JWT with verified signature, safe to trust claims
Example
import gleam/dynamic/decode
import gleam/time/duration
import gleam/time/timestamp
import gose
import gose/jose/jwt
let signing_key = gose.generate_hmac_key(gose.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(
gose.Mac(gose.Hmac(gose.HmacSha256)),
claims:,
key: signing_key,
)
let token = jwt.serialize(signed)
// Verify and validate using Verifier (enforces algorithm pinning)
let assert Ok(verifier) =
jwt.verifier(
gose.Mac(gose.Hmac(gose.HmacSha256)),
keys: [signing_key],
options: 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
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: gose.SigningAlg,
actual: gose.SigningAlg,
)
JweAlgorithmMismatch(
expected_alg: gose.KeyEncryptionAlg,
expected_enc: gose.ContentAlg,
actual_alg: gose.KeyEncryptionAlg,
actual_enc: gose.ContentAlg,
)
MissingKid
UnknownKid(kid: String)
MalformedToken(reason: String)
ClaimDecodingFailed(reason: String)
InsecureUnprotectedHeader(header: String)
InvalidClaim(reason: String)
JoseError(error: gose.GoseError)
}
Constructors
-
InvalidSignatureThe 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
expclaim is in the past. -
TokenNotYetValid(valid_from: timestamp.Timestamp)The
nbfclaim is in the future. -
MissingExpirationThe
expclaim is required by the verifier but absent. -
MissingIssuedAtThe
iatclaim is required by the verifier but absent. -
IssuedInFuture(issued_at: timestamp.Timestamp)The
iatclaim is in the future. -
TokenTooOld(issued_at: timestamp.Timestamp, max_age: Int)The token age (now −
iat) exceeds the configuredmax_agein seconds. -
InvalidJti(jti: String)The
jticlaim is empty or otherwise invalid. -
IssuerMismatch(expected: String, actual: option.Option(String))The
issclaim does not match the expected issuer. -
AudienceMismatch( expected: String, actual: option.Option(List(String)), )The
audclaim does not contain the expected audience. -
JwsAlgorithmMismatch( expected: gose.SigningAlg, actual: gose.SigningAlg, )The token’s JWS algorithm does not match the expected algorithm.
-
JweAlgorithmMismatch( expected_alg: gose.KeyEncryptionAlg, expected_enc: gose.ContentAlg, actual_alg: gose.KeyEncryptionAlg, actual_enc: gose.ContentAlg, )The token’s JWE algorithm or encryption does not match expected values.
-
MissingKidA
kidheader is required for key lookup but absent from the token. -
UnknownKid(kid: String)The token’s
kiddoes 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
issclaim. IfSome, the token’s issuer must match exactly orIssuerMismatchis returned.Noneskips the check. - audience
-
Expected
audclaim. IfSome, the token’s audience list must contain this value orAudienceMismatchis returned.Noneskips 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
expclaim must be present. IfTrueand absent,MissingExpirationis returned. - max_token_age
-
Maximum allowed token age in seconds (now −
iat). IfSome, tokens older than this are rejected withTokenTooOld. Requiresiatto be present; returnsMissingIssuedAtif absent. - jti_validator
-
Custom validation function for the
jti(JWT ID) claim. Receives thejtivalue; returnTrueto accept,Falseto reject withInvalidJti. Useful for replay detection. - kid_policy
-
Controls how the
kidheader is handled for key selection. SeeKidPolicyfor the available modes.
Policy for kid (Key ID) header validation during JWT verification.
pub type KidPolicy {
NoKidRequirement
RequireKid
RequireKidMatch
}
Constructors
-
NoKidRequirementNo kid requirement - prioritize matching keys but try all (default)
-
RequireKidToken must have a kid header, but it doesn’t need to match a configured key
-
RequireKidMatchToken must have a kid header AND it must match a configured key’s kid
Phantom type for unverified JWT.
pub type Unverified
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
usefield (if set) isSigning - Each key’s
key_opsfield (if set) includesVerify
pub opaque type Verifier
Values
pub fn claims() -> Claims
Create an empty claims set with no registered or custom claims.
Use the with_* functions to populate claims before signing.
pub fn dangerously_decode_unverified(
jwt: Jwt(Unverified),
using 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
pub fn decode(
jwt: Jwt(Verified),
using 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)
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.
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.
pub fn sign(
alg: gose.SigningAlg,
claims claims: Claims,
key key: gose.Key(String),
) -> Result(Jwt(Verified), JwtError)
Sign a JWT with the provided key.
Automatically sets typ: "JWT" in the header. The token is marked
Verified because locally-signed tokens are implicitly trusted.
Example
let claims = jwt.claims()
|> jwt.with_subject("user123")
|> jwt.with_expiration(exp)
let assert Ok(signed) = jwt.sign(gose.Mac(gose.Hmac(gose.HmacSha256)), claims, key)
let token = jwt.serialize(signed)
pub fn verifier(
alg: gose.SigningAlg,
keys keys: List(gose.Key(String)),
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(
gose.DigitalSignature(gose.RsaPkcs1(gose.RsaPkcs1Sha256)),
keys: rsa_keys,
options: jwt.default_validation(),
)
let assert Ok(ec_verifier) = jwt.verifier(
gose.DigitalSignature(gose.Ecdsa(gose.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:
- If token has
kidheader, prioritize keys with matching kid - Try keys in order until one succeeds
- 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
usefield is set but notSigning - Any key’s
key_opsfield is set but doesn’t includeVerify
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.
pub fn verify_and_validate(
verifier: Verifier,
token token: String,
now now: timestamp.Timestamp,
) -> Result(Jwt(Verified), JwtError)
Verify a JWT’s signature and validate its claims using a Verifier.
Checks:
- Token’s
algheader matches the verifier’s expected algorithm - Signature is valid for one of the verifier’s keys
- Claims pass validation (exp, nbf, iss, aud per options)
When multiple keys are configured:
- Keys with matching
kidare tried first (if token haskidheader) kid_policycontrols kid header enforcement (seeKidPolicytype)- With
NoKidRequirement, all keys are tried with matching keys prioritized
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.
pub fn with_claim(
claims: Claims,
key key: String,
value 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).
pub fn with_expiration(
claims: Claims,
exp: timestamp.Timestamp,
) -> Claims
Set the expiration time (exp) claim.
pub fn with_issued_at(
claims: Claims,
iat: timestamp.Timestamp,
) -> Claims
Set the issued at time (iat) claim.
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.
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.
pub fn with_not_before(
claims: Claims,
nbf: timestamp.Timestamp,
) -> Claims
Set the not before time (nbf) claim.