ywt

ywt is a suite of Gleam-native JWT packages. This is the ywt_core package, underpinning the platform-specific libraries. You should always use it in combination with a platform-specific package.

Package Version Hex Docs Package Version Hex Docs Package Version Hex Docs

gleam add ywt_core@1 ywt_erlang@1 # for erlang
gleam add ywt_core@1 ywt_webcrypto@1 # for javascript

ywt supports symmetrically as well as asymmetrically signed JWTs and can be used on the client as well as the server using an almost identical API.

Instead of wrapping existing libraries, ywt uses the underlying platform APIs for cryptography - the public_key application and WebCrypto - directly with minimal amounts of straightforward FFI. This keeps most code related to parsing, validating and signing JWTs in pure Gleam, reducing the surface area.

Supported Algorithms

Important Security Considerations

JWTs are often misused. You can think of them as signed structured cookies. All information you store in a JWT is publically readable even without access to a coresponding key and/or after the token has expired or got revoked. JWTs do not have a built-in method for revokation. ywt does does not add claims by default. It is paramount to at least at an expired_at claim. The standard requires implementations to reject tokens and keys with unknown fields, but ywt silently ignores them as a consequence of using the decode API. ywt does not validate key_ops or use fields. ywt does not support validating a certificate chain or encrypted/nested keys.

Example (ywt_erlang)

import gleam/string
import gleam/dynamic/decode
import gleam/io
import gleam/json
import gleam/time/duration
import ywt
import ywt/algorithm
import ywt/claim
import ywt/verify_key

pub fn main() -> Nil {
  // Generate a new, random signing key
  let signing_key = ywt.generate_key(algorithm.es384)

  // Create user payload data
  let payload = [
    #("sub", json.string("user123")),
    #("role", json.string("admin")),
  ]

  // Define security claims
  let claims = [
    claim.expires_at(max_age: duration.hours(1), leeway: duration.minutes(5)),
    claim.issuer("https://auth.myapp.com", []),
    claim.audience("https://api.myapp.com", []),
  ]

  // Create and sign the JWT
  let jwt = ywt.encode(payload, claims, signing_key)
  io.println("Signed JWT: " <> jwt)

  // Extract verification key (for distribution to other services)
  let verify_key = verify_key.derived(signing_key)
  io.println(
    "Public verification key: " <> json.to_string(verify_key.to_jwk(verify_key)),
  )

  // Verify the JWT
  let decoder = {
    use id <- decode.field("sub", decode.string)
    use role <- decode.field("role", decode.string)
    decode.success(#(id, role))
  }

  let result = ywt.decode(jwt, using: decoder, claims:, keys: [verify_key])

  case result {
    Ok(#(id, role)) -> {
      io.println("JWT verified successfully!")
      io.println("User id: " <> id)
      io.println("User role: " <> role)
    }

    Error(ywt.TokenExpired(_expired_at)) -> {
      io.println("Token expired!")
    }

    Error(ywt.InvalidSignature) -> {
      io.println("Invalid signature - token may be forged")
    }

    Error(error) -> {
      io.println("JWT verification failed: " <> string.inspect(error))
    }
  }
}

Development

Tests are shared by both targets and can be run from their individual package directories.

Resources:

Search Document