Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ lua-resty-jwt - [JWT](http://self-issued.info/docs/draft-jones-json-web-token-01
* [verify](#verify)
* [load and verify](#load--verify)
* [set_alg_whitelist](#set_alg_whitelist)
* [set_typ_whitelist](#set_typ_whitelist)
* [set_trusted_certs_file](#set_trusted_certs_file)
* [sign JWE](#sign-jwe)
* [Verification](#verification)
Expand Down Expand Up @@ -184,6 +185,32 @@ local jwt_obj = jwt:verify(public_key, jwt_token)

Pass `nil` to clear the whitelist and allow all algorithms again.

## set_typ_whitelist

`syntax: jwt:set_typ_whitelist(typs)`

`sign` validates the `typ` header value you supply against this whitelist *before* performing any signing or encryption. If the value isn't whitelisted, `sign` raises `invalid typ: <value>` and produces no token. Pass a table whose keys are the allowed typ values. Tokens that don't set a `typ` header are unaffected.

The default whitelist accepts `JWT` (RFC 7519), `JWE` (RFC 7516), and the RFC-registered `+jwt` structured-syntax values:

- `at+jwt` — RFC 9068 (JWT Profile for OAuth 2.0 Access Tokens)
- `dpop+jwt` — RFC 9449 (Demonstrating Proof of Possession)
- `token-introspection+jwt` — RFC 9701 (JWT Response for OAuth Token Introspection)
- `client-authentication+jwt` — draft-ietf-oauth-rfc7523bis
- `secevent+jwt` — RFC 8417 (Security Event Token)
- `logout+jwt` — OpenID Connect Back-Channel Logout 1.0

```lua
local jwt = require "resty.jwt"

-- Allow only JWT and a custom value
jwt:set_typ_whitelist({ JWT = 1, ["my-custom+jwt"] = 1 })
```

Pass `nil` to disable typ validation entirely — any value (or no value) is then accepted. Pass `{}` (an empty table) to reject every typ value, including `JWT`/`JWE`.

Note: this whitelist is consulted only during `sign`. The `verify`/`load` path does not validate `header.typ`. If you need to enforce a specific typ on incoming tokens (e.g. `at+jwt` per RFC 9068), attach a `__jwt` validator that inspects the parsed header — see [Verification](#verification) for details.

## set_trusted_certs_file

`syntax: jwt:set_trusted_certs_file(filename)`
Expand Down
35 changes: 33 additions & 2 deletions lib/resty/jwt.lua
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,37 @@ end
_M.alg_whitelist = nil


--- Set a whitelist of allowed "typ" header values
-- E.g., jwt:set_typ_whitelist({JWT=1, ["at+jwt"]=1})
--
-- @param typs - A table with keys for the supported typ values.
-- If the table is non-nil, during sign the "typ"
-- header (when present) must be a key in the table.
-- Pass nil to disable typ validation entirely.
function _M.set_typ_whitelist(self, typs)
self.typ_whitelist = typs
end

-- Default allowlist: JWT (RFC 7519), JWE (RFC 7516), and the RFC-registered
-- "+jwt" structured-syntax values:
-- at+jwt RFC 9068
-- dpop+jwt RFC 9449
-- token-introspection+jwt RFC 9701
-- client-authentication+jwt draft-ietf-oauth-rfc7523bis
-- secevent+jwt RFC 8417
-- logout+jwt OpenID Connect Back-Channel Logout 1.0
_M.typ_whitelist = {
[str_const.JWT] = 1,
[str_const.JWE] = 1,
["at+jwt"] = 1,
["dpop+jwt"] = 1,
["token-introspection+jwt"] = 1,
["client-authentication+jwt"] = 1,
["secevent+jwt"] = 1,
["logout+jwt"] = 1,
}


--- Returns the list of default validations that will be
--- applied upon the verification of a jwt.
function _M.get_default_validation_options(self, jwt_obj)
Expand Down Expand Up @@ -976,8 +1007,8 @@ function _M.sign(self, secret_key, jwt_obj)
-- header typ check
local typ = jwt_obj[str_const.header][str_const.typ]
-- Optional header typ check [See http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-5.1]
if typ ~= nil then
if typ ~= str_const.JWT and typ ~= str_const.JWE then
if typ ~= nil and self.typ_whitelist ~= nil then
if not self.typ_whitelist[typ] then
error({reason="invalid typ: " .. typ})
end
end
Expand Down
164 changes: 164 additions & 0 deletions t/sign-verify.t
Original file line number Diff line number Diff line change
Expand Up @@ -873,4 +873,168 @@ true
everything is awesome~ :p
bar
--- no_error_log
[error]



=== TEST 25: Default typ whitelist accepts at+jwt (RFC 9068)
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua '
local jwt = require "resty.jwt"
local jwt_token = jwt:sign(
"lua-resty-jwt",
{
header={typ="at+jwt", alg="HS256"},
payload={foo="bar"}
}
)
local jwt_obj = jwt:verify("lua-resty-jwt", jwt_token)
ngx.say(jwt_obj["verified"])
ngx.say(jwt_obj["header"]["typ"])
';
}
--- request
GET /t
--- response_body
true
at+jwt
--- no_error_log
[error]



=== TEST 26: Default typ whitelist accepts dpop+jwt (RFC 9449)
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua '
local jwt = require "resty.jwt"
local jwt_token = jwt:sign(
"lua-resty-jwt",
{
header={typ="dpop+jwt", alg="HS256"},
payload={foo="bar"}
}
)
local jwt_obj = jwt:verify("lua-resty-jwt", jwt_token)
ngx.say(jwt_obj["verified"])
ngx.say(jwt_obj["header"]["typ"])
';
}
--- request
GET /t
--- response_body
true
dpop+jwt
--- no_error_log
[error]



=== TEST 27: Default typ whitelist rejects unknown typ
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua '
local jwt = require "resty.jwt"
local success, err = pcall(function () jwt:sign(
"lua-resty-jwt",
{
header={typ="custom", alg="HS256"},
payload={foo="bar"}
}
) end)
ngx.say(err.reason)
';
}
--- request
GET /t
--- response_body
invalid typ: custom
--- no_error_log
[error]



=== TEST 28: set_typ_whitelist replaces defaults
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua '
local jwt = require "resty.jwt"
jwt:set_typ_whitelist({["at+jwt"]=1})

local ok1, err = pcall(function () jwt:sign(
"lua-resty-jwt",
{ header={typ="JWT", alg="HS256"}, payload={foo="bar"} }
) end)
ngx.say(err.reason)

local jwt_token = jwt:sign(
"lua-resty-jwt",
{ header={typ="at+jwt", alg="HS256"}, payload={foo="bar"} }
)
local jwt_obj = jwt:verify("lua-resty-jwt", jwt_token)
ngx.say(jwt_obj["verified"])
';
}
--- request
GET /t
--- response_body
invalid typ: JWT
true
--- no_error_log
[error]



=== TEST 29: set_typ_whitelist(nil) disables typ validation
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua '
local jwt = require "resty.jwt"
jwt:set_typ_whitelist(nil)

local jwt_token = jwt:sign(
"lua-resty-jwt",
{ header={typ="anything-goes", alg="HS256"}, payload={foo="bar"} }
)
local jwt_obj = jwt:verify("lua-resty-jwt", jwt_token)
ngx.say(jwt_obj["verified"])
ngx.say(jwt_obj["header"]["typ"])
';
}
--- request
GET /t
--- response_body
true
anything-goes
--- no_error_log
[error]



=== TEST 30: set_typ_whitelist({}) rejects every typ value
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua '
local jwt = require "resty.jwt"
jwt:set_typ_whitelist({})

local ok, err = pcall(function () jwt:sign(
"lua-resty-jwt",
{ header={typ="JWT", alg="HS256"}, payload={foo="bar"} }
) end)
ngx.say(err.reason)
';
}
--- request
GET /t
--- response_body
invalid typ: JWT
--- no_error_log
[error]