Skip to content

Commit

Permalink
Merge pull request #400 from zmartzone/harden-introspection-cache
Browse files Browse the repository at this point in the history
try to avoid cache confusion
  • Loading branch information
bodewig authored Sep 24, 2021
2 parents 6ba3a5f + 1e1e66f commit 5c8cb34
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 37 deletions.
10 changes: 7 additions & 3 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
115 changes: 85 additions & 30 deletions lib/resty/openidc.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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)
Expand All @@ -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")

Expand All @@ -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()
Expand All @@ -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
Expand Down

0 comments on commit 5c8cb34

Please sign in to comment.