diff --git a/apisix/plugins/openid-connect.lua b/apisix/plugins/openid-connect.lua index eb0d3b526c58..c1ebf58c93cb 100644 --- a/apisix/plugins/openid-connect.lua +++ b/apisix/plugins/openid-connect.lua @@ -298,6 +298,14 @@ local schema = { type = "boolean", default = false }, + hide_credentials = { + description = "Whether to clear the inbound Authorization header carrying the " .. + "bearer token so it is not forwarded to the upstream. The client-supplied " .. + "X-Access-Token header is always cleared regardless of this option. Works " .. + "independently of set_access_token_header.", + type = "boolean", + default = false + }, set_access_token_header = { description = "Whether the access token should be added as a header to the request " .. "for downstream", @@ -834,6 +842,15 @@ function _M.rewrite(plugin_conf, ctx) end end + -- Hide the inbound bearer credential from the upstream if + -- configured. This is done after validation because + -- lua-resty-openidc re-reads the Authorization header while + -- verifying the token. set_access_token_header may add the + -- validated token back as a fresh, plugin-controlled header below. + if conf.hide_credentials then + core.request.set_header(ctx, "Authorization", nil) + end + -- Add configured access token header, maybe. add_access_token_header(ctx, conf, access_token) diff --git a/docs/en/latest/plugins/openid-connect.md b/docs/en/latest/plugins/openid-connect.md index e196d6922206..f2756ce1ea3c 100644 --- a/docs/en/latest/plugins/openid-connect.md +++ b/docs/en/latest/plugins/openid-connect.md @@ -61,6 +61,7 @@ The `openid-connect` Plugin supports the integration with [OpenID Connect (OIDC) | use_jwks | boolean | False | false | | If true and if `public_key` is not set, use the JWKS to verify JWT signature and skip token introspection in client credentials flow. The JWKS endpoint is parsed from the discovery document. | | use_pkce | boolean | False | false | | If true, use the Proof Key for Code Exchange (PKCE) for Authorization Code Flow as defined in [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636). | | token_signing_alg_values_expected | string | False | | | Algorithm used for signing JWT, such as `RS256`. | +| hide_credentials | boolean | False | false | | If true, clear the inbound `Authorization` header carrying the bearer token so it is not forwarded to the upstream. The `X-Access-Token` header supplied by the client is always cleared regardless of this option. Works independently of `set_access_token_header`: when both are enabled, the original credential is removed and the validated access token is added back as a fresh, plugin-controlled header. | | set_access_token_header | boolean | False | true | | If true, set the access token in a request header. By default, the `X-Access-Token` header is used. | | access_token_in_authorization_header | boolean | False | false | | If true and if `set_access_token_header` is also true, set the access token in the `Authorization` header. | | set_id_token_header | boolean | False | true | | If true and if the ID token is available, set the value in the `X-ID-Token` request header. | diff --git a/docs/zh/latest/plugins/openid-connect.md b/docs/zh/latest/plugins/openid-connect.md index 445d2a977f3e..af5683508ad4 100644 --- a/docs/zh/latest/plugins/openid-connect.md +++ b/docs/zh/latest/plugins/openid-connect.md @@ -61,6 +61,7 @@ import TabItem from '@theme/TabItem'; | use_jwks | boolean | 否 | false | | 如果为 true 且未设置 `public_key`,则使用 JWKS 验证 JWT 签名并跳过客户端凭证流中的令牌内省。JWKS 端点从发现文档中解析。 | | use_pkce | boolean | 否 | false | | 如果为 true,则按照 [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636) 定义,在授权码流程中使用 PKCE(Proof Key for Code Exchange)。 | | token_signing_alg_values_expected | string | 否 | | | 用于签署 JWT 的算法,例如 `RS256`。 | +| hide_credentials | boolean | 否 | false | | 如果为 true,则清除携带 bearer 令牌的入站 `Authorization` 请求头,使其不会转发到上游。无论该选项如何设置,客户端提供的 `X-Access-Token` 头始终会被清除。该选项与 `set_access_token_header` 相互独立:当两者都启用时,原始凭证会被移除,而经过校验的访问令牌会作为由插件控制的新请求头重新添加。 | | set_access_token_header | boolean | 否 | true | | 如果为 true,则在请求标头中设置访问令牌。默认情况下,使用 `X-Access-Token` 标头。| | access_token_in_authorization_header | boolean | 否 | false | | 如果为 true 并且 `set_access_token_header` 也为 true,则在 `Authorization` 标头中设置访问令牌。 | | set_id_token_header | boolean | 否 | true | | 如果为 true 并且 ID 令牌可用,则在 `X-ID-Token` 请求标头中设置值。 | diff --git a/t/plugin/openid-connect-identity-headers.t b/t/plugin/openid-connect-identity-headers.t index ca842c827397..00aa3705cedf 100644 --- a/t/plugin/openid-connect-identity-headers.t +++ b/t/plugin/openid-connect-identity-headers.t @@ -166,3 +166,147 @@ host: localhost x-access-token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhMSI6IkRhdGEgMSIsImlhdCI6MTU4NTEyMjUwMiwiZXhwIjoxOTAwNjk4NTAyLCJhdWQiOiJodHRwOi8vbXlzb2Z0Y29ycC5pbiIsImlzcyI6Ik15c29mdCBjb3JwIiwic3ViIjoic29tZUB1c2VyLmNvbSJ9.Vq_sBN7nH67vMDbiJE01EP4hvJYE_5ju6izjkOX8pF5OS4g2RWKWpL6h6-b0tTkCzG4JD5BEl13LWW-Gxxw0i9vEK0FLg_kC_kZLYB8WuQ6B9B9YwzmZ3OLbgnYzt_VD7D-7psEbwapJl5hbFsIjDgOAEx-UCmjUcl2frZxZavG2LUiEGs9Ri7KqOZmTLgNDMWfeWh1t1LyD0_b-eTInbasVtKQxMlb5kR0Ln_Qg5092L-irJ7dqaZma7HItCnzXJROdqJEsMIBAYRwDGa_w5kIACeMOdU85QKtMHzOenYFkm6zh_s59ndziTctKMz196Y8AL08xuTi6d1gEWpM92A x-real-ip: 127.0.0.1 --- error_code: 200 + + + +=== TEST 5: Update route to bearer_only with hide_credentials = true. +--- 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", + "hide_credentials": true, + "public_key": "-----BEGIN PUBLIC KEY-----\n]] .. + [[MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw86xcJwNxL2MkWnjIGiw\n]] .. + [[94QY78Sq89dLqMdV/Ku2GIX9lYkbS0VDGtmxDGJLBOYW4cKTX+pigJyzglLgE+nD\n]] .. + [[z3VJf2oCqSV74gTyEdi7sw9e1rCyR6dR8VA7LEpIHwmhnDhhjXy1IYSKRdiVHLS5\n]] .. + [[sYmaAGckpUo3MLqUrgydGj5tFzvK/R/ELuZBdlZM+XuWxYry05r860E3uL+VdVCO\n]] .. + [[oU4RJQknlJnTRd7ht8KKcZb6uM14C057i26zX/xnOJpaVflA4EyEo99hKQAdr8Sh\n]] .. + [[G70MOLYvGCZxl1o8S3q4X67MxcPlfJaXnbog2AOOGRaFar88XiLFWTbXMCLuz7xD\n]] .. + [[zQIDAQAB\n]] .. + [[-----END PUBLIC KEY-----", + "token_signing_alg_values_expected": "RS256", + "set_id_token_header": false, + "set_userinfo_header": false, + "claim_validator": { + "issuer": { + "valid_issuers": ["Mysoft corp"] + } + } + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/uri" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 6: With hide_credentials, the source Authorization header is cleared, but the validated token is still added back as X-Access-Token (set_access_token_header defaults to true). +--- request +GET /uri HTTP/1.1 +--- more_headers +Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhMSI6IkRhdGEgMSIsImlhdCI6MTU4NTEyMjUwMiwiZXhwIjoxOTAwNjk4NTAyLCJhdWQiOiJodHRwOi8vbXlzb2Z0Y29ycC5pbiIsImlzcyI6Ik15c29mdCBjb3JwIiwic3ViIjoic29tZUB1c2VyLmNvbSJ9.Vq_sBN7nH67vMDbiJE01EP4hvJYE_5ju6izjkOX8pF5OS4g2RWKWpL6h6-b0tTkCzG4JD5BEl13LWW-Gxxw0i9vEK0FLg_kC_kZLYB8WuQ6B9B9YwzmZ3OLbgnYzt_VD7D-7psEbwapJl5hbFsIjDgOAEx-UCmjUcl2frZxZavG2LUiEGs9Ri7KqOZmTLgNDMWfeWh1t1LyD0_b-eTInbasVtKQxMlb5kR0Ln_Qg5092L-irJ7dqaZma7HItCnzXJROdqJEsMIBAYRwDGa_w5kIACeMOdU85QKtMHzOenYFkm6zh_s59ndziTctKMz196Y8AL08xuTi6d1gEWpM92A +--- response_body +uri: /uri +host: localhost +x-access-token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhMSI6IkRhdGEgMSIsImlhdCI6MTU4NTEyMjUwMiwiZXhwIjoxOTAwNjk4NTAyLCJhdWQiOiJodHRwOi8vbXlzb2Z0Y29ycC5pbiIsImlzcyI6Ik15c29mdCBjb3JwIiwic3ViIjoic29tZUB1c2VyLmNvbSJ9.Vq_sBN7nH67vMDbiJE01EP4hvJYE_5ju6izjkOX8pF5OS4g2RWKWpL6h6-b0tTkCzG4JD5BEl13LWW-Gxxw0i9vEK0FLg_kC_kZLYB8WuQ6B9B9YwzmZ3OLbgnYzt_VD7D-7psEbwapJl5hbFsIjDgOAEx-UCmjUcl2frZxZavG2LUiEGs9Ri7KqOZmTLgNDMWfeWh1t1LyD0_b-eTInbasVtKQxMlb5kR0Ln_Qg5092L-irJ7dqaZma7HItCnzXJROdqJEsMIBAYRwDGa_w5kIACeMOdU85QKtMHzOenYFkm6zh_s59ndziTctKMz196Y8AL08xuTi6d1gEWpM92A +x-real-ip: 127.0.0.1 +--- error_code: 200 + + + +=== TEST 7: hide_credentials with set_access_token_header = false clears Authorization and adds nothing back. +--- 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", + "hide_credentials": true, + "set_access_token_header": false, + "public_key": "-----BEGIN PUBLIC KEY-----\n]] .. + [[MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw86xcJwNxL2MkWnjIGiw\n]] .. + [[94QY78Sq89dLqMdV/Ku2GIX9lYkbS0VDGtmxDGJLBOYW4cKTX+pigJyzglLgE+nD\n]] .. + [[z3VJf2oCqSV74gTyEdi7sw9e1rCyR6dR8VA7LEpIHwmhnDhhjXy1IYSKRdiVHLS5\n]] .. + [[sYmaAGckpUo3MLqUrgydGj5tFzvK/R/ELuZBdlZM+XuWxYry05r860E3uL+VdVCO\n]] .. + [[oU4RJQknlJnTRd7ht8KKcZb6uM14C057i26zX/xnOJpaVflA4EyEo99hKQAdr8Sh\n]] .. + [[G70MOLYvGCZxl1o8S3q4X67MxcPlfJaXnbog2AOOGRaFar88XiLFWTbXMCLuz7xD\n]] .. + [[zQIDAQAB\n]] .. + [[-----END PUBLIC KEY-----", + "token_signing_alg_values_expected": "RS256", + "set_id_token_header": false, + "set_userinfo_header": false, + "claim_validator": { + "issuer": { + "valid_issuers": ["Mysoft corp"] + } + } + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/uri" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 8: Neither Authorization nor X-Access-Token reaches the upstream. +--- request +GET /uri HTTP/1.1 +--- more_headers +Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhMSI6IkRhdGEgMSIsImlhdCI6MTU4NTEyMjUwMiwiZXhwIjoxOTAwNjk4NTAyLCJhdWQiOiJodHRwOi8vbXlzb2Z0Y29ycC5pbiIsImlzcyI6Ik15c29mdCBjb3JwIiwic3ViIjoic29tZUB1c2VyLmNvbSJ9.Vq_sBN7nH67vMDbiJE01EP4hvJYE_5ju6izjkOX8pF5OS4g2RWKWpL6h6-b0tTkCzG4JD5BEl13LWW-Gxxw0i9vEK0FLg_kC_kZLYB8WuQ6B9B9YwzmZ3OLbgnYzt_VD7D-7psEbwapJl5hbFsIjDgOAEx-UCmjUcl2frZxZavG2LUiEGs9Ri7KqOZmTLgNDMWfeWh1t1LyD0_b-eTInbasVtKQxMlb5kR0Ln_Qg5092L-irJ7dqaZma7HItCnzXJROdqJEsMIBAYRwDGa_w5kIACeMOdU85QKtMHzOenYFkm6zh_s59ndziTctKMz196Y8AL08xuTi6d1gEWpM92A +--- response_body +uri: /uri +host: localhost +x-real-ip: 127.0.0.1 +--- error_code: 200