ywt

Types

Detailed error information for JWT parsing failures.

This provides specific information about why JWT validation failed, enabling better error handling, logging, and debugging. Different error types allow you to respond appropriately - for example, expired tokens might trigger token refresh, while signature failures indicate potential attacks.

Error Categories

  • Format Errors: Malformed JWT structure or encoding issues
  • Signature Errors: Cryptographic validation failures
  • Claim Errors: Business logic validation failures
  • Key Errors: Issues with cryptographic keys

Usage Example

use decoded <- promise.await(ywt.decode(jwt, using: decoder, claims: claims, keys: keys))
case decoded {
  Ok(payload) -> {
    // Success - use the payload
    next(payload)
  }
  Error(TokenExpired(expired_at)) -> {
    // Handle expired token - maybe refresh
    log.info("Token expired at: " <> timestamp.to_string(expired_at))
    redirect_to_refresh()
  }
  Error(InvalidSignature) -> {
    // Potential security issue - log and reject
    log.warning("Invalid JWT signature detected")
    unauthorized()
  }
  Error(InvalidIssuer(expected, actual)) -> {
    // Wrong issuer - reject but log for debugging
    log.error("Wrong issuer - expected: " <> expected <> ", got: " <> actual)
    forbidden()
  }
  Error(_) -> {
    // Generic handling for other errors
    bad_request()
  }
}
pub type ParseError {
  MalformedToken
  InvalidHeaderEncoding
  InvalidPayloadEncoding
  InvalidSignatureEncoding
  InvalidHeaderJson(json.DecodeError)
  InvalidPayloadJson(json.DecodeError)
  NoMatchingKey
  InvalidSignature
  TokenExpired(expired_at: timestamp.Timestamp)
  TokenNotYetValid(not_before: timestamp.Timestamp)
  InvalidIssuer(expected: List(String), actual: String)
  InvalidAudience(expected: List(String), actual: String)
  InvalidSubject(expected: List(String), actual: String)
  InvalidId(expected: List(String), actual: String)
  MissingClaim(claim_name: String)
  ClaimDecodingError(
    claim_name: String,
    error: List(decode.DecodeError),
  )
  InvalidCustomClaim(claim_name: String)
  PayloadDecodingError(List(decode.DecodeError))
}

Constructors

  • MalformedToken

    JWT doesn’t contain exactly 3 parts separated by dots

  • InvalidHeaderEncoding

    Base64 decoding failed for header

  • InvalidPayloadEncoding

    Base64 decoding failed for payload

  • InvalidSignatureEncoding

    Base64 decoding failed for signature

  • InvalidHeaderJson(json.DecodeError)

    JSON parsing or decoding failed for header

  • InvalidPayloadJson(json.DecodeError)

    JSON parsing failed for payload - note that this does not include failures for your payload decoder. See PayloadDecodingError.

  • NoMatchingKey

    No suitable key found to verify the signature

  • InvalidSignature

    Signature verification failed (potential tampering)

  • TokenExpired(expired_at: timestamp.Timestamp)

    Token has expired

  • TokenNotYetValid(not_before: timestamp.Timestamp)

    Token is not yet valid (nbf claim)

  • InvalidIssuer(expected: List(String), actual: String)

    Wrong issuer

  • InvalidAudience(expected: List(String), actual: String)

    Wrong audience

  • InvalidSubject(expected: List(String), actual: String)

    Wrong audience

  • InvalidId(expected: List(String), actual: String)
  • MissingClaim(claim_name: String)

    Required claim is missing

  • ClaimDecodingError(
      claim_name: String,
      error: List(decode.DecodeError),
    )

    Claim Decoding Error

  • InvalidCustomClaim(claim_name: String)
  • PayloadDecodingError(List(decode.DecodeError))

    Payload structure doesn’t match expected decoder

Values

pub fn decode(
  jwt jwt: String,
  using decoder: decode.Decoder(payload),
  claims claims: List(claim.Claim),
  keys keys: List(verify_key.VerifyKey),
) -> promise.Promise(Result(payload, ParseError))

Verifies a JWT signature and validates all claims, returning the decoded payload if successful.

Use this to validate incoming JWTs from clients, ensuring they’re authentic and haven’t been tampered with.

Security Considerations

  • Implement appropriate claims validation (expiration, issuer, audience)
  • Handle verification failures securely (don’t leak information)
  • Consider rate limiting to prevent brute force attacks

Example

// Define expected claims for validation
let claims = [
  claim.expires_at(max_age: duration.hours(1), leeway: duration.minutes(5)),
  claim.issuer("my-app", []),
]

// Parse and validate the JWT
use decoded <- promise.await(
  ywt.decode(jwt_token, using: payload_decoder, claims:, keys: [verify_key])
)
case decoded {
  Ok(payload) -> {
    // JWT is valid, use the payload
    let user_id = // extract user ID from payload
    authorize_request(user_id)
  }
  Error(_) -> {
    // JWT is invalid - reject the request
    unauthorized_response()
  }
}

💡 Best Practice: Always validate JWTs on every request and never trust client-provided tokens without verification.

pub fn decode_unsafely_without_validation(
  jwt: String,
  payload_decoder: decode.Decoder(payload),
) -> Result(payload, Nil)

Extracts payload from a JWT without verifying its signature or claims.

🚨 USE WITH EXTREME CAUTION Only use this when you need to inspect tokens from trusted sources where signature verification is handled elsewhere.

Security Considerations

  • NEVER use this in production for authentication
  • Tokens could be forged or tampered with
  • No expiration or claim validation is performed
  • Only use when signature validation happens at a different layer
  • Consider this as dangerous as accepting any user input
pub fn encode(
  payload payload: List(#(String, json.Json)),
  claims claims: List(claim.Claim),
  key key: sign_key.SignKey,
) -> promise.Promise(String)

Creates a signed JWT containing the specified payload and claims.

Signed JWTs prevent actors without access to the SignKey from modifying it. This can be verified using the corresponding VerifyKey.

Security Considerations

  • JWTs are signed, not encrypted - all data is publicly readable
  • Never include sensitive data like passwords or personal information
  • Keep payloads small to avoid large tokens
  • Include appropriate expiration times in your claims
  • Use strong signing keys and rotate them regularly

If a field is present in claims as well as the payload, the payload takes precedence.

Example

// Create a user session token
let payload = [
  #("sub", json.string("user_12345")),
  #("role", json.string("developer")),
  #("permissions", json.array([json.string("read"), json.string("write")]))
]

let claims = [
  claim.issued_at(),
  claim.expires_at(max_age: duration.hours(1), leeway: duration.minutes(5)),
  claim.issuer("my-app", []),
]

use jwt <- promise.await(ywt.encode(payload: payload, claims: claims, key: sign_key))
// Returns: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ..."

⚠️ Remember: Anyone can decode and read JWT contents. Only include data you’re comfortable being public.

pub fn generate_key(
  algorithm: algorithm.Algorithm,
) -> promise.Promise(sign_key.SignKey)

Generates a new cryptographically secure signing key for the specified algorithm.

This function creates fresh signing keys with appropriate parameters for each algorithm type. All keys are generated using cryptographically secure random number generators and follow current security best practices.

The generated keys will have a random id set that can be used to identify the key during key rotation.

Usage

Use this for key generation in development, testing, or when implementing key rotation systems. For production use, consider generating keys offline and storing them in secure key management systems.

Key Parameters

  • HMAC: Full-entropy random secrets of appropriate length (at least 32/48/64 bytes)
  • RSA: 4096-bit modulus with secure random prime generation
  • ECDSA: Secure random private scalars on specified curves. The digest type matches the selected curve.
pub fn sign_bits(
  message: BitArray,
  key: sign_key.SignKey,
) -> promise.Promise(BitArray)

Creates a cryptographic signature for the given message using the specified signing key.

This is a low-level function that performs the actual cryptographic signing operation.

Usage

This function is primarily used internally by higher-level JWT functions. Use this directly only when you need raw signature operations outside of JWT context.

Security Considerations

  • Never sign untrusted or unvalidated input data
  • Ensure signing keys are stored securely and accessed only by authorized code

Example

let message = <<"Hello, world!":utf8>>
let signing_key = load_secure_signing_key()

use signature <- promise.await(ywt.sign_bits(message, signing_key))
// signature is raw bytes that can be verified with corresponding verify key

⚠️ Warning: This function performs raw cryptographic operations. Most applications should use the higher-level JWT functions instead.

pub fn sign_string(
  message: String,
  key: sign_key.SignKey,
) -> promise.Promise(String)

Creates a base64url-encoded signature for a UTF-8 string message.

This is a convenience wrapper around sign_bits that handles string encoding and base64url encoding of the signature, commonly used for signing JWT components.

Usage

Use this when you need to sign string data and want the signature in base64url format for use in web contexts like JWTs or HTTP headers.

Security Considerations

  • Same security considerations as sign_bits apply
  • The string is encoded as UTF-8 before signing
  • Verify signatures using the corresponding verify_string function

Example

let jwt_payload = "eyJzdWIiOiIxMjM0NTY3ODkwIn0"
let signing_key = load_jwt_signing_key()

use signature <- promise.await(sign_string(jwt_payload, signing_key)))
// signature is base64url-encoded string ready for JWT use

💡 Note: The returned signature is base64url-encoded (URL-safe, no padding) as required by JWT and other web standards.

pub fn verify_bits(
  message: BitArray,
  signature: BitArray,
  key: verify_key.VerifyKey,
) -> promise.Promise(Bool)

Verifies a cryptographic signature against a message using the specified verification key.

This is a low-level function that performs the actual cryptographic verification. Returns True if the signature is valid for the given message and key, False otherwise.

Usage

Use this for raw signature verification operations. The verification algorithm is determined by the key type and must match the algorithm used for signing.

Example

let message = <<"Hello, world!":utf8>>
let signature = // ... received signature bytes
let verify_key = load_verification_key()

use is_valid <- promise.await(verify_bits(message, signature, verify_key))
case is_valid {
  True -> process_verified_message(message)
  False -> reject_invalid_signature()
}

🔒 Critical: Never trust data with invalid signatures. A False result indicates potential tampering or use of wrong keys.

pub fn verify_string(
  message: String,
  signature: String,
  key: verify_key.VerifyKey,
) -> promise.Promise(Bool)

Verifies a base64url-encoded signature against a UTF-8 string message.

This is a convenience wrapper around verify_bits that handles string encoding and base64url decoding of the signature. Commonly used for verifying JWT signatures.

Usage

Use this when verifying signatures that are base64url-encoded, such as those from JWTs or other web-based cryptographic protocols.

Security Considerations

  • Same security considerations as verify_bits apply
  • Invalid base64url encoding in the signature automatically returns False

Example

let payload = "eyJzdWIiOiIxMjM0NTY3ODkwIn0"
let signature = "base64url-encoded-signature-string"
let verify_key = load_jwt_verification_key()

use is_valid <- promise.await(verify_string(payload, signature, verify_key))
case is_valid {
  True -> {
    // Signature is valid, payload can be trusted
    process_authenticated_request(payload)
  }
  False -> {
    // Signature invalid - reject the request
    return_authentication_error()
  }
}

⚠️ Important: Malformed base64url signatures return False rather than causing errors, ensuring consistent handling of invalid input.

Search Document