JWT Functions
Create, verify, and decode JSON Web Tokens for authentication.
Token Operations
jwt_sign(payload, secret, options?)
Create a signed JWT token.
Parameters
payload : Hash - The claims to encode in the tokensecret : String - The secret key for signing. Must be at least 32 bytes (SEC-054). Load a high-entropy value from .env, e.g. generate it once with openssl rand -hex 32 and reference it as getenv("JWT_SECRET"). Never commit the secret to source.options : Hash? - Optional settingsOptions
expires_in : Int - Token lifetime in secondsalgorithm : String - HS256, HS384, HS512, RS256, or EdDSA (default: HS256)key : String - PEM-encoded private key for RS256/EdDSA algorithmstoken = jwt_sign(
{ "sub": "user123", "role": "admin" },
getenv("JWT_SECRET"),
{ "expires_in": 3600 }
)
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
token = jwt_sign(
{ "sub": "user123", "role": "admin" },
"ignored_for_rsa", # still required but unused when key is provided
{
"algorithm": "RS256",
"key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BBQEw...\n-----END PRIVATE KEY-----"
}
)
jwt_verify(token, secret, options?)
Verify and decode a JWT token. The verifier — not the token — chooses which algorithm is acceptable (SEC-091), closing the classic algorithm-confusion attack where an attacker who knew the verifier's RSA public key could sign an HS256 token using the public key bytes as an HMAC secret.
Parameters
token : String - The JWT token to verifysecret : String - The secret key (HMAC) or public key material (RSA/EdDSA). Same 32-byte minimum for HMAC algorithms (SEC-054).options : Hash? - Optional settingsOptions
algorithm : String - Pin verification to a specific algorithm (HS256, HS384, HS512, RS256, EdDSA). The token's header alg must match exactly or the call rejects.key : String - PEM-encoded public key for RS256/EdDSA algorithms. When key is provided without an explicit algorithm, the allowed set is RS256 / EdDSA only — HMAC tokens are rejected (the algorithm-confusion attack vector).
Without options, the 2-arg form accepts only HMAC algorithms (HS256/HS384/HS512), matching the back-compat default for existing jwt_verify(token, secret) callers.
Returns
Hash - The payload if valid, or { "error": true, "message": String } if invalid
# 2-arg form: HMAC only.
result = jwt_verify(token, getenv("JWT_SECRET"))
if has_key(result, "error")
println("Invalid token: " + result["message"])
else
println("User: " + result["sub"])
println("Role: " + result["role"])
end
# Asymmetric verification: pin the algorithm explicitly.
result = jwt_verify(token, "", { "algorithm": "RS256", "key": rsa_public_pem })
jwt_decode_unsafe(token)
Decode a JWT without verifying signature or expiration. The result is wrapped as {unverified: true, claims: {...}} so it cannot be confused with a verified jwt_verify response. Never trust these claims for authentication — use jwt_verify(token, secret) for that.
The previous jwt_decode(token) returned the same shape as jwt_verify, which made claims["sub"] a silent auth bypass. It was removed in SEC-029; calling it raises a migration error.
Parameters
token : String - The JWT token to decode
Returns
Hash — {unverified: true, claims: {...}} on success, {error: true, message: ...} on a malformed token
# Inspection only — DO NOT use for auth
let result = jwt_decode_unsafe(token)
println(result["claims"]["sub"])
Common Patterns
Authentication Flow
# Login endpoint
def login(email: String, password: String) -> Hash
user = User.find_by_email(email)
if !user || !argon2_verify(password, user["password_hash"])
return { "error": "Invalid credentials" }
end
token = jwt_sign(
{ "sub": str(user["id"]), "role": user["role"] },
getenv("JWT_SECRET"),
{ "expires_in": 86400 } # 24 hours
)
{ "token": token }
end
# Protected endpoint middleware
def authenticate(req: Hash) -> Hash?
auth_header = req["headers"]["Authorization"] ?? ""
if !contains(auth_header, "Bearer ")
return null
end
token = substring(auth_header, 7)
result = jwt_verify(token, getenv("JWT_SECRET"))
if has_key(result, "error")
return null
}
result
end