Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(openid-connect): allow set headers in introspection request #11090

Merged
merged 10 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
31 changes: 29 additions & 2 deletions apisix/plugins/openid-connect.lua
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ local openidc = require("resty.openidc")
local random = require("resty.random")
local string = string
local ngx = ngx
local ipairs = ipairs
local concat = table.concat
local ipairs = ipairs
local pairs = pairs
local concat = table.concat

local ngx_encode_base64 = ngx.encode_base64

Expand Down Expand Up @@ -260,6 +261,19 @@ local schema = {
description = "Name of the expiry claim that controls the cached access token TTL.",
type = "string"
},
introspection_addon_headers = {
description = "Extra http headers in introspection",
type = "object",
minProperties = 1,
patternProperties = {
["^[^:]+$"] = {
oneOf = {
{ type = "string" },
{ type = "number" }
}
}
}
},
required_scopes = {
description = "List of scopes that are required to be granted to the access token",
type = "array",
Expand Down Expand Up @@ -386,7 +400,20 @@ local function introspect(ctx, conf)
else
-- Validate token against introspection endpoint.
-- TODO: Same as above for public key validation.
if conf.introspection_addon_headers then
-- http_request_decorator option provides by lua-resty-openidc
shreemaan-abhishek marked this conversation as resolved.
Show resolved Hide resolved
conf.http_request_decorator = function(req)
local h = req.headers or {}
for name, value in pairs(conf.introspection_addon_headers) do
h[name] = value
end
req.headers = h
return req
end
end

local res, err = openidc.introspect(conf)
conf.http_request_decorator = nil

if err then
ngx.header["WWW-Authenticate"] = 'Bearer realm="' .. conf.realm ..
Expand Down
1 change: 1 addition & 0 deletions docs/en/latest/plugins/openid-connect.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ description: OpenID Connect allows the client to obtain user information from th
| cache_segment | string | False | | | Optional name of a cache segment, used to separate and differentiate caches used by token introspection or JWT verification. |
| introspection_interval | integer | False | 0 | | TTL of the cached and introspected access token in seconds. |
| introspection_expiry_claim | string | False | | | Name of the expiry claim, which controls the TTL of the cached and introspected access token. The default value is 0, which means this option is not used and the plugin defaults to use the TTL passed by expiry claim defined in `introspection_expiry_claim`. If `introspection_interval` is larger than 0 and less than the TTL passed by expiry claim defined in `introspection_expiry_claim`, use `introspection_interval`. |
| introspection_addon_headers | object | False | | | Append extras headers to the introspection http request. |

NOTE: `encrypt_fields = {"client_secret"}` is also defined in the schema, which means that the field will be stored encrypted in etcd. See [encrypted storage fields](../plugin-develop.md#encrypted-storage-fields).

Expand Down
1 change: 1 addition & 0 deletions docs/zh/latest/plugins/openid-connect.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ description: OpenID Connect(OIDC)是基于 OAuth 2.0 的身份认证协议
| cache_segment | string | 否 | | | 可选的缓存段的名称,用于区分和区分用于令牌内省或 JWT 验证的缓存。 |
| introspection_interval | integer | 否 | 0 | | 以秒为单位的缓存和内省访问令牌的 TTL。 |
| introspection_expiry_claim | string | 否 | | | 过期声明的名称,用于控制缓存和内省访问令牌的 TTL。 |
| introspection_addon_headers | object | 否 | | | 添加额外的请求头到内省 HTTP 请求中。|

注意:schema 中还定义了 `encrypt_fields = {"client_secret"}`,这意味着该字段将会被加密存储在 etcd 中。具体参考 [加密存储字段](../plugin-develop.md#加密存储字段)。

Expand Down
210 changes: 210 additions & 0 deletions t/plugin/openid-connect6.t
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,213 @@ passed
}
--- response_body
passed



=== TEST 4: Update route with Keycloak introspection endpoint and introspection addon headers.
--- 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": "course_management",
"client_secret": "d1ec69e9-55d2-4109-a3ea-befa071579d5",
"discovery": "http://127.0.0.1:8080/realms/University/.well-known/openid-configuration",
"redirect_uri": "http://localhost:3000",
"ssl_verify": false,
"timeout": 10,
"bearer_only": true,
"realm": "University",
"introspection_endpoint_auth_method": "client_secret_post",
"introspection_endpoint": "http://127.0.0.1:8080/realms/University/protocol/openid-connect/token/introspect",
"introspection_addon_headers": {
"X-Addon-Header-A": "VALUE",
shreemaan-abhishek marked this conversation as resolved.
Show resolved Hide resolved
"X-Addon-Header-B": "value"
}
}
},
"upstream": {
"nodes": {
"127.0.0.1:1980": 1
},
"type": "roundrobin"
},
"uri": "/hello"
}]]
)

if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed



=== TEST 5: Obtain valid token and access route with it, introspection work as expected when configured extras headers.
--- 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()
local uri = "http://127.0.0.1:8080/realms/University/protocol/openid-connect/token"
local res, err = httpc:request_uri(uri, {
method = "POST",
body = "grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456",
headers = {
["Content-Type"] = "application/x-www-form-urlencoded"
}
})

-- 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",
headers = {
["Authorization"] = "Bearer " .. body["access_token"]
}
})

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
}
}
--- response_body
true
--- grep_error_log eval
qr/token validate successfully by \w+/
--- grep_error_log_out
token validate successfully by introspection
shreemaan-abhishek marked this conversation as resolved.
Show resolved Hide resolved



=== TEST 6: Access route with an invalid token, should work as expected too.
shreemaan-abhishek marked this conversation as resolved.
Show resolved Hide resolved
--- 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"
local res, err = httpc:request_uri(uri, {
method = "GET",
headers = {
["Authorization"] = "Bearer " .. "fake access token",
}
})

if res.status == 200 then
ngx.say(true)
else
ngx.say(false)
end
}
}
--- response_body
false
--- error_log
OIDC introspection failed: invalid token



=== TEST 7: Update route with fake Keycloak introspection endpoint and introspection addon headers
--- 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": "course_management",
"client_secret": "d1ec69e9-55d2-4109-a3ea-befa071579d5",
"discovery": "http://127.0.0.1:8080/realms/University/.well-known/openid-configuration",
"redirect_uri": "http://localhost:3000",
"ssl_verify": false,
"timeout": 10,
"bearer_only": true,
"realm": "University",
"introspection_endpoint_auth_method": "client_secret_post",
"introspection_endpoint": "http://127.0.0.1:1980/log_request",
"introspection_addon_headers": {
"X-Addon-Header-A": "VALUE",
"X-Addon-Header-B": "value"
}
}
},
"upstream": {
"nodes": {
"127.0.0.1:1980": 1
},
"type": "roundrobin"
},
"uri": "/hello"
}]]
)

if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed



=== TEST 8: Check http headers from fake introspection endpoint.
shreemaan-abhishek marked this conversation as resolved.
Show resolved Hide resolved
--- config
location /t {
content_by_lua_block {
local http = require "resty.http"
local httpc = http.new()
local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello"
local res, err = httpc:request_uri(uri, {
method = "GET",
headers = {
["Authorization"] = "Bearer " .. "fake access token"
}
})
ngx.status = res.status
}
}
--- error_code: 401
--- error_log
OIDC introspection failed: JSON decoding failed
--- grep_error_log eval
qr/x-addon-header.{9}/
--- grep_error_log_out
x-addon-header-a: VALUE
x-addon-header-b: value
Loading