ywt/claim

Types

A Claim is a validation rule on the JWT. You can for example validate that the issuer has a value you expect, or that the JWT is not expired yet.

pub opaque type Claim

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.

Search Document