jwt.
JWT for iyi
v0.1.0Get started
iyi get github.com/iyilang/jwt
iyi get adds it to iyi.mod and fetches its newest version tag. Import it as github.com/iyilang/jwt::*.
README
iyi-jwt
JSON Web Tokens for iyi, after crystal-community/jwt: tokens signed with HMAC, RSA, RSA-PSS, ECDSA or Ed25519, decoded with their signature verified and their registered claims checked. Written in iyi, with no dependency outside iyi's own library.
Errors are values (SPEC.md III.1): encode answers the token or an EncodeError, decode the Token or a Failure.
Requirements
iyi 0.18.0 or later.
Installation
iyi get github.com/iyilang/jwt --as jwt
Files then import jwt for the root module (Jwt.encode, Jwt.decode, Jwt.decode_unverified, Jwt::Token, Jwt::VERSION) and its other modules as jwt/jwt/...: the short name, then the module's path in the package.
Usage
module app
import jwt
import jwt/jwt/algorithms::{Algorithm}
import jwt/jwt/errors::{Failure}
def run : Nil | Failure
token = Jwt.encode({"foo" => "bar"}, "SecretKey", Algorithm::HS256)!
# => "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIifQ.Y3shN5Wh..."
decoded = Jwt.decode(token, "SecretKey", Algorithm::HS256)!
decoded.payload["foo"].as_s # => "bar"
decoded.header["alg"].as_s # => "HS256"
nil
end
The payload is anything JSON.to_json writes as an object: a Hash(String, V), a Hash(String, Any) for mixed values, a type that derives serializable. A decoded token's header and payload are Hash(String, Any).
Header parameters go in a hash after the algorithm. typ replaces JWT; alg is passed over, since the header names the algorithm the token is signed with:
Jwt.encode(payload, key, Algorithm::RS256, {"kid" => "2026-10"})!
The key can be chosen from the token's header â by its kid, say â with a block. The header and the payload reach the block unverified; nil refuses the to
Jwt.decode(token, Algorithm::RS256) do |header, payload|
public_keys[header["kid"].as_s]?
end!
Jwt.decode_unverified(token) reads a token with nothing checked, neither its signature nor its claims: for looking at a token, never for trusting it.
Errors
case result = Jwt.decode(token, key, Algorithm::HS256, aud: "api")
in Jwt::Token then serve(result.payload)
in ExpiredSignatureError then puts "log in again"
in Failure then puts "refused: #{result.message}"
end
| Error | When |
|---|---|
DecodeError | not three segments of unpadded base64url JSON objects; alg missing; exp or nbf not a number; a crit header |
VerificationError | the signature is not the key's; the header names another algorithm than the one asked for; the key block answered nil |
InvalidKeyError | the key is not one the algorithm takes |
UnsupportedAlgorithmError | signing with ES384 or ES512 |
ExpiredSignatureError | exp is now or past |
ImmatureSignatureError | nbf is still to come |
InvalidAudienceError | aud asked for, and missing or another |
InvalidIssuerError | iss asked for, and missing or another |
InvalidSubjectError | sub asked for, and missing or another |
InvalidTypeError | typ asked for, and the header's is missing or another |
ValidationError is the last six, EncodeError what encode answers, and Failure all of them; each is in jwt/jwt/errors.
Algorithms
| Algorithm | Signs with | Verifies with |
|---|---|---|
None | nothing: the signature is empty | an empty signature only |
HS256, HS384, HS512 | the secret | the secret |
RS256, RS384, RS512, PS256, PS384, PS512 | a PEM PRIVATE KEY or RSA PRIVATE KEY | a PEM PUBLIC KEY or RSA PUBLIC KEY, or the private key |
ES256 | a PEM PRIVATE KEY or EC PRIVATE KEY on P-256 | a PEM PUBLIC KEY, or the private key |
ES384, ES512 | â | a PEM PUBLIC KEY on P-384 or P-521, or the private key |
EdDSA (Ed25519) | the 32-byte seed in hex, or a PEM PRIVATE KEY | the 32-byte public key in hex, a PEM PUBLIC KEY, or the private key in PEM |
RSA keys must be 2048 bits or more (RFC 7518 §3.3). An EC key must be on the algorithm's curve. ES384 and ES512 verify only: iyi's std/ecdsa signs on P-256 alone
Claims
decode checks, once the signature holds:
expâ refused from that second on (RFC 7519 §4.1nbfâ refused before that second (§4.1aud:â when given,audmust be that string or an array holding iiss:,sub:â when given, the claim must be that strintyp:â when given, the header'stypmust be it, ignoring cas
validate: false skips the claims and keeps the signature check; typ: is checked either way.
Jwt.decode(token, key, Algorithm::HS256, aud: "api", iss: "auth.example.com")!
Differences from crystal-community/jwt
encodeanddecodeanswer errors as values rather than raising.- The algorithm is always the caller's.
decoderefuses a token whose header names another one, and the block form takes the algorithm as an argument rather than reading it from the header. - No
verify: false;Jwt.decode_unverifiedreads a token without checking it. - A hex Ed25519 key given to
decodeis the public key, never a seed to derive one from: trying the public key as a seed lets anyone sign a token it accepts. exprefuses the token at its own second, as RFC 7519 says, where crystal-community/jwt accepted it for that second.- A header with
critis refused (RFC 7515 §4.1.11), and so is a payload that is not a JSON object - RSA keys under 2048 bits are refused.
- Not ported: ES256K (iyi's library has no secp256k1), signing with ES384 and ES512, and JWKS.
Development
iyi test .
iyi fmt --check jwt.iyi jwt_test.iyi jwt examples