diff --git a/README.md b/README.md index 3bdf96e..016e227 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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: ` 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)` diff --git a/lib/resty/jwt.lua b/lib/resty/jwt.lua index 4c6a3df..a4d2067 100644 --- a/lib/resty/jwt.lua +++ b/lib/resty/jwt.lua @@ -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) @@ -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 diff --git a/t/sign-verify.t b/t/sign-verify.t index 2610972..386624f 100644 --- a/t/sign-verify.t +++ b/t/sign-verify.t @@ -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] \ No newline at end of file