ywt
ywt is a suite of Gleam-native JWT packages. This is the ywt_webcrypto
package, providing cryptographic routines
using the SubtleCrypto
web api to ywt. It supports the server as well as the browser.
gleam add ywt_core@1 ywt_webcrypto@1
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
HS256
- HMAC with SHA-256HS384
- HMAC with SHA-384HS512
- HMAC with SHA-512ES256
- ECDSA using the P-256 (secp256r1) curve and SHA-256ES384
- ECDSA using the P-384 (secp384r1) curve and SHA-384ES512
- ECDSA using the P-512 (secp521r1) curve and SHA-512RS256
- RSA PKCS#1 v1.5 with SHA-256RS384
- RSA PKCS#1 v1.5 with SHA-384RS512
- RSA PKCS#1 v1.5 with SHA-512PS256
- RSA PSS with SHA-256PS384
- RSA PSS with SHA-384PS512
- RSA PSS with SHA-512
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
import gleam/dynamic/decode
import gleam/io
import gleam/javascript/promise
import gleam/json
import gleam/string
import gleam/time/duration
import ywt
import ywt/algorithm
import ywt/claim
import ywt/verify_key
pub fn main() -> promise.Promise(_) {
// Generate a new, random signing key
use signing_key <- promise.await(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
use jwt <- promise.await(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))
}
use result <- promise.await(
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))
}
}
promise.resolve(Nil)
}
Development
Tests are shared by both targets and can be run from their individual package directories.
Resources:
- JWT debugger: jwt.io
- JWT (Json Web Tokens): RFC 7519
- JWS (Json Web Signatures): RFC 7515
- JWK (Json Web Keys): RFC 7517
- JWA (Json Web Algorithms): RFC 7518
- Elliptic Curve Cryptography: RFC 5480
- RSA: RFC 3447
- HMAC: RFC 2104
- Erlang
public_key
module: Docs - Erlang-JOSE : github
- WebCrypto/SubtleCrypto: MDN
- jsonwebtoken library: npm