Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion apisix/plugins/openid-connect.lua
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,12 @@ local schema = {
type = "boolean",
default = true
},
set_enc_id_token_header = {
description = "Whether the raw signed ID token JWT should be added in the " ..
"X-Enc-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.",
Expand Down Expand Up @@ -703,14 +709,15 @@ 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")
core.request.set_header(ctx, "X-Access-Token", nil)
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-Enc-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.
Expand Down Expand Up @@ -854,6 +861,15 @@ function _M.rewrite(plugin_conf, ctx)
unauth_action = "deny"
end

-- When set_enc_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_enc_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
Expand Down Expand Up @@ -910,6 +926,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-Enc-ID-Token header, maybe.
local enc_id_token = session:get("enc_id_token")
if enc_id_token and conf.set_enc_id_token_header then
core.request.set_header(ctx, "X-Enc-ID-Token", enc_id_token)
end
end
end
if session then
Expand Down
3 changes: 2 additions & 1 deletion docs/en/latest/plugins/openid-connect.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_enc_id_token_header | boolean | False | false | | If true and if the raw signed ID token JWT is available, set the value in the `X-Enc-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. |
Expand Down
108 changes: 108 additions & 0 deletions t/plugin/openid-connect.t
Original file line number Diff line number Diff line change
Expand Up @@ -2049,3 +2049,111 @@ passed
--- timeout: 20
--- response_body
passed



=== TEST 55: Configure plugin with set_enc_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_enc_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-Enc-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, "teacher@gmail.com", "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-Enc-ID-Token must be present and contain a JWT (starts with "ey").
if not res.body:find("x-enc-id-token: ey", 1, true) then
ngx.status = 500
ngx.say("expected x-enc-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
Loading