ESC
Type to search...
S
Soli Docs

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 token
secret : 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 settings

Options

expires_in : Int - Token lifetime in seconds
algorithm : String - HS256, HS384, HS512, RS256, or EdDSA (default: HS256)
key : String - PEM-encoded private key for RS256/EdDSA algorithms
token = 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 verify
secret : String - The secret key (HMAC) or public key material (RSA/EdDSA). Same 32-byte minimum for HMAC algorithms (SEC-054).
options : Hash? - Optional settings

Options

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