From 67df073ed4450a0378e5019f723adc619bf0f503 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 5 Jan 2021 03:14:37 +0100 Subject: [PATCH] feat: Make headers to add to request in openid-connect plugin configurable. (#2903) --- apisix/plugins/openid-connect.lua | 208 +++++-- doc/plugins/openid-connect.md | 110 +++- t/plugin/openid-connect.t | 926 +++++++++++++++++++++++++++++- 3 files changed, 1164 insertions(+), 80 deletions(-) diff --git a/apisix/plugins/openid-connect.lua b/apisix/plugins/openid-connect.lua index e70a9d2ac229..1c8d78c1c3ba 100644 --- a/apisix/plugins/openid-connect.lua +++ b/apisix/plugins/openid-connect.lua @@ -14,6 +14,7 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -- +local string = string local core = require("apisix.core") local ngx_re = require("ngx.re") local openidc = require("resty.openidc") @@ -67,7 +68,31 @@ local schema = { description = "use ngx.var.request_uri if not configured" }, public_key = {type = "string"}, - token_signing_alg_values_expected = {type = "string"} + token_signing_alg_values_expected = {type = "string"}, + set_access_token_header = { + description = "Whether the access token should be added as a header to the request " .. + "for downstream", + type = "boolean", + default = true + }, + access_token_in_authorization_header = { + description = "Whether the access token should be added in the Authorization " .. + "header as opposed to the X-Access-Token header.", + type = "boolean", + default = false + }, + set_id_token_header = { + description = "Whether the ID token should be added in the X-ID-Token header to " .. + "the request for downstream.", + type = "boolean", + default = true + }, + set_userinfo_header = { + description = "Whether the user info token should be added in the X-Userinfo " .. + "header to the request for downstream.", + type = "boolean", + default = true + } }, required = {"client_id", "client_secret", "discovery"} } @@ -80,6 +105,7 @@ local _M = { schema = schema, } + function _M.check_schema(conf) if conf.ssl_verify == "no" then -- we used to set 'ssl_verify' to "no" @@ -95,56 +121,117 @@ function _M.check_schema(conf) end -local function has_bearer_access_token(ctx) +local function get_bearer_access_token(ctx) + -- Get Authorization header, maybe. local auth_header = core.request.header(ctx, "Authorization") if not auth_header then - return false + -- No Authorization header, get X-Access-Token header, maybe. + local access_token_header = core.request.header(ctx, "X-Access-Token") + if not access_token_header then + -- No X-Access-Token header neither. + return false, nil, nil + end + + -- Return extracted header value. + return true, access_token_header, nil end + -- Check format of Authorization header. local res, err = ngx_re.split(auth_header, " ", nil, nil, 2) + if not res then - return false, err + -- No result was returned. + return false, nil, err + elseif #res < 2 then + -- Header doesn't split into enough tokens. + return false, nil, "Invalid Authorization header format." end - if res[1] == "bearer" then - return true + if string.lower(res[1]) == "bearer" then + -- Return extracted token. + return true, res[2], nil end - return false + return false, nil, nil end local function introspect(ctx, conf) - if has_bearer_access_token(ctx) or conf.bearer_only then - local res, err + -- Extract token, maybe. + local has_token, token, err = get_bearer_access_token(ctx) + + if err then + return ngx.HTTP_BAD_REQUEST, err, nil, nil + end + + if not has_token then + -- Could not find token. - if conf.public_key then - res, err = openidc.bearer_jwt_verify(conf) - if res then - return res - end - else - res, err = openidc.introspect(conf) - if err then - return ngx.HTTP_UNAUTHORIZED, err - else - return res - end - end if conf.bearer_only then - ngx.header["WWW-Authenticate"] = 'Bearer realm="' .. conf.realm - .. '",error="' .. err .. '"' - return ngx.HTTP_UNAUTHORIZED, err + -- Token strictly required in request. + ngx.header["WWW-Authenticate"] = 'Bearer realm="' .. conf.realm .. '"' + return ngx.HTTP_UNAUTHORIZED, "No bearer token found in request.", nil, nil + else + -- Return empty result. + return nil, nil, nil, nil end end - return nil + -- If we get here, token was found in request. + + if conf.public_key then + -- Validate token against public key. + -- TODO: In the called method, the openidc module will try to extract + -- the token by itself again -- from a request header or session cookie. + -- It is inefficient that we also need to extract it (just from headers) + -- so we can add it in the configured header. Find a way to use openidc + -- module's internal methods to extract the token. + local res, err = openidc.bearer_jwt_verify(conf) + + if err then + -- Error while validating or token invalid. + ngx.header["WWW-Authenticate"] = 'Bearer realm="' .. conf.realm .. + '", error="invalid_token", error_description="' .. err .. '"' + return ngx.HTTP_UNAUTHORIZED, err, nil, nil + end + + -- Token successfully validated. + return res, err, token, nil + else + -- Validate token against introspection endpoint. + -- TODO: Same as above for public key validation. + local res, err = openidc.introspect(conf) + + if err then + ngx.header["WWW-Authenticate"] = 'Bearer realm="' .. conf.realm .. + '", error="invalid_token", error_description="' .. err .. '"' + return ngx.HTTP_UNAUTHORIZED, err, nil, nil + end + + -- Token successfully validated and response from the introspection + -- endpoint contains the userinfo. + return res, err, token, res + end end -local function add_user_header(user) - local userinfo = core.json.encode(user) - ngx.req.set_header("X-Userinfo", ngx_encode_base64(userinfo)) +local function add_access_token_header(ctx, conf, token) + if token then + -- Add Authorization or X-Access-Token header, respectively, if not already set. + if conf.set_access_token_header then + if conf.access_token_in_authorization_header then + if not core.request.header(ctx, "Authorization") then + -- Add Authorization header. + core.request.set_header(ctx, "Authorization", "Bearer " .. token) + end + else + if not core.request.header(ctx, "X-Access-Token") then + -- Add X-Access-Token header. + core.request.set_header(ctx, "X-Access-Token", token) + end + end + end + end end @@ -160,40 +247,75 @@ function _M.rewrite(plugin_conf, ctx) if not conf.redirect_uri then conf.redirect_uri = ctx.var.request_uri end + if not conf.ssl_verify then -- openidc use "no" to disable ssl verification conf.ssl_verify = "no" end local response, err - if conf.introspection_endpoint or conf.public_key then - response, err = introspect(ctx, conf) + + if conf.bearer_only or conf.introspection_endpoint or conf.public_key then + -- An introspection endpoint or a public key has been configured. Try to + -- validate the access token from the request, if it is present in a + -- request header. Otherwise, return a nil response. See below for + -- handling of the case where the access token is stored in a session cookie. + local access_token, userinfo + response, err, access_token, userinfo = introspect(ctx, conf) + if err then - core.log.error("failed to introspect in openidc: ", err) + -- Error while validating token or invalid token. + core.log.error("OIDC introspection failed: ", err) return response end + if response then - add_user_header(response) + -- Add configured access token header, maybe. + add_access_token_header(ctx, conf, access_token) + + if userinfo and conf.set_userinfo_header then + -- Set X-Userinfo header to introspection endpoint response. + core.request.set_header(ctx, "X-Userinfo", + ngx_encode_base64(core.json.encode(userinfo))) + end end end if not response then - local response, err = openidc.authenticate(conf) + -- Either token validation via introspection endpoint or public key is + -- not configured, and/or token could not be extracted from the request. + + -- Authenticate the request. This will validate the access token if it + -- is stored in a session cookie, and also renew the token if required. + -- If no token can be extracted, the response will redirect to the ID + -- provider's authorization endpoint to initiate the Relying Party flow. + -- This code path also handles when the ID provider then redirects to + -- the configured redirect URI after successful authentication. + response, err = openidc.authenticate(conf) + if err then - core.log.error("failed to authenticate in openidc: ", err) + core.log.error("OIDC authentication failed: ", err) return 500 end if response then - if response.user then - add_user_header(response.user) - end - if response.access_token then - ngx.req.set_header("X-Access-Token", response.access_token) - end - if response.id_token then + -- If the openidc module has returned a response, it may contain, + -- respectively, the access token, the ID token, and the userinfo. + -- Add respective headers to the request, if so configured. + + -- Add configured access token header, maybe. + add_access_token_header(ctx, conf, response.access_token) + + -- Add X-ID-Token header, maybe. + if response.id_token and conf.set_id_token_header then local token = core.json.encode(response.id_token) - ngx.req.set_header("X-ID-Token", ngx.encode_base64(token)) + core.request.set_header(ctx, "X-ID-Token", ngx.encode_base64(token)) + end + + -- Add X-Userinfo header, maybe. + if response.user and conf.set_userinfo_header then + core.request.set_header(ctx, "X-Userinfo", + ngx_encode_base64(core.json.encode(response.user))) end end end diff --git a/doc/plugins/openid-connect.md b/doc/plugins/openid-connect.md index ddfffa739ba5..11983161670c 100644 --- a/doc/plugins/openid-connect.md +++ b/doc/plugins/openid-connect.md @@ -31,22 +31,55 @@ The OAuth 2 / Open ID Connect(OIDC) plugin provides authentication and introspec ## Attributes -| Name | Type | Requirement | Default | Valid | Description | -| ---------------------------------- | ------- | ----------- | --------------------- | ------- | ---------------------------------------------------------------------------------------------- | -| client_id | string | required | | | OAuth client ID | -| client_secret | string | required | | | OAuth client secret | -| discovery | string | required | | | URL of the discovery endpoint of the identity server | -| scope | string | optional | "openid" | | Scope used for the authentication | -| realm | string | optional | "apisix" | | Realm used for the authentication | -| bearer_only | boolean | optional | false | | Setting this `true` will check for the authorization header in the request with a bearer token | -| logout_path | string | optional | "/logout" | | | -| redirect_uri | string | optional | "ngx.var.request_uri" | | | -| timeout | integer | optional | 3 | [1,...] | Timeout in seconds | -| ssl_verify | boolean | optional | false | | | -| introspection_endpoint | string | optional | | | URL of the token verification endpoint of the identity server | -| introspection_endpoint_auth_method | string | optional | "client_secret_basic" | | Authentication method name for token introspection | -| public_key | string | optional | | | The public key to verify the token | -| token_signing_alg_values_expected | string | optional | | | Algorithm used to sign the token | +| Name | Type | Requirement | Default | Valid | Description | +| ------------------------------------ | ------- | ----------- | --------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------- | +| client_id | string | required | | | OAuth client ID | +| client_secret | string | required | | | OAuth client secret | +| discovery | string | required | | | URL of the discovery endpoint of the identity server | +| scope | string | optional | "openid" | | Scope used for the authentication | +| realm | string | optional | "apisix" | | Realm used for the authentication | +| bearer_only | boolean | optional | false | | Setting this `true` will check for the authorization header in the request with a bearer token | +| logout_path | string | optional | "/logout" | | | +| redirect_uri | string | optional | "ngx.var.request_uri" | | | +| timeout | integer | optional | 3 | [1,...] | Timeout in seconds | +| ssl_verify | boolean | optional | false | | | +| introspection_endpoint | string | optional | | | URL of the token verification endpoint of the identity server | +| introspection_endpoint_auth_method | string | optional | "client_secret_basic" | | Authentication method name for token introspection | +| public_key | string | optional | | | The public key to verify the token | +| token_signing_alg_values_expected | string | optional | | | Algorithm used to sign the token | +| set_access_token_header | boolean | optional | true | | Whether to ensure the access token is set in a request header. | +| access_token_in_authorization_header | boolean | optional | false | | If set to `true`, ensure that the access token is set in the `Authorization` header, otherwise use the `X-Access-Token` header. | +| set_id_token_header | boolean | optional | true | | Whether to ensure the ID token, if available, is set in the `X-ID-Token` request header. | +| set_userinfo_header | boolean | optional | true | | Whether to ensure the UserInfo object, if available, is set in the `X-Userinfo` request header. | + +### Modes of operation +The plugin supports different modes of operation. + +1) It can be configured to just validate an access token that is expected to be present in a request header. +In this case, requests without a token or where the token is invalid are always rejected. This requires +`bearer_only` be set to `true` and that either an introspection endpoint has been configured through +`introspection_endpoint`, or that a public key has been configured through `public_key`. See the relevant +sections below. + +2) Alternatively, the plugin can also be configured to authenticate a request without a valid token against +an identity provider by going through the OIDC Authorization Code flow. The plugin then acts as an OIDC Relying Party. +In this scenario, when the requesting user has authenticated successfully, the plugin will obtain and manage +an access token and further user claims on behalf of the user in a session cookie. Subsequent requests that +contain the cookie will use the access token stored in the cookie. In this case, `bearer_only` must be set to `false`. + +The first option is typically appropriate for service-to-service communication where the requesting side can +be reasonably expected to obtain and manage a valid access token by itself. The second option is convenient +to support web browser interaction with endpoints through a human user that may still need to be authenticated +when accessing for the first time. + +The plugin can also be configured to support both scenarios by setting `bearer_only` to false, but still configuring +either an introspection endpoint or a public key. In this case, introspection of an existing token from a request +header takes precedence over the Relying Party flow. That is, if a request contains an invalid token, the request +will be rejected without redirecting to the ID provider to obtain a valid token. + +The method used to authenticate a request also affects the headers that can be enforced on the request before +sending it to upstream. The headers that can be enforced are mentioned below in each relevant section. + ### Token Introspection @@ -92,6 +125,8 @@ The following command can be used to access the new route. curl -i -X GET http://127.0.0.1:9080/get -H "Host: httpbin.org" -H "Authorization: Bearer {replace_jwt_token}" ``` +In this case, the plugin can enforce that the access token and the UserInfo object get set in respective configured request headers. + #### Introspecting with public key You can also provide the public key of the JWT token to verify the token. In case if you have provided a public key and @@ -129,6 +164,47 @@ curl http://127.0.0.1:9080/apisix/admin/routes/5 -H 'X-API-KEY: edd1c9f034335f13 }' ``` +In this case, the plugin can only enforce that the access token gets set in the configured request headers. + +#### Authentication through OIDC Relying Party flow + +When an incoming request does not contain an access token in a header, nor in an appropriate session cookie, +the plugin can act as an OIDC Relying Party and redirect to the authorization endpoint of the identity provider +to go through the OIDC Authorization Code flow; see https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth. +Once the user has authenticated against the identity provider, the plugin will obtain and manage an access token +and further information from the identity provider on behalf of the user. The information is currently stored +in a session cookie that the user agent can submit on subsequent requests. The plugin will recognize the cookie +and use the information therein to avoid having to go through the flow again. + +The following command adds this mode of operation to a route. + +```bash +curl http://127.0.0.1:9080/apisix/admin/routes/5 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/get", + "plugins": { + "proxy-rewrite": { + "scheme": "https" + }, + "openid-connect": { + "client_id": "api_six_client_id", + "client_secret": "client_secret_code", + "discovery": "full_URL_of_the_discovery_endpoint", + "bearer_only": false, + "realm": "master" +} + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "httpbin.org:443": 1 + } + } +}' +``` + +In this case, the plugin can enforce that the access token, the ID token, and the UserInfo object get set in respective configured request headers. + ## Troubleshooting -Check/modify the DNS settings (`conf/config.yaml) if APISIX cannot resolve/connect to the identity provider. +Check/modify the DNS settings (`conf/config.yaml`) if APISIX cannot resolve/connect to the identity provider. diff --git a/t/plugin/openid-connect.t b/t/plugin/openid-connect.t index 1040d348018e..7222b0034533 100644 --- a/t/plugin/openid-connect.t +++ b/t/plugin/openid-connect.t @@ -19,11 +19,12 @@ use t::APISIX 'no_plan'; repeat_each(1); no_long_string(); no_root_location(); +no_shuffle(); run_tests; __DATA__ -=== TEST 1: sanity +=== TEST 1: Sanity check with minimal valid configuration. --- config location /t { content_by_lua_block { @@ -45,7 +46,7 @@ done -=== TEST 2: missing client_id +=== TEST 2: Missing `client_id`. --- config location /t { content_by_lua_block { @@ -68,7 +69,7 @@ done -=== TEST 3: wrong type of string +=== TEST 3: Wrong type for `client_id`. --- config location /t { content_by_lua_block { @@ -91,7 +92,7 @@ done -=== TEST 4: add plugin +=== TEST 4: Set up new route with plugin matching URI `/hello`. --- config location /t { content_by_lua_block { @@ -161,7 +162,7 @@ passed -=== TEST 5: access +=== TEST 5: Access route w/o bearer token. Should redirect to authentication endpoint of ID provider. --- config location /t { content_by_lua_block { @@ -191,7 +192,591 @@ true -=== TEST 6: update plugin with bearer_only=true +=== TEST 6: Modify route to match catch-all URI `/*` and point plugin to local Keycloak instance. +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "openid-connect": { + "discovery": "http://127.0.0.1:8090/auth/realms/University/.well-known/openid-configuration", + "realm": "University", + "client_id": "course_management", + "client_secret": "d1ec69e9-55d2-4109-a3ea-befa071579d5", + "redirect_uri": "http://127.0.0.1:]] .. ngx.var.server_port .. [[/authenticated", + "ssl_verify": false, + "timeout": 10, + "introspection_endpoint_auth_method": "client_secret_post", + "introspection_endpoint": "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token/introspect", + "set_access_token_header": true, + "access_token_in_authorization_header": false, + "set_id_token_header": true, + "set_userinfo_header": true + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/*" + }]], + [[{ + "node": { + "value": { + "plugins": { + "openid-connect": { + "client_id": "course_management", + "client_secret": "d1ec69e9-55d2-4109-a3ea-befa071579d5", + "discovery": "http://127.0.0.1:8090/auth/realms/University/.well-known/openid-configuration", + "redirect_uri": "http://127.0.0.1:]] .. ngx.var.server_port .. [[/authenticated", + "ssl_verify": false, + "timeout": 10, + "realm": "University", + "introspection_endpoint_auth_method": "client_secret_post", + "introspection_endpoint": "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token/introspect", + "set_access_token_header": true, + "access_token_in_authorization_header": false, + "set_id_token_header": true, + "set_userinfo_header": true + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/*" + }, + "key": "/apisix/routes/1" + }, + "action": "set" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 7: Access route w/o bearer token and go through the full OIDC Relying Party authentication process. +--- config + location /t { + content_by_lua_block { + local http = require "resty.http" + local httpc = http.new() + + -- Invoke /uri endpoint w/o bearer token. Should receive redirect to Keycloak authorization endpoint. + local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/uri" + local res, err = httpc:request_uri(uri, {method = "GET"}) + + if not res then + -- No response, must be an error. + ngx.status = 500 + ngx.say(err) + return + elseif res.status ~= 302 then + -- Not a redirect which we expect. + -- Use 500 to indicate error. + ngx.status = 500 + ngx.say("Initial request was not redirected to ID provider authorization endpoint.") + return + else + -- Redirect to ID provider's authorization endpoint. + + -- Extract nonce and state from response header. + local nonce = res.headers['Location']:match('.*nonce=([^&]+).*') + local state = res.headers['Location']:match('.*state=([^&]+).*') + + -- Extract cookies. Important since OIDC module tracks state with a session cookie. + local cookies = res.headers['Set-Cookie'] + + -- Concatenate cookies into one string as expected when sent in request header. + local cookie_str = "" + + if type(cookies) == 'string' then + cookie_str = cookies:match('([^;]*); .*') + else + -- Must be a table. + local len = #cookies + if len > 0 then + cookie_str = cookies[1]:match('([^;]*); .*') + for i = 2, len do + cookie_str = cookie_str .. "; " .. cookies[i]:match('([^;]*); .*') + end + end + end + + -- Call authorization endpoint we were redirected to. + -- Note: This typically returns a login form which is the case here for Keycloak as well. + -- However, how we process the form to perform the login is specific to Keycloak and + -- possibly even the version used. + res, err = httpc:request_uri(res.headers['Location'], {method = "GET"}) + + if not res then + -- No response, must be an error. + ngx.status = 500 + ngx.say(err) + return + elseif res.status ~= 200 then + -- Unexpected response. + ngx.status = res.status + ngx.say(res.body) + return + end + + -- Check if response code was ok. + if res.status == 200 then + -- From the returned form, extract the submit URI and parameters. + local uri, params = res.body:match('.*action="(.*)%?(.*)" method="post">') + + -- Substitute escaped ampersand in parameters. + params = params:gsub("&", "&") + + -- Get all cookies returned. Probably not so important since not part of OIDC specification. + local auth_cookies = res.headers['Set-Cookie'] + + -- Concatenate cookies into one string as expected when sent in request header. + local auth_cookie_str = "" + + if type(auth_cookies) == 'string' then + auth_cookie_str = auth_cookies:match('([^;]*); .*') + else + -- Must be a table. + local len = #auth_cookies + if len > 0 then + auth_cookie_str = auth_cookies[1]:match('([^;]*); .*') + for i = 2, len do + auth_cookie_str = auth_cookie_str .. "; " .. auth_cookies[i]:match('([^;]*); .*') + end + end + end + + -- Invoke the submit URI with parameters and cookies, adding username and password in the body. + -- Note: Username and password are specific to the Keycloak Docker image used. + res, err = httpc:request_uri(uri .. "?" .. params, { + method = "POST", + body = "username=teacher@gmail.com&password=123456", + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded", + ["Cookie"] = auth_cookie_str + } + }) + + if not res then + -- No response, must be an error. + ngx.status = 500 + ngx.say(err) + return + elseif res.status ~= 302 then + -- Not a redirect which we expect. + -- Use 500 to indicate error. + ngx.status = 500 + ngx.say("Login form submission did not return redirect to redirect URI.") + return + end + + -- Extract the redirect URI from the response header. + -- TODO: Consider validating this against the plugin configuration. + local redirect_uri = res.headers['Location'] + + -- Invoke the redirect URI (which contains the authorization code as an URL parameter). + res, err = httpc:request_uri(redirect_uri, { + method = "GET", + headers = { + ["Cookie"] = cookie_str + } + }) + + if not res then + -- No response, must be an error. + ngx.status = 500 + ngx.say(err) + return + elseif res.status ~= 302 then + -- Not a redirect which we expect. + -- Use 500 to indicate error. + ngx.status = 500 + ngx.say("Invoking redirect URI with authorization code did not return redirect to original URI.") + return + end + + -- Get all cookies returned. This should update the session cookie maintained by the OIDC module with the new state. + -- E.g. the session cookie should now contain the access token, ID token and user info. + -- The cookie itself should however be treated as opaque. + cookies = res.headers['Set-Cookie'] + + -- Concatenate cookies into one string as expected when sent in request header. + if type(cookies) == 'string' then + cookie_str = cookies:match('([^;]*); .*') + else + -- Must be a table. + local len = #cookies + if len > 0 then + cookie_str = cookies[1]:match('([^;]*); .*') + for i = 2, len do + cookie_str = cookie_str .. "; " .. cookies[i]:match('([^;]*); .*') + end + end + end + + -- Get the final URI out of the Location response header. This should be the original URI that was requested. + -- TODO: Consider checking the URI against the original request URI. + redirect_uri = "http://127.0.0.1:" .. ngx.var.server_port .. res.headers['Location'] + + -- Make the final call back to the original URI. + res, err = httpc:request_uri(redirect_uri, { + method = "GET", + headers = { + ["Cookie"] = cookie_str + } + }) + + if not res then + -- No response, must be an error. + ngx.status = 500 + ngx.say(err) + return + elseif res.status ~= 200 then + -- Not a valid response. + -- Use 500 to indicate error. + ngx.status = 500 + ngx.say("Invoking the original URI didn't return the expected result.") + return + end + + ngx.status = res.status + ngx.say(res.body) + else + -- Response from Keycloak not ok. + ngx.say(false) + end + end + } + } +--- request +GET /t +--- response_body_like +uri: /uri +cookie: .* +host: 127.0.0.1 +user-agent: .* +x-access-token: ey.* +x-id-token: ey.* +x-real-ip: 127.0.0.1 +x-userinfo: ey.* +--- no_error_log +[error] + + + +=== TEST 8: Re-configure plugin with respect to headers that get sent to upstream. +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "openid-connect": { + "discovery": "http://127.0.0.1:8090/auth/realms/University/.well-known/openid-configuration", + "realm": "University", + "client_id": "course_management", + "client_secret": "d1ec69e9-55d2-4109-a3ea-befa071579d5", + "redirect_uri": "http://127.0.0.1:]] .. ngx.var.server_port .. [[/authenticated", + "ssl_verify": false, + "timeout": 10, + "introspection_endpoint_auth_method": "client_secret_post", + "introspection_endpoint": "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token/introspect", + "set_access_token_header": true, + "access_token_in_authorization_header": true, + "set_id_token_header": false, + "set_userinfo_header": false + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/*" + }]], + [[{ + "node": { + "value": { + "plugins": { + "openid-connect": { + "client_id": "course_management", + "client_secret": "d1ec69e9-55d2-4109-a3ea-befa071579d5", + "discovery": "http://127.0.0.1:8090/auth/realms/University/.well-known/openid-configuration", + "redirect_uri": "http://127.0.0.1:]] .. ngx.var.server_port .. [[/authenticated", + "ssl_verify": false, + "timeout": 10, + "realm": "University", + "introspection_endpoint_auth_method": "client_secret_post", + "introspection_endpoint": "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token/introspect", + "set_access_token_header": true, + "access_token_in_authorization_header": true, + "set_id_token_header": false, + "set_userinfo_header": false + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/*" + }, + "key": "/apisix/routes/1" + }, + "action": "set" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 9: Access route w/o bearer token and go through the full OIDC Relying Party authentication process. +--- config + location /t { + content_by_lua_block { + local http = require "resty.http" + local httpc = http.new() + + -- Invoke /uri endpoint w/o bearer token. Should receive redirect to Keycloak authorization endpoint. + local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/uri" + local res, err = httpc:request_uri(uri, {method = "GET"}) + + if not res then + -- No response, must be an error. + ngx.status = 500 + ngx.say(err) + return + elseif res.status ~= 302 then + -- Not a redirect which we expect. + -- Use 500 to indicate error. + ngx.status = 500 + ngx.say("Initial request was not redirected to ID provider authorization endpoint.") + return + else + -- Redirect to ID provider's authorization endpoint. + + -- Extract nonce and state from response header. + local nonce = res.headers['Location']:match('.*nonce=([^&]+).*') + local state = res.headers['Location']:match('.*state=([^&]+).*') + + -- Extract cookies. Important since OIDC module tracks state with a session cookie. + local cookies = res.headers['Set-Cookie'] + + -- Concatenate cookies into one string as expected when sent in request header. + local cookie_str = "" + + if type(cookies) == 'string' then + cookie_str = cookies:match('([^;]*); .*') + else + -- Must be a table. + local len = #cookies + if len > 0 then + cookie_str = cookies[1]:match('([^;]*); .*') + for i = 2, len do + cookie_str = cookie_str .. "; " .. cookies[i]:match('([^;]*); .*') + end + end + end + + -- Call authorization endpoint we were redirected to. + -- Note: This typically returns a login form which is the case here for Keycloak as well. + -- However, how we process the form to perform the login is specific to Keycloak and + -- possibly even the version used. + res, err = httpc:request_uri(res.headers['Location'], {method = "GET"}) + + if not res then + -- No response, must be an error. + ngx.status = 500 + ngx.say(err) + return + elseif res.status ~= 200 then + -- Unexpected response. + ngx.status = res.status + ngx.say(res.body) + return + end + + -- Check if response code was ok. + if res.status == 200 then + -- From the returned form, extract the submit URI and parameters. + local uri, params = res.body:match('.*action="(.*)%?(.*)" method="post">') + + -- Substitute escaped ampersand in parameters. + params = params:gsub("&", "&") + + -- Get all cookies returned. Probably not so important since not part of OIDC specification. + local auth_cookies = res.headers['Set-Cookie'] + + -- Concatenate cookies into one string as expected when sent in request header. + local auth_cookie_str = "" + + if type(auth_cookies) == 'string' then + auth_cookie_str = auth_cookies:match('([^;]*); .*') + else + -- Must be a table. + local len = #auth_cookies + if len > 0 then + auth_cookie_str = auth_cookies[1]:match('([^;]*); .*') + for i = 2, len do + auth_cookie_str = auth_cookie_str .. "; " .. auth_cookies[i]:match('([^;]*); .*') + end + end + end + + -- Invoke the submit URI with parameters and cookies, adding username and password in the body. + -- Note: Username and password are specific to the Keycloak Docker image used. + res, err = httpc:request_uri(uri .. "?" .. params, { + method = "POST", + body = "username=teacher@gmail.com&password=123456", + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded", + ["Cookie"] = auth_cookie_str + } + }) + + if not res then + -- No response, must be an error. + ngx.status = 500 + ngx.say(err) + return + elseif res.status ~= 302 then + -- Not a redirect which we expect. + -- Use 500 to indicate error. + ngx.status = 500 + ngx.say("Login form submission did not return redirect to redirect URI.") + return + end + + -- Extract the redirect URI from the response header. + -- TODO: Consider validating this against the plugin configuration. + local redirect_uri = res.headers['Location'] + + -- Invoke the redirect URI (which contains the authorization code as an URL parameter). + res, err = httpc:request_uri(redirect_uri, { + method = "GET", + headers = { + ["Cookie"] = cookie_str + } + }) + + if not res then + -- No response, must be an error. + ngx.status = 500 + ngx.say(err) + return + elseif res.status ~= 302 then + -- Not a redirect which we expect. + -- Use 500 to indicate error. + ngx.status = 500 + ngx.say("Invoking redirect URI with authorization code did not return redirect to original URI.") + return + end + + -- Get all cookies returned. This should update the session cookie maintained by the OIDC module with the new state. + -- E.g. the session cookie should now contain the access token, ID token and user info. + -- The cookie itself should however be treated as opaque. + cookies = res.headers['Set-Cookie'] + + -- Concatenate cookies into one string as expected when sent in request header. + if type(cookies) == 'string' then + cookie_str = cookies:match('([^;]*); .*') + else + -- Must be a table. + local len = #cookies + if len > 0 then + cookie_str = cookies[1]:match('([^;]*); .*') + for i = 2, len do + cookie_str = cookie_str .. "; " .. cookies[i]:match('([^;]*); .*') + end + end + end + + -- Get the final URI out of the Location response header. This should be the original URI that was requested. + -- TODO: Consider checking the URI against the original request URI. + redirect_uri = "http://127.0.0.1:" .. ngx.var.server_port .. res.headers['Location'] + + -- Make the final call back to the original URI. + res, err = httpc:request_uri(redirect_uri, { + method = "GET", + headers = { + ["Cookie"] = cookie_str + } + }) + + if not res then + -- No response, must be an error. + ngx.status = 500 + ngx.say(err) + return + elseif res.status ~= 200 then + -- Not a valid response. + -- Use 500 to indicate error. + ngx.status = 500 + ngx.say("Invoking the original URI didn't return the expected result.") + return + end + + ngx.status = res.status + ngx.say(res.body) + else + -- Response from Keycloak not ok. + ngx.say(false) + end + end + } + } +--- request +GET /t +--- response_body_like +uri: /uri +authorization: Bearer ey.* +cookie: .* +host: 127.0.0.1 +user-agent: .* +x-real-ip: 127.0.0.1 +--- no_error_log +[error] + + + +=== TEST 10: Update plugin with `bearer_only=true`. --- config location /t { content_by_lua_block { @@ -263,20 +848,31 @@ passed -=== TEST 7: access +=== TEST 11: Access route w/o bearer token. Should return 401 (Unauthorized). --- timeout: 10s --- request GET /hello --- error_code: 401 --- response_headers_like -WWW-Authenticate: Bearer realm=apisix ---- no_error_log -[error] ---- SKIP +WWW-Authenticate: Bearer realm="apisix" +--- error_log +OIDC introspection failed: No bearer token found in request. + + + +=== TEST 12: Access route with invalid Authorization header value. Should return 400 (Bad Request). +--- timeout: 10s +--- request +GET /hello +--- more_headers +Authorization: foo +--- error_code: 400 +--- error_log +OIDC introspection failed: Invalid Authorization header format. -=== TEST 8: update plugin public key +=== TEST 13: Update plugin with ID provider public key, so tokens can be validated locally. --- config location /t { content_by_lua_block { @@ -356,7 +952,7 @@ passed -=== TEST 9: access introspection with correct token +=== TEST 14: Access route with valid token. --- config location /t { content_by_lua_block { @@ -388,7 +984,287 @@ true -=== TEST 10: access introspection with wrong token +=== TEST 15: Update route URI to '/uri' where upstream endpoint returns request headers in response body. +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ "plugins": { + "openid-connect": { + "client_id": "kbyuFDidLLm280LIwVFiazOqjO3ty8KH", + "client_secret": "60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa", + "discovery": "https://samples.auth0.com/.well-known/openid-configuration", + "redirect_uri": "https://iresty.com", + "ssl_verify": false, + "timeout": 10, + "bearer_only": true, + "scope": "apisix", + "public_key": "-----BEGIN PUBLIC KEY-----\n]] .. + [[MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANW16kX5SMrMa2t7F2R1w6Bk/qpjS4QQ\n]] .. + [[hnrbED3Dpsl9JXAx90MYsIWp51hBxJSE/EPVK8WF/sjHK1xQbEuDfEECAwEAAQ==\n]] .. + [[-----END PUBLIC KEY-----", + "token_signing_alg_values_expected": "RS256" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/uri" + }]], + [[{ "node": { + "value": { + "plugins": { + "openid-connect": { + "client_id": "kbyuFDidLLm280LIwVFiazOqjO3ty8KH", + "client_secret": "60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa", + "discovery": "https://samples.auth0.com/.well-known/openid-configuration", + "redirect_uri": "https://iresty.com", + "ssl_verify": false, + "timeout": 10, + "bearer_only": true, + "scope": "apisix", + "public_key": "-----BEGIN PUBLIC KEY-----\n]] .. + [[MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANW16kX5SMrMa2t7F2R1w6Bk/qpjS4QQ\n]] .. + [[hnrbED3Dpsl9JXAx90MYsIWp51hBxJSE/EPVK8WF/sjHK1xQbEuDfEECAwEAAQ==\n]] .. + [[-----END PUBLIC KEY-----", + "token_signing_alg_values_expected": "RS256" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/uri" + }, + "key": "/apisix/routes/1" + }, + "action": "set" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 16: Access route with valid token in `Authorization` header. Upstream should additionally get the token in the `X-Access-Token` header. +--- request +GET /uri HTTP/1.1 +--- more_headers +Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhMSI6IkRhdGEgMSIsImlhdCI6MTU4NTEyMjUwMiwiZXhwIjoxOTAwNjk4NTAyLCJhdWQiOiJodHRwOi8vbXlzb2Z0Y29ycC5pbiIsImlzcyI6Ik15c29mdCBjb3JwIiwic3ViIjoic29tZUB1c2VyLmNvbSJ9.u1ISx7JbuK_GFRIUqIMP175FqXRyF9V7y86480Q4N3jNxs3ePbc51TFtIHDrKttstU4Tub28PYVSlr-HXfjo7w +--- response_body +uri: /uri +authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhMSI6IkRhdGEgMSIsImlhdCI6MTU4NTEyMjUwMiwiZXhwIjoxOTAwNjk4NTAyLCJhdWQiOiJodHRwOi8vbXlzb2Z0Y29ycC5pbiIsImlzcyI6Ik15c29mdCBjb3JwIiwic3ViIjoic29tZUB1c2VyLmNvbSJ9.u1ISx7JbuK_GFRIUqIMP175FqXRyF9V7y86480Q4N3jNxs3ePbc51TFtIHDrKttstU4Tub28PYVSlr-HXfjo7w +host: localhost +x-access-token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhMSI6IkRhdGEgMSIsImlhdCI6MTU4NTEyMjUwMiwiZXhwIjoxOTAwNjk4NTAyLCJhdWQiOiJodHRwOi8vbXlzb2Z0Y29ycC5pbiIsImlzcyI6Ik15c29mdCBjb3JwIiwic3ViIjoic29tZUB1c2VyLmNvbSJ9.u1ISx7JbuK_GFRIUqIMP175FqXRyF9V7y86480Q4N3jNxs3ePbc51TFtIHDrKttstU4Tub28PYVSlr-HXfjo7w +x-real-ip: 127.0.0.1 +--- no_error_log +[error] +--- error_code: 200 + + + +=== TEST 17: Update plugin to only use `Authorization` header. +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ "plugins": { + "openid-connect": { + "client_id": "kbyuFDidLLm280LIwVFiazOqjO3ty8KH", + "client_secret": "60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa", + "discovery": "https://samples.auth0.com/.well-known/openid-configuration", + "redirect_uri": "https://iresty.com", + "ssl_verify": false, + "timeout": 10, + "bearer_only": true, + "scope": "apisix", + "public_key": "-----BEGIN PUBLIC KEY-----\n]] .. + [[MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANW16kX5SMrMa2t7F2R1w6Bk/qpjS4QQ\n]] .. + [[hnrbED3Dpsl9JXAx90MYsIWp51hBxJSE/EPVK8WF/sjHK1xQbEuDfEECAwEAAQ==\n]] .. + [[-----END PUBLIC KEY-----", + "token_signing_alg_values_expected": "RS256", + "set_access_token_header": true, + "access_token_in_authorization_header": true, + "set_id_token_header": false, + "set_userinfo_header": false + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/uri" + }]], + [[{ "node": { + "value": { + "plugins": { + "openid-connect": { + "client_id": "kbyuFDidLLm280LIwVFiazOqjO3ty8KH", + "client_secret": "60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa", + "discovery": "https://samples.auth0.com/.well-known/openid-configuration", + "redirect_uri": "https://iresty.com", + "ssl_verify": false, + "timeout": 10, + "bearer_only": true, + "scope": "apisix", + "public_key": "-----BEGIN PUBLIC KEY-----\n]] .. + [[MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANW16kX5SMrMa2t7F2R1w6Bk/qpjS4QQ\n]] .. + [[hnrbED3Dpsl9JXAx90MYsIWp51hBxJSE/EPVK8WF/sjHK1xQbEuDfEECAwEAAQ==\n]] .. + [[-----END PUBLIC KEY-----", + "token_signing_alg_values_expected": "RS256", + "access_token_in_authorization_header": true, + "set_id_token_header": false, + "set_userinfo_header": false + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/uri" + }, + "key": "/apisix/routes/1" + }, + "action": "set" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 18: Access route with valid token in `Authorization` header. Upstream should not get the additional `X-Access-Token` header. +--- request +GET /uri HTTP/1.1 +--- more_headers +Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhMSI6IkRhdGEgMSIsImlhdCI6MTU4NTEyMjUwMiwiZXhwIjoxOTAwNjk4NTAyLCJhdWQiOiJodHRwOi8vbXlzb2Z0Y29ycC5pbiIsImlzcyI6Ik15c29mdCBjb3JwIiwic3ViIjoic29tZUB1c2VyLmNvbSJ9.u1ISx7JbuK_GFRIUqIMP175FqXRyF9V7y86480Q4N3jNxs3ePbc51TFtIHDrKttstU4Tub28PYVSlr-HXfjo7w +--- response_body +uri: /uri +authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhMSI6IkRhdGEgMSIsImlhdCI6MTU4NTEyMjUwMiwiZXhwIjoxOTAwNjk4NTAyLCJhdWQiOiJodHRwOi8vbXlzb2Z0Y29ycC5pbiIsImlzcyI6Ik15c29mdCBjb3JwIiwic3ViIjoic29tZUB1c2VyLmNvbSJ9.u1ISx7JbuK_GFRIUqIMP175FqXRyF9V7y86480Q4N3jNxs3ePbc51TFtIHDrKttstU4Tub28PYVSlr-HXfjo7w +host: localhost +x-real-ip: 127.0.0.1 +--- no_error_log +[error] +--- error_code: 200 + + + +=== TEST 19: Switch route URI back to `/hello`. +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ "plugins": { + "openid-connect": { + "client_id": "kbyuFDidLLm280LIwVFiazOqjO3ty8KH", + "client_secret": "60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa", + "discovery": "https://samples.auth0.com/.well-known/openid-configuration", + "redirect_uri": "https://iresty.com", + "ssl_verify": false, + "timeout": 10, + "bearer_only": true, + "scope": "apisix", + "public_key": "-----BEGIN PUBLIC KEY-----\n]] .. + [[MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANW16kX5SMrMa2t7F2R1w6Bk/qpjS4QQ\n]] .. + [[hnrbED3Dpsl9JXAx90MYsIWp51hBxJSE/EPVK8WF/sjHK1xQbEuDfEECAwEAAQ==\n]] .. + [[-----END PUBLIC KEY-----", + "token_signing_alg_values_expected": "RS256" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]], + [[{ "node": { + "value": { + "plugins": { + "openid-connect": { + "client_id": "kbyuFDidLLm280LIwVFiazOqjO3ty8KH", + "client_secret": "60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa", + "discovery": "https://samples.auth0.com/.well-known/openid-configuration", + "redirect_uri": "https://iresty.com", + "ssl_verify": false, + "timeout": 10, + "bearer_only": true, + "scope": "apisix", + "public_key": "-----BEGIN PUBLIC KEY-----\n]] .. + [[MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANW16kX5SMrMa2t7F2R1w6Bk/qpjS4QQ\n]] .. + [[hnrbED3Dpsl9JXAx90MYsIWp51hBxJSE/EPVK8WF/sjHK1xQbEuDfEECAwEAAQ==\n]] .. + [[-----END PUBLIC KEY-----", + "token_signing_alg_values_expected": "RS256" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }, + "key": "/apisix/routes/1" + }, + "action": "set" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 20: Access route with invalid token. Should return 401. --- config location /t { content_by_lua_block { @@ -419,7 +1295,7 @@ jwt signature verification failed -=== TEST 11: Update route with keycloak config for introspection +=== TEST 21: Update route with Keycloak introspection endpoint and public key removed. Should now invoke introspection endpoint to validate tokens. --- config location /t { content_by_lua_block { @@ -495,10 +1371,11 @@ passed -=== TEST 12: Access keycloak with correct token +=== TEST 22: Obtain valid token and access route with it. --- config location /t { content_by_lua_block { + -- Obtain valid access token from Keycloak using known username and password. local json_decode = require("toolkit.json").decode local http = require "resty.http" local httpc = http.new() @@ -511,14 +1388,19 @@ passed } }) + -- Check response from keycloak and fail quickly if there's no response. if not res then ngx.say(err) return end + -- Check if response code was ok. if res.status == 200 then + -- Get access token from JSON response body. local body = json_decode(res.body) local accessToken = body["access_token"] + + -- Access route using access token. Should work. uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello" local res, err = httpc:request_uri(uri, { method = "GET", @@ -528,11 +1410,14 @@ passed }) if res.status == 200 then + -- Route accessed successfully. ngx.say(true) else + -- Couldn't access route. ngx.say(false) end else + -- Response from Keycloak not ok. ngx.say(false) end } @@ -546,10 +1431,11 @@ true -=== TEST 13: Access keycloak with wrong token +=== TEST 23: Access route with an invalid token. --- config location /t { content_by_lua_block { + -- Access route using a fake access token. local http = require "resty.http" local httpc = http.new() local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello" @@ -572,11 +1458,11 @@ GET /t --- response_body false --- error_log -failed to introspect in openidc: invalid token +OIDC introspection failed: invalid token -=== TEST 14: check default value +=== TEST 24: Check defaults. --- config location /t { content_by_lua_block { @@ -598,6 +1484,6 @@ failed to introspect in openidc: invalid token --- request GET /t --- response_body -{"bearer_only":false,"client_id":"kbyuFDidLLm280LIwVFiazOqjO3ty8KH","client_secret":"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa","discovery":"http://127.0.0.1:1980/.well-known/openid-configuration","introspection_endpoint_auth_method":"client_secret_basic","logout_path":"/logout","realm":"apisix","scope":"openid","ssl_verify":false,"timeout":3} +{"access_token_in_authorization_header":false,"bearer_only":false,"client_id":"kbyuFDidLLm280LIwVFiazOqjO3ty8KH","client_secret":"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa","discovery":"http://127.0.0.1:1980/.well-known/openid-configuration","introspection_endpoint_auth_method":"client_secret_basic","logout_path":"/logout","realm":"apisix","scope":"openid","set_access_token_header":true,"set_id_token_header":true,"set_userinfo_header":true,"ssl_verify":false,"timeout":3} --- no_error_log [error]