diff --git a/ChangeLog b/ChangeLog index 4c764d3..ad8850f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -2,9 +2,13 @@ - if lifecyle handlers return truthy values they cause the operation they are handlers of to fail; see #384; thanks to @arcivanov -09/19/2021 -- made jwt_verify() and bearer_jwt_verify() honor - opts.introspection_cache_ignore as well. +- added opts.cache_segment as option to shard the cache used by token + introspection or JWT verification; see #399 + +09/22/2021 +- made jwt_verify() and bearer_jwt_verify() use a separate cache named + "jwt_verification" and introduced opts.jwt_verification_cache_ignore + to disable caching completely; see #399 12/05/2020 - fixed a session leak in access_token() and for a very unlikely diff --git a/README.md b/README.md index 6258e3c..b6ef300 100644 --- a/README.md +++ b/README.md @@ -299,6 +299,16 @@ local res, err, target, session = require("resty.openidc").authenticate(opts) session:close() ``` +## Caching of Introspection and JWT Verification Results + +Note the `jwt_verification` and `introspection` caches are shared +between all configured locations. If you are using locations with +different `opts` configuration the shared cache may allow a token that +is valid for only one location to be accepted by another if it is read +from the cache. In order to avoid cache confusion it is recommended to +set `opts.cache_segment` to unique strings for each set of related +locations. + ## Sample Configuration for OAuth 2.0 JWT Token Validation Sample `nginx.conf` configuration for verifying Bearer JWT Access Tokens against a pre-configured secret/key. @@ -316,7 +326,7 @@ http { resolver 8.8.8.8; # cache for JWT verification results - lua_shared_dict introspection 10m; + lua_shared_dict jwt_verification 10m; server { listen 8080; @@ -376,9 +386,13 @@ lAc5Csj0o5Q+oEhPUAVBIF07m4rd0OvAVPOCQ2NJhQSL1oWASbf+fg== -- the expiration time in seconds for jwk cache, default is 1 day. --jwk_expires_in = 24 * 60 * 60 - -- It may be necessary to force an introspection call for a bearer token and ignore the existing cached - -- introspection results. If so you need to set set the introspection_cache_ignore option to true. - -- introspection_cache_ignore = true + -- It may be necessary to force verification for a bearer token and ignore the existing cached + -- verification results. If so you need to set set the jwt_verification_cache_ignore option to true. + -- jwt_verification_cache_ignore = true + + -- optional name of a cache-segment if you need separate + -- caches for differently configured locations + -- cache_segment = 'api' } -- call bearer_jwt_verify for OAuth 2.0 JWT validation @@ -447,6 +461,10 @@ http { -- Defaults to "exp" - Controls the TTL of the introspection cache -- https://tools.ietf.org/html/rfc7662#section-2.2 -- introspection_expiry_claim = "exp" + + -- optional name of a cache-segment if you need separate + -- caches for differently configured locations + -- cache_segment = 'api' } -- call introspect for OAuth 2.0 Bearer Access Token validation @@ -547,6 +565,10 @@ http { -- It may be necessary to force an introspection call for an access_token and ignore the existing cached -- introspection results. If so you need to set set the introspection_cache_ignore option to true. -- introspection_cache_ignore = true + + -- optional name of a cache-segment if you need separate + -- caches for differently configured locations + -- cache_segment = 'api' } -- call introspect for OAuth 2.0 Bearer Access Token validation diff --git a/lib/resty/openidc.lua b/lib/resty/openidc.lua index b4dfbfd..51d1447 100644 --- a/lib/resty/openidc.lua +++ b/lib/resty/openidc.lua @@ -132,6 +132,7 @@ function openidc.invalidate_caches() openidc_cache_invalidate("discovery") openidc_cache_invalidate("jwks") openidc_cache_invalidate("introspection") + openidc_cache_invalidate("jwt_verification") end -- validate the contents of and id_token @@ -1612,6 +1613,45 @@ local function openidc_get_bearer_access_token(opts) return access_token, err end +local function get_introspection_endpoint(opts) + local introspection_endpoint = opts.introspection_endpoint + if not introspection_endpoint then + local err = openidc_ensure_discovered_data(opts) + if err then + return nil, "opts.introspection_endpoint not said and " .. err + end + local endpoint = opts.discovery and opts.discovery.introspection_endpoint + if endpoint then + return endpoint + end + end + return introspection_endpoint +end + +local function get_introspection_cache_prefix(opts) + return (opts.cache_segment and opts.cache_segment.gsub(',', '_') or 'DEFAULT') .. ',' + .. (get_introspection_endpoint(opts) or 'nil-endpoint') .. ',' + .. (opts.client_id or 'no-client_id') .. ',' + .. (opts.client_secret and 'secret' or 'no-client_secret') .. ':' +end + +local function get_cached_introspection(opts, access_token) + local introspection_cache_ignore = opts.introspection_cache_ignore or false + if not introspection_cache_ignore then + return openidc_cache_get("introspection", + get_introspection_cache_prefix(opts) .. access_token) + end +end + +local function set_cached_introspection(opts, access_token, encoded_json, ttl) + local introspection_cache_ignore = opts.introspection_cache_ignore or false + if not introspection_cache_ignore then + openidc_cache_set("introspection", + get_introspection_cache_prefix(opts) .. access_token, + encoded_json, ttl) + end +end + -- main routine for OAuth 2.0 token introspection function openidc.introspect(opts) @@ -1623,12 +1663,7 @@ function openidc.introspect(opts) -- see if we've previously cached the introspection result for this access token local json - local v - local introspection_cache_ignore = opts.introspection_cache_ignore or false - - if not introspection_cache_ignore then - v = openidc_cache_get("introspection", access_token) - end + local v = get_cached_introspection(opts, access_token) if v then json = cjson.decode(v) @@ -1655,16 +1690,10 @@ function openidc.introspect(opts) end -- call the introspection endpoint - local introspection_endpoint = opts.introspection_endpoint - if not introspection_endpoint then - err = openidc_ensure_discovered_data(opts) - if err then - return nil, "opts.introspection_endpoint not said and " .. err - end - local endpoint = opts.discovery and opts.discovery.introspection_endpoint - if endpoint then - introspection_endpoint = endpoint - end + local introspection_endpoint + introspection_endpoint, err = get_introspection_endpoint(opts) + if err then + return nil, err end json, err = openidc.call_token_endpoint(opts, introspection_endpoint, body, opts.introspection_endpoint_auth_method, "introspection") @@ -1679,10 +1708,11 @@ function openidc.introspect(opts) end -- cache the results + local introspection_cache_ignore = opts.introspection_cache_ignore or false local expiry_claim = opts.introspection_expiry_claim or "exp" - local introspection_interval = opts.introspection_interval or 0 if not introspection_cache_ignore and json[expiry_claim] then + local introspection_interval = opts.introspection_interval or 0 local ttl = json[expiry_claim] if expiry_claim == "exp" then --https://tools.ietf.org/html/rfc7662#section-2.2 ttl = ttl - ngx.time() @@ -1693,39 +1723,64 @@ function openidc.introspect(opts) end end log(DEBUG, "cache token ttl: " .. ttl) - openidc_cache_set("introspection", access_token, cjson.encode(json), ttl) - + set_cached_introspection(opts, access_token, cjson.encode(json), ttl) end return json, err end +local function get_jwt_verification_cache_prefix(opts) + local signing_alg_values_expected = (opts.accept_none_alg and 'none' or 'no-none') + local expected_algs = opts.token_signing_alg_values_expected or {} + if type(expected_algs) == 'string' then + expected_algs = { expected_algs } + end + for _, alg in ipairs(expected_algs) do + signing_alg_values_expected = signing_alg_values_expected .. ',' .. alg + end + return (opts.cache_segment and opts.cache_segment.gsub(',', '_') or 'DEFAULT') .. ',' + .. (opts.public_key or 'no-pubkey') .. ',' + .. (opts.symmetric_key or 'no-symkey') .. ',' + .. signing_alg_values_expected .. ':' +end + +local function get_cached_jwt_verification(opts, access_token) + local jwt_verification_cache_ignore = opts.jwt_verification_cache_ignore or false + if not jwt_verification_cache_ignore then + return openidc_cache_get("jwt_verification", + get_jwt_verification_cache_prefix(opts) .. access_token) + end +end + +local function set_cached_jwt_verification(opts, access_token, encoded_json, ttl) + local jwt_verification_cache_ignore = opts.jwt_verification_cache_ignore or false + if not jwt_verification_cache_ignore then + openidc_cache_set("jwt_verification", + get_jwt_verification_cache_prefix(opts) .. access_token, + encoded_json, ttl) + end +end + -- main routine for OAuth 2.0 JWT token validation -- optional args are claim specs, see jwt-validators in resty.jwt function openidc.jwt_verify(access_token, opts, ...) local err local json - local v - local introspection_cache_ignore = opts.introspection_cache_ignore or false + local v = get_cached_jwt_verification(opts, access_token) local slack = opts.iat_slack and opts.iat_slack or 120 - -- see if we've previously cached the validation result for this access token - if not introspection_cache_ignore then - v = openidc_cache_get("introspection", access_token) - end if not v then local jwt_obj jwt_obj, err = openidc_load_jwt_and_verify_crypto(opts, access_token, opts.public_key, opts.symmetric_key, opts.token_signing_alg_values_expected, ...) if not err then json = jwt_obj.payload - log(DEBUG, "jwt: ", cjson.encode(json)) + local encoded_json = cjson.encode(json) + log(DEBUG, "jwt: ", encoded_json) - if not introspection_cache_ignore then - local ttl = json.exp and json.exp - ngx.time() or 120 - openidc_cache_set("introspection", access_token, cjson.encode(json), ttl) - end + set_cached_jwt_verification(opts, access_token, encoded_json, + json.exp and json.exp - ngx.time() or 120) end else