ywt/claim
Types
Values
pub fn audience(primary: String, others: List(String)) -> Claim
Validates that the JWT is intended for your service, and includes the primary audience in generated tokens.
Include this to ensure JWTs are only used by their intended recipients, preventing tokens meant for other services from being accepted.
Example
// For an API service
let claims = [
claim.audience("api.myapp.com", ["admin-api.myapp.com"]),
]
pub fn custom(
name name: String,
value value: a,
encode encode: fn(a) -> json.Json,
decoder decoder: decode.Decoder(a),
) -> Claim
Creates a custom claim with a specific name and value that must match exactly during validation.
Use this for application-specific claims like roles, permissions, or custom business logic requirements.
Example
// Add role-based authorization
let claims = [
claim.custom(
name: "role",
value: "admin",
encode: json.string,
decoder: decode.string
),
claim.custom(
name: "permissions",
value: ["read", "write", "delete"],
encode: json.array(_, json.string),
decoder: decode.list(decode.string)
),
claim.expires_at(max_age: duration.hours(1), leeway: duration.minutes(5))
]
pub fn encode(
claims: List(Claim),
data: List(#(String, json.Json)),
) -> json.Json
Encode claims and application data into JSON.
This function merges application data with JWT claims into a single JSON object for token encoding. When the same field appears in both lists, claims take precedence to ensure security-critical values cannot be overridden.
🚨 This is a low-level function used internally by ywt.
Example
let user_data = [
#("name", json.string("Alice")),
#("role", json.string("user")),
#("exp", json.int(1234567890)) // This will be overridden
]
let security_claims = [
expires_at(max_age: duration.hours(1), leeway: duration.minutes(5)),
custom("role", "admin", json.string, decode.string) // This takes precedence
]
let payload = encode(security_claims, user_data)
// Result: {"name": "Alice", "role": "admin", "exp": <actual_expiry>}
💡 Best Practice: Place security-sensitive fields in claims rather than data to validate them and ensure they cannot be accidentally overridden.
pub fn encode_numeric_date(
timestamp: timestamp.Timestamp,
) -> json.Json
Encodes a timestamp into the JWT numeric date format (seconds since Unix epoch).
Use this when manually creating JWT payloads with timestamp values.
Example
// Add custom timestamp to payload
let payload = [
#("custom_date", encode_numeric_date(my_timestamp))
]
pub fn expires_at(
max_age max_age: duration.Duration,
leeway leeway: duration.Duration,
) -> Claim
Sets an expiration time for the JWT, automatically rejecting expired tokens.
ALWAYS include this - it’s your primary defense against token replay attacks and limits the damage from compromised tokens.
Security Considerations
- Make expiration times as short as reasonably possible
- Include reasonable leeway for clock differences
Example
// Short-lived API access token
let api_claims = [
claim.expires_at(max_age: duration.minutes(30), leeway: duration.minutes(5))
]
// Longer-lived refresh token
let session_claims = [
claim.expires_at(max_age: duration.hours(24 * 7), leeway: duration.minutes(5))
]
🚨 Critical: Never create or parse JWTs without expiration times - they would be valid forever if compromised.
pub fn id(id: String, others: List(String)) -> Claim
Adds a unique identifier (jti) to the JWT for tracking and revocation.
Use this when you need to track specific tokens or implement token revocation mechanisms.
pub fn issued_at() -> Claim
Adds the current timestamp as the “issued at” (iat) claim.
Include this in JWTs to track when tokens were issued, useful for debugging
and audit trails. To reject tokens that were created in the future, use not_before.
Example
let claims = [
claim.expires_at(max_age: duration.hours(1), leeway: duration.minutes(5)),
...
]
pub fn issuer(issuer: String, others: List(String)) -> Claim
Validates that the JWT comes from a trusted issuer, and includes that issuer in generated tokens.
Use this to ensure JWTs are only accepted from authorized authentication services,
preventing token misuse across different services. It is common practice to
set the issuer to a URL with a .well-known/jwks.json endpoint, from which
public verification keys can be fetched.
Example
// Only accept tokens from your auth service
let claims = [
claim.issuer("https://auth.example.com", [])
]
pub fn not_before(
time time: timestamp.Timestamp,
leeway leeway: duration.Duration,
) -> Claim
Adds a “not before” (nbf) claim that prevents the JWT from being valid until the current time.
Use this for JWTs that should only become valid at a specific time, such as scheduled operations or delayed activations.
Example
// Token becomes valid immediately, allowing 1 minute clock skew
let claims = [
claim.not_before(timestamp.system_time(), leeway: duration.minutes(1)),
claim.expires_at(max_age: duration.hours(2), leeway: duration.minutes(1))
]
pub fn numeric_date_decoder() -> decode.Decoder(
timestamp.Timestamp,
)
Decodes JWT numeric date values (seconds since Unix epoch) into timestamp objects.
Use this when manually processing JWT claims that contain timestamp values.
Example
// Manually decode an expiration claim
let exp_decoder = decode.field("exp", numeric_date_decoder())
pub fn subject(subject: String, others: List(String)) -> Claim
Identifies the subject (typically a user) that the JWT represents.
Use this to bind JWTs to specific users or entities, enabling proper authorization decisions. This could be an internal user id.
Security Considerations
- Use stable, unique identifiers (user IDs, not usernames)
- Don’t include personally identifiable information
Example
// Bind token to a specific user
let claims = [
claim.subject("user_12345", []),
claim.expires_at(max_age: duration.hours(1), leeway: duration.minutes(5)),
claim.audience("api.myapp.com", [])
]
pub fn verify(
payload: dynamic.Dynamic,
claims: List(Claim),
) -> Result(Nil, @internal ParseError)
Validates all claims against a JWT payload.
This function checks each claim in the provided list against the JWT payload, returning the first validation error encountered or success if all claims pass.
🚨 This is a low-level function used internally by ywt.
Example
let payload = // ... decoded JWT payload
let security_claims = [
expires_at(max_age: duration.hours(1), leeway: duration.minutes(5)),
issuer("https://auth.myapp.com", []),
audience("https://api.myapp.com", []),
custom("role", "admin", json.string, decode.string)
]
case verify(payload, security_claims) {
Ok(Nil) -> {
// All claims validated successfully
process_authenticated_request(payload)
}
Error(TokenExpired(expired_at)) -> {
log.info("Token expired at: " <> timestamp.to_string(expired_at))
return_authentication_error()
}
Error(InvalidIssuer(expected, actual)) -> {
log.warning("Invalid issuer - expected: " <> expected <> ", got: " <> actual)
return_forbidden_error()
}
Error(error) -> {
log.error("Claim validation failed: " <> string.inspect(error))
return_bad_request_error()
}
}
⚠️ Important: This function only validates claims - it does not verify
cryptographic signatures. Use the full decode() function for complete JWT validation.