diff --git a/ChangeLog b/ChangeLog index f3b13744..7cec6b2a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,9 @@ +09/11/2026 +- coalesce concurrent introspection cache misses for the same token, allowing + waiting requests to share successful and failed results +- make introspection coordination honor explicit cache bypass, treat locks as + bounded leases, and fall back to direct introspection on coordination failure + 09/05/2026 - propagate session destroy and closed-mode revocation-store failures from logout, session initialization, and access-token retrieval; see #571; diff --git a/README.md b/README.md index 982b60af..8b19fbb5 100644 --- a/README.md +++ b/README.md @@ -423,7 +423,14 @@ Currently up to four caches are used introspection. Cache items expire when the corresponding token expires. Tokens with unknown expiry are not cached at all. This cache will contain one entry per introspected access token - usually - this will be a few kB per token. + this will be a few kB per token. Concurrent cache misses for the same + token and cache segment are coalesced within an NGINX instance. One + request calls the introspection endpoint while the others wait for and + share its result, including endpoint failures and responses without an + expiry claim. If coordination times out or fails, the request rechecks the + cache and then falls back to direct introspection, preserving the behavior + without coordination. Setting `introspection_cache_ignore` disables both + caching and request coalescing. * the cache named `jwt_verification` stores the result of JWT verification. Cache items expire when the corresponding token expires. Tokens with unknown expiry are not cached for two @@ -689,6 +696,17 @@ http { -- see introspection_expiry_claim) hint as returned by the Authorization Server -- introspection_interval = 60, + -- Maximum time in seconds that a concurrent introspection waits + -- for the request which owns the per-token lock. Defaults to 5. + -- On timeout, the request falls back to direct introspection. + -- introspection_lock_timeout = 5, + + -- Lease duration in seconds for the per-token lock. This must + -- exceed the maximum introspection request duration; an owner + -- continuing after its lease expires is not supported. Defaults + -- to 30. + -- introspection_lock_exptime = 30, + -- Defines the way in which bearer OAuth 2.0 access tokens can be passed to this Resource Server. -- "cookie" as a cookie header called "PA.global" or using the name specified after ":" -- "header" "Authorization: bearer" header diff --git a/lib/resty/openidc.lua b/lib/resty/openidc.lua index 1b691cbc..628a717d 100644 --- a/lib/resty/openidc.lua +++ b/lib/resty/openidc.lua @@ -2326,11 +2326,15 @@ local function get_introspection_cache_prefix(opts) .. (opts.client_secret and 'secret' or 'no-client_secret') .. ':' end +local function get_introspection_cache_key(opts, access_token) + return get_introspection_cache_prefix(opts) .. access_token +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) + get_introspection_cache_key(opts, access_token)) end end @@ -2338,34 +2342,12 @@ 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, + get_introspection_cache_key(opts, access_token), encoded_json, ttl) end end --- main routine for OAuth 2.0 token introspection -function openidc.introspect(opts) - - -- get the access token from the request - local access_token, err = openidc_get_bearer_access_token(opts) - if access_token == nil then - return nil, err - end - - -- see if we've previously cached the introspection result for this access token - local json - local v = get_cached_introspection(opts, access_token) - - if v then - json = cjson.decode(v) - - if not json or not json.active then - err = "invalid cached token" - end - - return json, err - end - +local function introspect_access_token(opts, access_token) -- assemble the parameters to the introspection (token) endpoint local token_param_name = opts.introspection_token_param_name and opts.introspection_token_param_name or "token" @@ -2396,10 +2378,12 @@ function openidc.introspect(opts) -- call the introspection endpoint local introspection_endpoint + local err introspection_endpoint, err = get_introspection_endpoint(opts) if err then return nil, err end + local json json, err = openidc.call_token_endpoint(opts, introspection_endpoint, body, opts.introspection_endpoint_auth_method, "introspection") if not json then @@ -2438,6 +2422,216 @@ function openidc.introspect(opts) end return json, err +end + +local function decode_cached_introspection(value) + local json = cjson.decode(value) + local err + + if not json or not json.active then + err = "invalid cached token" + end + + return json, err +end + +local function get_introspection_coordination_result(dict, cache_key, + result_key_prefix, observed_owner) + local cached_value = dict:get(cache_key) + if cached_value then + return "cached", cached_value + end + + local completed_value = observed_owner and + dict:get(result_key_prefix .. observed_owner) + if completed_value then + return "completed", completed_value + end +end + +local function acquire_introspection_lock(dict, key, owner, timeout, exptime, + cache_key, result_key_prefix) + local ok, err = dict:add(key, owner, exptime) + if ok then + return "locked" + end + if err ~= "exists" then + return nil, err + end + + local observed_owner = dict:get(key) + local state, value = get_introspection_coordination_result( + dict, cache_key, result_key_prefix, observed_owner) + if state then + return state, value + end + + local elapsed = 0 + local step = 0.001 + while elapsed < timeout do + step = math.min(step, timeout - elapsed) + ngx.sleep(step) + elapsed = elapsed + step + + -- The owner publishes before releasing its lock. Check its generation + -- after sleeping and before attempting to become a new owner. + state, value = get_introspection_coordination_result( + dict, cache_key, result_key_prefix, observed_owner) + if state then + return state, value + end + + ok, err = dict:add(key, owner, exptime) + if ok then + return "locked", nil, observed_owner + end + if err ~= "exists" then + return nil, err + end + -- add() and get() are not atomic. If the first owner disappeared before + -- it could be observed, remember the first later generation we can see. + observed_owner = observed_owner or dict:get(key) + + step = math.min(step * 2, 0.5) + end + + state, value = get_introspection_coordination_result( + dict, cache_key, result_key_prefix, observed_owner) + if state then + return state, value + end + + return nil, "timeout" +end + +-- ngx.shared.DICT has no atomic compare-and-delete operation, so this owner +-- check only narrows the race window. The lock remains a lease: the owner must +-- normally finish and release it before exptime. +local function release_introspection_lock(dict, key, owner) + if dict:get(key) == owner then + dict:delete(key) + end +end + +local function publish_introspection_result(dict, key, value, ttl) + local ok, err = dict:set(key, value, ttl) + if not ok then + log(WARN, "failed to publish introspection result: " .. err) + end +end + +-- main routine for OAuth 2.0 token introspection +function openidc.introspect(opts) + + -- get the access token from the request + local access_token, err = openidc_get_bearer_access_token(opts) + if access_token == nil then + return nil, err + end + + if opts.introspection_cache_ignore then + return introspect_access_token(opts, access_token) + end + + -- avoid lock overhead for normal cache hits + local value = get_cached_introspection(opts, access_token) + if value then + return decode_cached_introspection(value) + end + + -- Without the shared cache there is nowhere to coordinate workers. Preserve + -- the existing uncached behavior for configurations that omit the dictionary. + local introspection_cache = ngx.shared.introspection + if not introspection_cache then + return introspect_access_token(opts, access_token) + end + + local cache_key = get_introspection_cache_key(opts, access_token) + local digest = b64url(openidc_sha256(cache_key)) + local lock_key = "openidc-introspection-lock:" .. digest + local result_key_prefix = "openidc-introspection-result:" .. digest .. ":" + local lock_owner = openidc_random_jti() + local lock_timeout = opts.introspection_lock_timeout or 5 + local lock_exptime = opts.introspection_lock_exptime or 30 + + if type(lock_timeout) ~= "number" or lock_timeout < 0 then + return nil, "introspection_lock_timeout must be a non-negative number" + end + if type(lock_exptime) ~= "number" or lock_exptime <= 0 then + return nil, "introspection_lock_exptime must be a positive number" + end + + local lock_state + local coordination_value + local observed_owner + lock_state, coordination_value, observed_owner = acquire_introspection_lock( + introspection_cache, lock_key, lock_owner, lock_timeout, lock_exptime, + cache_key, result_key_prefix) + if lock_state == "cached" then + return decode_cached_introspection(coordination_value) + end + if lock_state == "completed" then + local completed = cjson_s.decode(coordination_value) + if completed then + return completed.json, completed.err + end + log(WARN, "failed to decode published introspection result; " .. + "falling back to direct introspection") + return introspect_access_token(opts, access_token) + end + if lock_state ~= "locked" then + err = coordination_value + -- Coordination is an optimization. Preserve the previous behavior when + -- the lock is unavailable, while avoiding a duplicate call if the owner + -- populated the cache immediately before the timeout. + value = get_cached_introspection(opts, access_token) + if value then + return decode_cached_introspection(value) + end + log(WARN, "failed to acquire introspection lock: ", err, + "; falling back to direct introspection") + return introspect_access_token(opts, access_token) + end + + -- Another request may have populated the regular cache while this request + -- waited for the lock. + value = get_cached_introspection(opts, access_token) + if value then + release_introspection_lock(introspection_cache, lock_key, lock_owner) + return decode_cached_introspection(value) + end + + -- Responses which cannot enter the regular cache (including endpoint + -- failures) are published briefly under the lock generation. Only requests + -- which observed that generation while waiting can share the outcome. + local completed_value = observed_owner and + introspection_cache:get(result_key_prefix .. observed_owner) + if completed_value and observed_owner then + local completed = cjson_s.decode(completed_value) + if completed then + release_introspection_lock(introspection_cache, lock_key, lock_owner) + return completed.json, completed.err + end + end + + local json + json, err = introspect_access_token(opts, access_token) + + -- A cacheable response is already visible to waiters. Publish only outcomes + -- that the regular introspection cache did not retain. + if not introspection_cache:get(cache_key) then + local completed, encode_err = cjson_s.encode({ json = json, err = err }) + if completed then + publish_introspection_result( + introspection_cache, result_key_prefix .. lock_owner, + completed, math.max(lock_timeout, 1)) + else + log(WARN, "failed to encode introspection result: " .. encode_err) + end + end + + release_introspection_lock(introspection_cache, lock_key, lock_owner) + return json, err end diff --git a/tests/spec/introspection_spec.lua b/tests/spec/introspection_spec.lua index 695bde6e..812ede47 100644 --- a/tests/spec/introspection_spec.lua +++ b/tests/spec/introspection_spec.lua @@ -26,6 +26,75 @@ local function error_log_occurrences(s) end end +local function count_introspection_client_workers() + local workers = {} + local log = test_support.load("/tmp/server/logs/error.log") + for pid in log:gmatch("Introspection client worker: (%d+)") do + workers[pid] = true + end + + local count = 0 + for _ in pairs(workers) do + count = count + 1 + end + return count +end + +local function count_statuses(statuses) + local seen = 0 + local counts = {} + for status in statuses:gmatch("%d+") do + counts[status] = (counts[status] or 0) + 1 + seen = seen + 1 + end + return counts, seen +end + +local function collect_introspection_statuses_concurrently(jwt, count) + local output = "/tmp/introspection-statuses" + os.remove(output) + local command = "seq 1 " .. count .. " | xargs -P " .. count .. + " -I '{}' curl -sS -o /dev/null -w '%{http_code}\\n'" .. + " -H 'Authorization: Bearer " .. jwt .. "'" .. + " http://127.0.0.1/introspect > " .. output + local ok = os.execute(command) + assert.truthy(ok == true or ok == 0) + + local counts, seen = count_statuses(test_support.load(output)) + assert.are.equals(count, seen) + return counts +end + +local function collect_introspection_statuses_with_eviction(jwt, count) + local owner_output = "/tmp/introspection-owner-status" + local waiters_output = "/tmp/introspection-waiter-statuses" + os.remove(owner_output) + os.remove(waiters_output) + local request = "curl -sS -o /dev/null -w '%{http_code}\\n'" .. + " -H 'Authorization: Bearer " .. jwt .. "'" .. + " http://127.0.0.1/introspect" + local command = request .. " > " .. owner_output .. " & owner_pid=$!;" .. + " sleep 0.1;" .. + " curl -fsS http://127.0.0.1/evict-introspection-coordination > /dev/null &&" .. + " seq 1 " .. (count - 1) .. " | xargs -P " .. (count - 1) .. + " -I '{}' " .. request .. " > " .. waiters_output .. ";" .. + " wait $owner_pid" + local ok = os.execute(command) + assert.truthy(ok == true or ok == 0) + + local statuses = test_support.load(owner_output) .. + test_support.load(waiters_output) + local counts, seen = count_statuses(statuses) + assert.are.equals(count, seen) + return counts +end + +local function request_introspection_concurrently(jwt, count, expected_status) + expected_status = expected_status or "200" + local counts = collect_introspection_statuses_concurrently(jwt, count) + assert.are.equals(count, counts[expected_status]) +end + local legacy_introspection_body_auth_warning = "introspection_endpoint_auth_method is not set; " .. "sending introspection client credentials in the POST body is deprecated" @@ -467,7 +536,174 @@ describe("when the response is active but lacks the exp claim", function() end) end) --- TODO find a way to assert caching +describe("when a batch sends 35 concurrent requests with the same uncached token", function() + test_support.start_server({ + -- Keep the first lookup in flight long enough for the whole batch to + -- arrive inside the same cache-miss window. + delay_response = { introspection = 1000 }, + introspection_opts = { introspection_cache_ignore = false }, + }) + teardown(test_support.stop_server) + local jwt = test_support.trim(http.request("http://127.0.0.1/jwt")) + request_introspection_concurrently(jwt, 35) + + it("coalesces the batch into one introspection endpoint call", function() + assert.are.equals(1, error_log_occurrences("Received introspection request:")) + end) +end) + +describe("when concurrent requests explicitly bypass the introspection cache", function() + test_support.start_server({ + delay_response = { introspection = 300 }, + introspection_opts = { introspection_cache_ignore = true }, + }) + teardown(test_support.stop_server) + local jwt = test_support.trim(http.request("http://127.0.0.1/jwt")) + request_introspection_concurrently(jwt, 10) + + it("introspects every request independently", function() + assert.are.equals(10, error_log_occurrences("Received introspection request:")) + end) +end) + +describe("when concurrent requests introspect a response without an expiry", function() + test_support.start_server({ + delay_response = { introspection = 300 }, + remove_introspection_claims = { "exp" }, + introspection_opts = { introspection_cache_ignore = false }, + }) + teardown(test_support.stop_server) + local jwt = test_support.trim(http.request("http://127.0.0.1/jwt")) + request_introspection_concurrently(jwt, 20) + + it("shares the in-flight response without caching it for later requests", function() + assert.are.equals(1, error_log_occurrences("Received introspection request:")) + os.execute("sleep 0.1") + local _, status = http.request({ + url = "http://127.0.0.1/introspect", + headers = { authorization = "Bearer " .. jwt } + }) + assert.are.equals(200, status) + assert.are.equals(2, error_log_occurrences("Received introspection request:")) + end) +end) + +describe("when a batch receives an introspection endpoint failure", function() + test_support.start_server({ + delay_response = { introspection = 1000 }, + introspection_response_status = 503, + introspection_opts = { introspection_cache_ignore = false }, + }) + teardown(test_support.stop_server) + local jwt = test_support.trim(http.request("http://127.0.0.1/jwt")) + request_introspection_concurrently(jwt, 35, "401") + + it("coalesces the batch into one introspection endpoint call", function() + assert.are.equals(1, error_log_occurrences("Received introspection request:")) + end) + it("shares the endpoint failure with every request", function() + assert.are.equals(35, error_log_occurrences( + "Introspection error: response indicates failure, status=503,")) + end) +end) + +describe("when concurrent introspection lock acquisition times out", function() + test_support.start_server({ + delay_response = { introspection = 1000 }, + introspection_opts = { + introspection_cache_ignore = false, + introspection_lock_timeout = 0, + }, + }) + teardown(test_support.stop_server) + local jwt = test_support.trim(http.request("http://127.0.0.1/jwt")) + request_introspection_concurrently(jwt, 2) + + it("falls back to direct introspection", function() + assert.are.equals(2, error_log_occurrences("Received introspection request:")) + end) + it("logs the coordination failure", function() + assert.error_log_contains( + "failed to acquire introspection lock: timeout; falling back to direct introspection") + end) +end) + +describe("when an introspection lock lease expires across workers", function() + test_support.start_server({ + worker_processes = 4, + log_introspection_worker = true, + delay_response = { introspection = 300 }, + introspection_opts = { + introspection_cache_ignore = false, + introspection_lock_timeout = 2, + introspection_lock_exptime = 0.05, + }, + }) + teardown(test_support.stop_server) + local jwt = test_support.trim(http.request("http://127.0.0.1/jwt")) + local counts = collect_introspection_statuses_concurrently(jwt, 20) + + it("does not turn lease expiry into authentication failures", function() + assert.are.equals(20, counts["200"]) + end) + it("allows duplicate upstream calls after the lease expires", function() + assert.is_true(error_log_occurrences("Received introspection request:") > 1) + end) + it("exercises more than one nginx worker", function() + assert.is_true(count_introspection_client_workers() > 1) + end) +end) + +describe("when introspection coordination state is evicted across workers", function() + test_support.start_server({ + worker_processes = 4, + log_introspection_worker = true, + introspection_shared_dict_size = "64k", + delay_response = { introspection = 500 }, + introspection_opts = { + introspection_cache_ignore = false, + introspection_lock_timeout = 2, + }, + }) + teardown(test_support.stop_server) + local jwt = test_support.trim(http.request("http://127.0.0.1/jwt")) + local counts = collect_introspection_statuses_with_eviction(jwt, 16) + + it("does not turn coordination eviction into authentication failures", function() + assert.are.equals(16, counts["200"]) + end) + it("allows duplicate upstream calls after coordination is evicted", function() + assert.is_true(error_log_occurrences("Received introspection request:") > 1) + end) + it("exercises more than one nginx worker", function() + assert.is_true(count_introspection_client_workers() > 1) + end) +end) + +describe("when sequential non-cacheable requests have the same ngx.now value", function() + test_support.start_server({ + fixed_ngx_now = 1000, + remove_introspection_claims = { "exp" }, + introspection_opts = { introspection_cache_ignore = false }, + }) + teardown(test_support.stop_server) + local jwt = test_support.trim(http.request("http://127.0.0.1/jwt")) + local headers = { authorization = "Bearer " .. jwt } + local _, first_status = http.request({ + url = "http://127.0.0.1/introspect", + headers = headers, + }) + local _, second_status = http.request({ + url = "http://127.0.0.1/introspect", + headers = headers, + }) + + it("does not share the first request's temporary result", function() + assert.are.equals(200, first_status) + assert.are.equals(200, second_status) + assert.are.equals(2, error_log_occurrences("Received introspection request:")) + end) +end) describe("when introspection endpoint is not resolvable", function() test_support.start_server({ @@ -661,4 +897,3 @@ describe("when introspection endpoint hasn't been specified but discovery doc pr assert.are.equals(200, status) end) end) - diff --git a/tests/spec/test_support.lua b/tests/spec/test_support.lua index 2250a157..d2738441 100644 --- a/tests/spec/test_support.lua +++ b/tests/spec/test_support.lua @@ -74,6 +74,8 @@ local DEFAULT_INTROSPECTION_OPTS = { introspection_endpoint = "http://127.0.0.1/introspection", client_id = "client_id", client_secret = "client_secret", + -- Most specs inspect the outgoing request, so caching is opt-in in tests. + introspection_cache_ignore = true, } local DEFAULT_TOKEN_RESPONSE_EXPIRES_IN = "3600" @@ -94,6 +96,7 @@ local DEFAULT_UNAUTH_ACTION = "nil" local DEFAULT_SHARE_OIDC_OPTS = "false" local DEFAULT_DELAY_RESPONSE = "0" +local DEFAULT_INTROSPECTION_RESPONSE_STATUS = "200" local DEFAULT_REVOCATION_TEST_ENABLED = "false" local DEFAULT_REVOCATION_FAIL_MODE = '"closed"' @@ -122,6 +125,12 @@ if FIXED_NGX_TIME ~= nil then return fixed_ngx_time end end +if FIXED_NGX_NOW ~= nil then + local fixed_ngx_now = FIXED_NGX_NOW + ngx.now = function() + return fixed_ngx_now + end +end test_globals.delay = function(delay_response) if delay_response > 0 then ngx.sleep(delay_response / 1000) @@ -185,7 +194,7 @@ return test_globals ]] local DEFAULT_CONFIG_TEMPLATE = [[ -worker_processes 1; +worker_processes WORKER_PROCESSES; pid /tmp/server/logs/nginx.pid; error_log /tmp/server/logs/error.log debug; @@ -197,6 +206,7 @@ http { access_log /tmp/server/logs/access.log; lua_package_path '~/lua/?.lua;/tmp/server/conf/?.lua;;'; lua_shared_dict discovery 1m; + lua_shared_dict introspection INTROSPECTION_SHARED_DICT_SIZE; lua_shared_dict jwt_verification 1m; lua_shared_dict revocation_test 1m; init_by_lua_block { @@ -527,12 +537,16 @@ http { end ngx.header.content_type = 'application/json;charset=UTF-8' test_globals.delay(INTROSPECTION_DELAY_RESPONSE) + ngx.status = INTROSPECTION_RESPONSE_STATUS ngx.say(test_globals.cjson.encode(INTROSPECTION_RESPONSE)) } } location /introspect { content_by_lua_block { + if LOG_INTROSPECTION_WORKER then + ngx.log(ngx.ERR, "Introspection client worker: " .. ngx.worker.pid()) + end local opts = INTROSPECTION_OPTS if opts.decorate then opts.http_request_decorator = test_globals.body_decorator @@ -548,6 +562,39 @@ http { } } + location /evict-introspection-coordination { + content_by_lua_block { + local dict = ngx.shared.introspection + local filler = string.rep("x", 1024) + local forced = false + for i = 1, 4096 do + local ok, err, forcible = dict:set( + "introspection-test-filler:" .. i, filler) + if not ok then + ngx.status = 500 + ngx.say("failed to fill introspection dictionary: " .. err) + return + end + forced = forced or forcible + + local has_coordination_state = false + for _, key in ipairs(dict:get_keys(0)) do + if key:find("^openidc%-introspection%-lock:") or + key:find("^openidc%-introspection%-result:") then + has_coordination_state = true + break + end + end + if forced and not has_coordination_state then + ngx.say("evicted") + return + end + end + ngx.status = 500 + ngx.say("coordination state was not evicted") + } + } + location /access_token { content_by_lua_block { local access_token, err = test_globals.oidc.access_token(ACCESS_TOKEN_OPTS, test_globals.session_opts) @@ -657,10 +704,17 @@ local function write_template(out, template, custom_config) introspection_opts[k] = nil end local content = template + :gsub("WORKER_PROCESSES", tostring(custom_config["worker_processes"] or 1)) + :gsub("INTROSPECTION_SHARED_DICT_SIZE", + custom_config["introspection_shared_dict_size"] or "1m") + :gsub("LOG_INTROSPECTION_WORKER", + custom_config["log_introspection_worker"] and "true" or "false") :gsub("OIDC_CONFIG", serpent.block(oidc_config, {comment = false })) :gsub("TOKEN_HEADER", serpent.block(token_header, {comment = false })) :gsub("JWT_SIGN_SECRET", custom_config["jwt_sign_secret"] or DEFAULT_JWT_SIGN_SECRET) :gsub("VERIFY_OPTS", serpent.block(verify_opts, {comment = false })) + :gsub("INTROSPECTION_RESPONSE_STATUS", tostring(custom_config["introspection_response_status"] or + DEFAULT_INTROSPECTION_RESPONSE_STATUS)) :gsub("INTROSPECTION_RESPONSE", serpent.block(introspection_response, {comment = false })) :gsub("INTROSPECTION_OPTS", serpent.block(introspection_opts, {comment = false })) :gsub("TOKEN_RESPONSE_EXPIRES_IN", token_response_expires_in) @@ -688,6 +742,7 @@ local function write_template(out, template, custom_config) :gsub("REFRESH_ID_TOKEN", serpent.block(refresh_id_token, {comment = false })) :gsub("ID_TOKEN", serpent.block(id_token, {comment = false })) :gsub("ACCESS_TOKEN", serpent.block(access_token, {comment = false })) + :gsub("FIXED_NGX_NOW", custom_config["fixed_ngx_now"] or "nil") :gsub("FIXED_NGX_TIME", custom_config["fixed_ngx_time"] or "nil") :gsub("UNAUTH_ACTION", custom_config["unauth_action"] and ('"' .. custom_config["unauth_action"] .. '"') or DEFAULT_UNAUTH_ACTION) :gsub("SHARE_OIDC_OPTS", custom_config["share_oidc_opts"] and "true" or DEFAULT_SHARE_OIDC_OPTS) @@ -733,6 +788,10 @@ end -- - access_token_opts is a table containing options that are accepted by oidc.access_token -- - delay_response is a table specifying a delay for the response of various endpoint in ms -- { jwk = 1, token = 1, discovery = 1, userinfo = 1, introspection = 1} +-- - worker_processes configures the number of nginx workers; defaults to 1 +-- - introspection_shared_dict_size configures the introspection shared dictionary size; +-- defaults to 1m +-- - log_introspection_worker logs the worker handling each /introspect request -- - refreshing_token_fails whether to grant an access token via the refresh token grant -- - fake_access_token_signature whether to fake a JWT signature with unknown algorithm for the -- JWT returned by /jwt