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:
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/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
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
-
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: 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.
-
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 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 fromparse().decoder- Agleam/dynamic/decodedecoder 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 fromverify_and_validateorsign.decoder- Agleam/dynamic/decodedecoder 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:
- 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
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 withverifier()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:
- 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
Parameters
verifier- A verifier created withverifier()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 thejtivalue and returnsTrueif 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.