This is an automated email from the ASF dual-hosted git repository.
nic-6443 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push:
new 64cd2df78 feat(openid-connect): add set_raw_id_token_header option
(#13616)
64cd2df78 is described below
commit 64cd2df78a83e7a4700606295444c263fdb2b282
Author: Luarx <[email protected]>
AuthorDate: Fri Jul 24 05:34:39 2026 +0200
feat(openid-connect): add set_raw_id_token_header option (#13616)
---
apisix/plugins/openid-connect.lua | 24 ++++++-
docs/en/latest/plugins/openid-connect.md | 3 +-
t/plugin/openid-connect.t | 110 ++++++++++++++++++++++++++++++-
3 files changed, 134 insertions(+), 3 deletions(-)
diff --git a/apisix/plugins/openid-connect.lua
b/apisix/plugins/openid-connect.lua
index 202cf7e2c..3cf05e0fd 100644
--- a/apisix/plugins/openid-connect.lua
+++ b/apisix/plugins/openid-connect.lua
@@ -322,6 +322,12 @@ local schema = {
type = "boolean",
default = true
},
+ set_raw_id_token_header = {
+ description = "Whether the raw signed ID token JWT should be added
in the " ..
+ "X-Raw-ID-Token header to the request for downstream.",
+ type = "boolean",
+ default = false
+ },
set_userinfo_header = {
description = "Whether the user info token should be added in the
X-Userinfo " ..
"header to the request for downstream.",
@@ -709,7 +715,7 @@ function _M.rewrite(plugin_conf, ctx)
local conf = core.table.clone(plugin_conf)
-- Snapshot the client-supplied X-Access-Token (it doubles as a bearer
- -- input via get_bearer_access_token) and clear the four headers this
+ -- input via get_bearer_access_token) and clear the five headers this
-- plugin advertises as outputs so client-supplied values cannot bleed
-- through to the upstream.
ctx.openid_connect_client_x_access_token = core.request.header(ctx,
"X-Access-Token")
@@ -717,6 +723,7 @@ function _M.rewrite(plugin_conf, ctx)
core.request.set_header(ctx, "X-Userinfo", nil)
core.request.set_header(ctx, "X-ID-Token", nil)
core.request.set_header(ctx, "X-Refresh-Token", nil)
+ core.request.set_header(ctx, "X-Raw-ID-Token", nil)
-- Previously, we multiply conf.timeout before storing it in etcd.
-- If the timeout is too large, we should not multiply it again.
@@ -860,6 +867,15 @@ function _M.rewrite(plugin_conf, ctx)
unauth_action = "deny"
end
+ -- When set_raw_id_token_header is enabled and the user has explicitly
restricted
+ -- session_contents, ensure enc_id_token is included so
session:get("enc_id_token")
+ -- returns the raw signed JWT. When session_contents is nil,
lua-resty-openidc stores
+ -- all session data by default (including enc_id_token), so no action
is needed.
+ if conf.set_raw_id_token_header and conf.session_contents then
+ conf.session_contents = core.table.clone(conf.session_contents)
+ conf.session_contents.enc_id_token = true
+ end
+
-- Authenticate the request. This will validate the access token if it
-- is stored in a sessions cookie, and also renew the token if
required.
-- If no token can be extracted, the response will redirect to the ID
@@ -934,6 +950,12 @@ function _M.rewrite(plugin_conf, ctx)
if refresh_token and conf.set_refresh_token_header then
core.request.set_header(ctx, "X-Refresh-Token", refresh_token)
end
+
+ -- Add X-Raw-ID-Token header, maybe.
+ local enc_id_token = session:get("enc_id_token")
+ if enc_id_token and conf.set_raw_id_token_header then
+ core.request.set_header(ctx, "X-Raw-ID-Token", enc_id_token)
+ end
end
end
if session then
diff --git a/docs/en/latest/plugins/openid-connect.md
b/docs/en/latest/plugins/openid-connect.md
index 5bb74efd3..99a89945f 100644
--- a/docs/en/latest/plugins/openid-connect.md
+++ b/docs/en/latest/plugins/openid-connect.md
@@ -63,7 +63,8 @@ The `openid-connect` Plugin supports the integration with
[OpenID Connect (OIDC)
| token_signing_alg_values_expected | string | False | | | Algorithm used for
signing JWT, such as `RS256`. |
| 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. |
+| 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. Note: this
header contains `base64(JSON(decoded_claims))` and carries no cryptographic
signature. |
+| set_raw_id_token_header | boolean | False | false | | If true and if the raw
signed ID token JWT is available, set the value in the `X-Raw-ID-Token` request
header. Unlike `X-ID-Token`, this header contains the original RS256-signed JWT
from the identity provider and can be verified against the provider's JWKS
endpoint. The plugin automatically persists the raw JWT in the session when
this option is enabled. |
| set_userinfo_header | boolean | False | true | | If true and if user info
data is available, set the value in the `X-Userinfo` request header. |
| set_refresh_token_header | boolean | False | false | | If true and if the
refresh token is available, set the value in the `X-Refresh-Token` request
header. |
| session | object | False | | | Session configuration used when `bearer_only`
is `false` and the Plugin uses Authorization Code flow. |
diff --git a/t/plugin/openid-connect.t b/t/plugin/openid-connect.t
index 312104c67..6241884a1 100644
--- a/t/plugin/openid-connect.t
+++ b/t/plugin/openid-connect.t
@@ -937,7 +937,7 @@ OIDC introspection failed: invalid token
}
}
--- response_body
-{"accept_none_alg":false,"accept_unsupported_alg":true,"access_token_expires_leeway":0,"access_token_in_authorization_header":false,"bearer_only":false,"client_id":"kbyuFDidLLm280LIwVFiazOqjO3ty8KH","client_jwt_assertion_expires_in":60,"client_secret":"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa","discovery":"http://127.0.0.1:1980/.well-known/openid-configuration","force_reauthorize":false,"iat_slack":120,"introspection_endpoint_auth_method":"client_secret_basic","in
[...]
+{"accept_none_alg":false,"accept_unsupported_alg":true,"access_token_expires_leeway":0,"access_token_in_authorization_header":false,"bearer_only":false,"client_id":"kbyuFDidLLm280LIwVFiazOqjO3ty8KH","client_jwt_assertion_expires_in":60,"client_secret":"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa","discovery":"http://127.0.0.1:1980/.well-known/openid-configuration","force_reauthorize":false,"iat_slack":120,"introspection_endpoint_auth_method":"client_secret_basic","in
[...]
@@ -2049,3 +2049,111 @@ passed
--- timeout: 20
--- response_body
passed
+
+
+
+=== TEST 55: Configure plugin with set_raw_id_token_header enabled.
+--- 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:8080/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,
+ "set_access_token_header": false,
+ "set_id_token_header": false,
+ "set_userinfo_header": false,
+ "set_raw_id_token_header": true,
+ "session": {
+ "secret":
"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK"
+ }
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/*"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 56: Full OIDC login sets X-Raw-ID-Token with the raw signed JWT;
other auth headers are absent.
+--- config
+ location /t {
+ content_by_lua_block {
+ local http = require "resty.http"
+ local login_keycloak = require("lib.keycloak").login_keycloak
+ local concatenate_cookies =
require("lib.keycloak").concatenate_cookies
+
+ local httpc = http.new()
+
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/uri"
+ local res, err = login_keycloak(uri, "[email protected]", "123456")
+ if err then
+ ngx.status = 500
+ ngx.say(err)
+ return
+ end
+
+ local cookie_str = concatenate_cookies(res.headers['Set-Cookie'])
+ local redirect_uri = "http://127.0.0.1:" .. ngx.var.server_port ..
res.headers['Location']
+ res, err = httpc:request_uri(redirect_uri, {
+ method = "GET",
+ headers = {
+ ["Cookie"] = cookie_str
+ }
+ })
+
+ if not res then
+ ngx.status = 500
+ ngx.say(err)
+ return
+ elseif res.status ~= 200 then
+ ngx.status = 500
+ ngx.say("Invoking the original URI didn't return the expected
result.")
+ return
+ end
+
+ -- X-Raw-ID-Token must be present and contain a JWT (starts with
"ey").
+ if not res.body:find("x-raw-id-token: ey", 1, true) then
+ ngx.status = 500
+ ngx.say("expected x-raw-id-token header with a JWT value,
body: " .. res.body)
+ return
+ end
+
+ -- The other auth headers must be absent (set_*_header = false).
+ for _, unwanted in ipairs({"x-access-token:", "x-id-token:",
"x-userinfo:"}) do
+ if res.body:find(unwanted, 1, true) then
+ ngx.status = 500
+ ngx.say("unexpected header found: " .. unwanted)
+ return
+ end
+ end
+
+ ngx.say("passed")
+ }
+ }
+--- response_body
+passed