jwt: JWT For The Great nginx-module-lua
Installation
If you haven't set up RPM repository subscription, sign up. Then you can proceed with the following steps.
CentOS/RHEL 7 or Amazon Linux 2
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install lua-resty-jwt
CentOS/RHEL 8+, Fedora Linux, Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install lua5.1-resty-jwt
To use this Lua library with NGINX, ensure that nginx-module-lua is installed.
This document describes lua-resty-jwt v0.1.11 released on Jul 11 2017.
lua-resty-jwt - JWT for ngx_lua and LuaJIT
Attention the hmac lib used here is lua-resty-hmac, not the one in luarocks.
Status
This library is under active development but is considered production ready.
Description
This library requires an nginx build with OpenSSL, the ngx_lua module, the LuaJIT 2.0, the lua-resty-hmac, and the lua-resty-string,
Synopsis
# nginx.conf:
server {
default_type text/plain;
location = /verify {
content_by_lua '
local cjson = require "cjson"
local jwt = require "resty.jwt"
local jwt_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" ..
".eyJmb28iOiJiYXIifQ" ..
".VAoRL1IU0nOguxURF2ZcKR0SGKE1gCbqwyh8u2MLAyY"
local jwt_obj = jwt:verify("lua-resty-jwt", jwt_token)
ngx.say(cjson.encode(jwt_obj))
';
}
location = /sign {
content_by_lua '
local cjson = require "cjson"
local jwt = require "resty.jwt"
local jwt_token = jwt:sign(
"lua-resty-jwt",
{
header={typ="JWT", alg="HS256"},
payload={foo="bar"}
}
)
ngx.say(jwt_token)
';
}
}
Methods
To load this library,
- you need to specify this library's path in ngx_lua's lua_package_path directive. For example,
lua_package_path "/path/to/lua-resty-jwt/lib/?.lua;;";
. - you use
require
to load the library into a local Lua variable:
local jwt = require "resty.jwt"
sign
syntax: local jwt_token = jwt:sign(key, table_of_jwt)
sign a table_of_jwt to a jwt_token.
The alg
argument specifies which hashing algorithm to use (HS256
, HS512
, RS256
).
sample of table_of_jwt
{
"header": {"typ": "JWT", "alg": "HS512"},
"payload": {"foo": "bar"}
}
verify
syntax: local jwt_obj = jwt:verify(key, jwt_token [, claim_spec [, ...]])
verify a jwt_token and returns a jwt_obj table. key
can be a pre-shared key (as a string), or a function which takes a single parameter (the value of kid
from the header) and returns either the pre-shared key (as a string) for the kid
or nil
if the kid
lookup failed. This call will fail if you try to specify a function for key
and there is no kid
existing in the header.
See Verification for details on the format of claim_spec
parameters.
load & verify
syntax: local jwt_obj = jwt:load_jwt(jwt_token)
syntax: local verified = jwt:verify_jwt_obj(key, jwt_obj [, claim_spec [, ...]])
verify = load_jwt + verify_jwt_obj
load jwt, check for kid, then verify it with the correct key
sample of jwt_obj
{
"raw_header": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9",
"raw_payload: "eyJmb28iOiJiYXIifQ",
"signature": "wrong-signature",
"header": {"typ": "JWT", "alg": "HS256"},
"payload": {"foo": "bar"},
"verified": false,
"valid": true,
"reason": "signature mismatched: wrong-signature"
}
sign-jwe
syntax: local jwt_token = jwt:sign(key, table_of_jwt)
sign a table_of_jwt to a jwt_token.
The alg
argument specifies which hashing algorithm to use for encrypting key (dir
).
The enc
argument specifies which hashing algorithm to use for encrypting payload (A128CBC-HS256
, A256CBC-HS512
)
sample of table_of_jwt
{
"header": {"typ": "JWE", "alg": "dir", "enc":"A128CBC-HS256"},
"payload": {"foo": "bar"}
}
Verification
Both the jwt:load
and jwt:verify_jwt_obj
functions take, as additional parameters, any number of optional claim_spec
parameters. A claim_spec
is simply a lua table of claims and validators. Each key in the claim_spec
table corresponds to a matching key in the payload, and the validator
is a function that will be called to determine if the claims are met.
The signature of a validator
function is:
function(val, claim, jwt_json)
Where val
is the value of the claim from the jwt_obj
being tested (or nil if it doesn't exist in the object's payload), claim
is the name of the claim that is being verified, and jwt_json
is a json-serialized representation of the object that is being verified. If the function has no need of the claim
or jwt_json
, parameters, they may be left off.
A validator
function returns either true
or false
. Any validator
MAY raise an error, and the validation will be treated as a failure, and the error that was raised will be put into the reason field of the resulting object. If a validator
returns nothing (i.e. nil
), then the function is treated to have succeeded - under the assumption that it would have raised an error if it would have failed.
A special claim named __jwt
can be used such that if a validator
function exists for it, then the validator
will be called with a deep clone of the entire parsed jwt object as the value of val
. This is so that you can write verifications for an entire object that may depend on one or more claims.
Multiple claim_spec
tables can be specified to the jwt:load
and jwt:verify_jwt_obj
- and they will be executed in order. There is no guarantee of the execution order of individual validators
within a single claim_spec
. If a claim_spec
fails, then any following claim_specs
will NOT be executed.
sample claim_spec
{
sub = function(val) return string.match("^[a-z]+$", val) end,
iss = function(val)
for _, value in pairs({ "first", "second" }) do
if value == val then return true end
end
return false
end,
__jwt = function(val, claim, jwt_json)
if val.payload.foo == nil or val.payload.bar == nil then
error("Need to specify either 'foo' or 'bar'")
end
end
}
JWT Validators
A library of helpful validator
functions exists at resty.jwt-validators
. You can use this library by including:
local validators = require "resty.jwt-validators"
The following functions are currently defined in the validator library. Those marked with "(opt)" means that the same function exists named opt_<name>
which takes the same parameters. The "opt" version of the function will return true
if the key does not exist in the payload of the jwt_object being verified, while the "non-opt" version of the function will return false if the key does not exist in the payload of the jwt_object being verified.
validators.chain(...)
Returns a validator that chains the given functions together, one after another - as long as they keep passing their checks.
validators.required(chain_function)
Returns a validator that returns false
if a value doesn't exist. If the value exists and a chain_function
is specified, then the value of chain_function(val, claim, jwt_json)
will be returned, otherwise, true
will be returned. This allows for specifying that a value is both required and it must match some additional check.
validators.require_one_of(claim_keys)
Returns a validator which errors with a message if NONE of the given claim keys exist. It is expected that this function is used against a full jwt object. The claim_keys must be a non-empty table of strings.
validators.check(check_val, check_function, name, check_type)
(opt)
Returns a validator that checks if the result of calling the given check_function
for the tested value and check_val
returns true. The value of check_val
and check_function
cannot be nil. The optional name
is used for error messages and defaults to "check_value". The optional check_type
is used to make sure that the check type matches and defaults to type(check_val)
. The first parameter passed to check_function will never be nil. If the check_function
raises an error, that will be appended to the error message.
validators.equals(check_val)
(opt)
Returns a validator that checks if a value exactly equals (using ==
) the given check_value. The value of check_val
cannot be nil.
validators.matches(pattern)
(opt)
Returns a validator that checks if a value matches the given pattern (using string.match
). The value of pattern
must be a string.
validators.any_of(check_values, check_function, name, check_type, table_type)
(opt)
Returns a validator which calls the given check_function
for each of the given check_values
and the tested value. If any of these calls return true
, then this function returns true
. The value of check_values
must be a non-empty table with all the same types, and the value of check_function
must not be nil
. The optional name
is used for error messages and defaults to "check_values". The optional check_type
is used to make sure that the check type matches and defaults to type(check_values[1])
- the table type.
validators.equals_any_of(check_values)
(opt)
Returns a validator that checks if a value exactly equals any of the given check_values
.
validators.matches_any_of(patterns)
(opt)
Returns a validator that checks if a value matches any of the given patterns
.
validators.contains_any_of(check_values,name)
(opt)
Returns a validator that checks if a value of expected type string
exists in any of the given check_values
. The value of check_values
must be a non-empty table with all the same types. The optional name is used for error messages and defaults to check_values
.
validators.greater_than(check_val)
(opt)
Returns a validator that checks how a value compares (numerically, using >
) to a given check_value
. The value of check_val
cannot be nil
and must be a number.
validators.greater_than_or_equal(check_val)
(opt)
Returns a validator that checks how a value compares (numerically, using >=
) to a given check_value
. The value of check_val
cannot be nil
and must be a number.
validators.less_than(check_val)
(opt)
Returns a validator that checks how a value compares (numerically, using <
) to a given check_value
. The value of check_val
cannot be nil
and must be a number.
validators.less_than_or_equal(check_val)
(opt)
Returns a validator that checks how a value compares (numerically, using <=
) to a given check_value
. The value of check_val
cannot be nil
and must be a number.
validators.is_not_before()
(opt)
Returns a validator that checks if the current time is not before the tested value within the system's leeway. This means that:
val <= (system_clock() + system_leeway).
validators.is_not_expired()
(opt)
Returns a validator that checks if the current time is not equal to or after the tested value within the system's leeway. This means that:
val > (system_clock() - system_leeway).
validators.is_at()
(opt)
Returns a validator that checks if the current time is the same as the tested value within the system's leeway. This means that:
val >= (system_clock() - system_leeway) and val <= (system_clock() + system_leeway).
validators.set_system_leeway(leeway)
A function to set the leeway (in seconds) used for is_not_before
and is_not_expired
. The default is to use 0
seconds
validators.set_system_clock(clock)
A function to set the system clock used for is_not_before
and is_not_expired
. The default is to use ngx.now
sample claim_spec
using validators
local validators = require "resty.jwt-validators"
local claim_spec = {
sub = validators.opt_matches("^[a-z]+$),
iss = validators.equals_any_of({ "first", "second" }),
__jwt = validators.require_one_of({ "foo", "bar" })
}
Legacy/Timeframe options
In order to support code which used previous versions of this library, as well as to simplify specifying timeframe-based claim_specs
, you may use in place of any single claim_spec
parameter a table of validation_options
. The parameter should be expressed as a key/value table. Each key of the table should be picked from the following list.
When using legacy validation_options
, you MUST ONLY specify these options. That is, you cannot mix legacy validation_options
with other claim_spec
validators. In order to achieve that, you must specify multiple options to the jwt:load
/jwt:verify_jwt_obj
functions.
-
lifetime_grace_period
: Define the leeway in seconds to account for clock skew between the server that generated the jwt and the server validating it. Value should be zero (0
) or a positive integer.-
When this validation option is specified, the process will ensure that the jwt contains at least one of the two
nbf
orexp
claim and compare the current clock time against those boundaries. Would the jwt be deemed as expired or not valid yet, verification will fail. -
When none of the
nbf
andexp
claims can be found, verification will fail. -
nbf
andexp
claims are expected to be expressed in the jwt as numerical values. Wouldn't that be the case, verification will fail. -
Specifying this option is equivalent to calling:
validators.set_system_leeway(leeway)
and specifying as a
claim_spec
:{ __jwt = validators.require_one_of({ "nbf", "exp" }), nbf = validators.opt_is_not_before(), exp = validators.opt_is_not_expired() }
-
-
require_nbf_claim
: Express if thenbf
claim is optional or not. Value should be a boolean.-
When this validation option is set to
true
and nolifetime_grace_period
has been specified, a zero (0
) leeway is implied. -
Specifying this option is equivalent to specifying as a
claim_spec
:{ nbf = validators.is_not_before(), }
-
-
require_exp_claim
: Express if theexp
claim is optional or not. Value should be a boolean.-
When this validation option is set to
true
and nolifetime_grace_period
has been specified, a zero (0
) leeway is implied. -
Specifying this option is equivalent to specifying as a
claim_spec
:{ exp = validators.is_not_expired(), }
-
-
valid_issuers
: Whitelist the vetted issuers of the jwt. Value should be a array of strings.-
When this validation option is specified, the process will compare the jwt
iss
claim against the list of valid issuers. Comparison is done in a case sensitive manner. Would the jwt issuer not be found in the whitelist, verification will fail. -
iss
claim is expected to be expressed in the jwt as a string. Wouldn't that be the case, verification will fail. -
Specifying this option is equivalent to specifying as a
claim_spec
:{ iss = validators.equals_any_of(valid_issuers), }
-
sample of validation_options usage
local jwt_obj = jwt:verify(key, jwt_token,
{
lifetime_grace_period = 120,
require_exp_claim = true,
valid_issuers = { "my-trusted-issuer", "my-other-trusteed-issuer" }
}
)
Examples
Testing With Docker
./ci script
See Also
- the ngx_lua module: http://wiki.nginx.org/HttpLuaModule
GitHub
You may find additional configuration tips and documentation for this module in the GitHub repository for nginx-module-jwt.