JWT verification library for OpenResty.
- Description
- Status
- Library non-goals
- Differences from lua-resty-jwt
- Supported features
- Missing features
- Dependencies
- JWT Verification Usage
- RFCs used as reference
- Run tests
JWT verification library for OpenResty.
The project's goal is to be a modern and slimmer replacement for lua-resty-jwt.
This project does not provide JWT manipulation or creation features: you can only verify/decrypt tokens.
Ready for testing: looking for more people to take it for a spin and provide feedback.
- JWT creation/modification
- Feature complete for the sake of RFCs completeness.
- Senseless and unsafe RFCs features (e.g. alg none) won't be implemented.
Main differences are:
- No JWT manipulation of any kind (you can only decrypt/verify them)
- Simpler internal structure reliant on more recent lua-resty-openssl and OpenSSL versions.
- Supports different JWE algorithms (see tables above).
If any of the points above are a problem, or you need compatibility with older OpenResty version, I recommend sticking with lua-resty-jwt.
- JWS verification: with symmetric or asymmetric keys.
- JWE decryption: with symmetric or asymmetric keys.
- Asymmetric keys format supported:
- PEM
- DER
- JWK
- JWT claim validation.
Claims | Implemented |
---|---|
alg | âś… |
jku | ❌ |
jwk | ❌ |
kid | ❌ |
x5u | ❌ |
x5c | ❌ |
x5t | ❌ |
x5t#S256 | ❌ |
typ | âś… |
cty | ❌ |
crit | âś… |
Alg | Implemented |
---|---|
HS256 | âś… |
HS384 | âś… |
HS512 | âś… |
RS256 | âś… |
RS384 | âś… |
RS512 | âś… |
ES256 | âś… |
ES384 | âś… |
ES512 | âś… |
PS256 | âś… |
PS384 | âś… |
PS512 | âś… |
none | ❌ |
Claims | Implemented |
---|---|
alg | âś… |
enc | âś… |
zip | ❌ |
jku | ❌ |
jwk | ❌ |
kid | ❌ |
x5u | ❌ |
x5c | ❌ |
x5t | ❌ |
x5t#S256 | ❌ |
typ | âś… |
cty | ❌ |
crit | âś… |
Alg | Implemented | Requirements |
---|---|---|
RSA1_5 | ❌ | |
RSA-OAEP | ❌ | |
RSA-OAEP-256 | ❌ | |
A128KW | âś… | OpenSSL 3.0+ |
A192KW | âś… | OpenSSL 3.0+ |
A256KW | âś… | OpenSSL 3.0+ |
dir | âś… | |
ECDH-ES | ❌ | |
A128GCMKW | ❌ | |
A192GCMKW | ❌ | |
A256GCMKW | ❌ | |
PBES2-HS256+A128KW | ❌ | |
PBES2-HS384+A192KW | ❌ | |
PBES2-HS512+A256KW | ❌ |
Enc | Implemented |
---|---|
A128CBC-HS256 | âś… |
A192CBC-HS384 | âś… |
A256CBC-HS512 | âś… |
A128GCM | âś… |
A192GCM | âś… |
A256GCM | âś… |
- Implement JWE validation with at least 1 asymmetric
alg
. - Nested JWT (i.e. JWT in JWE).
- JWKS workflow:
- Key retrieval via HTTP with lua-resty-http.
- Automatic and configurable keys rotation.
- Investigate keys caching (?).
luarocks install lua-cjson
luarocks install lua-resty-openssl
luarocks install lua-resty-http
syntax: header, err = jwt.decode_header_unsafe(token)
Read a jwt header and convert it to a lua table.
Important: this method does not validate JWT signature! Only use if you need to inspect the token's header without having to perform the full validation.
local jwt = require("resty.jwt-verification")
local token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE3MTY2NDkwNzJ9._MwFdsBPSyci9iARpoAaulReGcn1q7mKiPZjR2JDvdY"
local header, err = jwt.decode_header_unsafe(token)
if not header then
return nil, "malformed jwt: " .. err
end
print("alg: " .. header.alg) -- alg: HS256
syntax: decoded_token, err = jwt.verify(token, secret, options?)
Validate a JWS token and convert it to a lua table.
The optional parameter options
can be passed to configure the token validator. Valid fields are:
valid_signing_algorithms
(dict<string, string> | nil): a dict containing allowedalg
claims used to validate the JWT.typ
(string | nil): if non-null, ensure JWT claimtyp
matches the passed value.issuer
(string | nil): if non-null, ensure JWT claimiss
matches the passed value.audiences
(string | table | nil): if non-null, ensure JWT claimaud
matches one of the supplied values.subject
(string | nil): if non-null, ensure JWT claimsub
matches the passed value.jwtid
(string | nil): if non-null, ensure JWT claimjti
matches the passed value.ignore_not_before
(bool): If true, the JWT claimnbf
will be ignored.ignore_expiration
(bool): If true, the JWT claimexp
will be ignored.current_unix_timestamp
(datetime | nil): the JWTnbf
andexp
claims will be validated against this timestamp. If null, will use the current datetime supplied byngx.time()
.timestamp_skew_seconds
(int):
Default values for options
fields:
local verify_default_options = {
valid_signing_algorithms = {
["HS256"]="HS256", ["HS384"]="HS384", ["HS512"]="HS512",
["RS256"]="RS256", ["RS384"]="RS384", ["RS512"]="RS512",
["ES256"]="ES256", ["ES384"]="ES384", ["ES512"]="ES512",
["PS256"]="PS256", ["PS384"]="PS384", ["PS512"]="PS512",
},
typ = nil,
issuer = nil,
audiences = nil,
subject = nil,
jwtid = nil,
ignore_not_before = false,
ignore_expiration = false,
current_unix_timestamp = nil,
timestamp_skew_seconds = 1,
}
Minimal example with symmetric keys:
local jwt = require("resty.jwt-verification")
local token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE3MTY2NTUwMTV9.NuEhIzUuufJgPZ8CmCPnD4Vrw7EnTyWD8bGtYCwuDZ0"
local decoded_token, err = jwt.verify(token, "superSecretKey")
if not decoded_token then
return nil, "invalid jwt: " .. err
end
print(decoded_token.header.alg) -- HS256
print(decoded_token.payload.foo) -- bar
Minimal example with asymmetric keys:
local jwt = require("resty.jwt-verification")
local token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE3MTY2Njg2Mzd9.H6PE-zLizMMqefx8DG4X5glVjyxR9UNT225Tq2yufHhu4k9K0IGttpykjMCG8Ck_4Qt2ezEWIgoiWhSn1rv_zwxe7Pv-B09fDs7h1hbASi5MZ0YVAmK9ID1RCKM_NTBEnPLot_iopKZRj2_J5F7lvXwJDZSzEAFJZdrgjKeBS4saDZAv7SIL9Nk75rdhgY-RgRwsjmTYSksj7eioRJJLHifrMnlQDbdrBD5_Qk5tD6VPcssO-vIVBUAYrYYTa7M7A_v47UH84zDtzNYBbk9NrDbyq5-tYs0lZwNhIX8t-0VAxjuCyrrGZvv8_O01pdi90kQmntFIbaiDiD-1WlGcGA"
local decoded_token, err = jwt.verify(token, "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvXFhNyhFWuWtFSJqfOAw\np42lLIn9kB9oaciiKgNAYZ8SYw5t9Fo+Zh7IciVijn+cVS2/aoBNg2HhfdYgfpQ/\nsb6jwbRqFMln2GmG+X2aJ2wXMJ/QfxrPFdO9L36bAEwkubUTYXwgMSm1KqWRN8xX\n+oBu+dbyzw7iUbrmw0ybzXKZLJvetCvmt0reU5TvdwoczOWFBSKeYnzBrC6hISD8\n8TYDJ4tiw1EWVOupQGqgel0KjC7iwdIYi7PROn6/1MMnF48zlBbT/7/zORj84Z/y\nDnmxZu1MQ07kHqXDRYumSfCerg5Xw5vde7Tz8O0TWtaYV3HJXNa0VpN5OI3L4y7P\nhwIDAQAB\n-----END PUBLIC KEY-----")
if not decoded_token then
return nil, "invalid jwt: " .. err
end
print(decoded_token.header.alg) -- RS256
print(decoded_token.payload.foo) -- bar
Examples with custom options
:
local jwt = require("resty.jwt-verification")
local token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE3MTY2NTUwMTV9.NuEhIzUuufJgPZ8CmCPnD4Vrw7EnTyWD8bGtYCwuDZ0"
local decoded_token, err = jwt.verify(token, "superSecretKey", {
valid_signing_algorithms = {["HS256"]="HS256", ["HS384"]="HS384", ["HS512"]="HS512"}, -- only allow HS family algs
audiences = {"user", "admin"}, -- `aud` must be one of the following
ignore_not_before = true -- ignore `nbf` claim (not recommended)
})
if not decoded_token then
return nil, "invalid jwt: " .. err
end
print(decoded_token.header.alg) -- HS256
print(decoded_token.payload.foo) -- bar
syntax: decoded_token, err = jwt.decrypt(token, secret, options?)
Decrypt and validate a JWE token and convert it to a lua table.
The optional parameter options
can be passed to configure the token validator. Valid fields are:
valid_encryption_alg_algorithms
(dict<string, string> | nil): a dict containing allowedalg
claims used to decrypt the JWT.valid_encryption_enc_algorithms
(dict<string, string> | nil): a dict containing allowedenc
claims used to decrypt the JWT.typ
(string | nil): if non-null, ensure JWT claimtyp
matches the passed value.issuer
(string | nil): if non-null, ensure JWT claimiss
matches the passed value.audiences
(string | table | nil): if non-null, ensure JWT claimaud
matches one of the supplied values.subject
(string | nil): if non-null, ensure JWT claimsub
matches the passed value.jwtid
(string | nil): if non-null, ensure JWT claimjti
matches the passed value.ignore_not_before
(bool): If true, the JWT claimnbf
will be ignored.ignore_expiration
(bool): If true, the JWT claimexp
will be ignored.current_unix_timestamp
(datetime | nil): the JWTnbf
andexp
claims will be validated against this timestamp. If null, will use the current datetime supplied byngx.time()
.timestamp_skew_seconds
(int):
Default values for options
fields:
local decrypt_default_options = {
valid_encryption_alg_algorithms = {
["A128KW"]="A128KW", ["A192KW"]="A192KW", ["A256KW"]="A256KW",
["dir"]="dir",
},
valid_encryption_enc_algorithms = {
["A128CBC-HS256"]="A128CBC-HS256",
["A192CBC-HS384"]="A192CBC-HS384",
["A256CBC-HS512"]="A256CBC-HS512",
["A128GCM"]="A128GCM",
["A192GCM"]="A192GCM",
["A256GCM"]="A256GCM",
},
typ = nil,
issuer = nil,
audiences = nil,
subject = nil,
jwtid = nil,
ignore_not_before = false,
ignore_expiration = false,
current_unix_timestamp = nil,
timestamp_skew_seconds = 1,
}
Minimal example with symmetric keys:
local jwt = require("resty.jwt-verification")
local token = "eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.zAIq7qVAEO-eCG6gOdd3ld8_IHzeq3UlaWLHF2IDn6nNUuHh5n_i4w.5CM864cgiBgFPwluW4ViRg.mUeX7zHDVNsXhys0XO5S4w.t3yAR_HU0GDTEyCbpRa6BQ"
local decoded_token, err = jwt.decrypt(token, "superSecretKey12")
if not decoded_token then
return nil, "invalid jwt: " .. err
end
print(decoded_token.header.alg) -- A128KW
print(decoded_token.header.enc) -- A128CBC-HS256
print(decoded_token.payload.foo) -- bar
Minimal example with asymmetric keys:
TODO: not implemented
Examples with custom options
:
local jwt = require("resty.jwt-verification")
local token = "eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.zAIq7qVAEO-eCG6gOdd3ld8_IHzeq3UlaWLHF2IDn6nNUuHh5n_i4w.5CM864cgiBgFPwluW4ViRg.mUeX7zHDVNsXhys0XO5S4w.t3yAR_HU0GDTEyCbpRa6BQ"
local decoded_token, err = jwt.decrypt(token, "superSecretKey12", {
valid_encryption_alg_algorithms = {["A128KW"]="A128KW"}, -- only allow A128KW family algs (requires OpenSSL 3.0+)
valid_encryption_enc_algorithms = {["A128CBC-HS256"]="A128CBC-HS256"}, -- only allow A128CBC family encs
audiences = {"user", "admin"}, -- `aud` must be one of the following
ignore_not_before = true -- ignore `nbf` claim (not recommended)
})
if not decoded_token then
return nil, "invalid jwt: " .. err
end
print(decoded_token.header.alg) -- A128KW
print(decoded_token.header.enc) -- A128CBC-HS256
print(decoded_token.payload.foo) -- bar
- RFC 7515 JSON Web Signature (JWS)
- RFC 7516 JSON Web Encryption (JWE)
- RFC 7517 JSON Web Key (JWK)
- RFC 7518 JSON Web Algorithms (JWA)
- RFC 7519 JSON Web Token (JWT)
- RFC 7520 Examples of Protecting Content Using JSON Object Signing and Encryption (JOSE)
Install test suit:
sudo cpan Test::Nginx
Install openresty: see https://openresty.org/en/linux-packages.html
export PATH=/usr/local/openresty/nginx/sbin:$PATH
prove -r t