Skip to content

feat: support OIDC claim validator (#8772) #11824

New issue

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

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

Already on GitHub? # to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
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
35 changes: 35 additions & 0 deletions apisix/plugins/openid-connect.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ local core = require("apisix.core")
local ngx_re = require("ngx.re")
local openidc = require("resty.openidc")
local random = require("resty.random")
local jsonschema = require('jsonschema')
local string = string
local ngx = ngx
local ipairs = ipairs
local type = type
local pcall = pcall
local concat = table.concat

local ngx_encode_base64 = ngx.encode_base64
Expand Down Expand Up @@ -317,6 +319,11 @@ local schema = {
items = {
type = "string"
}
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to add documentation for this

claim_schema = {
description = "JSON schema of OIDC response claim",
type = "object",
default = nil,
}
},
encrypt_fields = {"client_secret", "client_rsa_private_key"},
Expand All @@ -331,6 +338,7 @@ local _M = {
schema = schema,
}

local claim_validator = nil;

function _M.check_schema(conf)
if conf.ssl_verify == "no" then
Expand All @@ -357,6 +365,14 @@ function _M.check_schema(conf)
return false, err
end

if conf.claim_schema then
local ok, res = pcall(jsonschema.generate_validator, conf.claim_schema)
if not ok then
return false, "generate claim_schema validator failed"
end
claim_validator = res
end

return true
end

Expand Down Expand Up @@ -528,6 +544,18 @@ local function required_scopes_present(required_scopes, http_scopes)
return true
end

local function validate_claims_in_oidcauth_response(resp)
if not claim_validator then
return true, nil
end
local data = {
user = resp.user,
access_token = resp.access_token,
id_token = resp.id_token,
}
return claim_validator(data)
end

function _M.rewrite(plugin_conf, ctx)
local conf = core.table.clone(plugin_conf)

Expand Down Expand Up @@ -682,6 +710,13 @@ function _M.rewrite(plugin_conf, ctx)
end

if response then
local ok, err = validate_claims_in_oidcauth_response( response)
if not ok then
core.log.error("OIDC claim validation failed: ", err)
ngx.header["WWW-Authenticate"] = 'Bearer realm="' .. conf.realm ..
'", error="invalid_token", error_description="' .. err .. '"'
return ngx.HTTP_UNAUTHORIZED
end
-- If the openidc module has returned a response, it may contain,
-- respectively, the access token, the ID token, the refresh token,
-- and the userinfo.
Expand Down
217 changes: 217 additions & 0 deletions t/plugin/openid-connect2.t
Original file line number Diff line number Diff line change
Expand Up @@ -401,3 +401,220 @@ passed
--- response_body
true
--- error_code: 302



=== TEST 11: Set up route with plugin matching URI `/*` and point plugin to local Keycloak instance and set claim validator.
--- 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,
"introspection_endpoint_auth_method": "client_secret_post",
"introspection_endpoint": "http://127.0.0.1:8080/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,
"set_refresh_token_header": true,
"claim_schema": {
"type": "object",
"properties": {
"access_token": { "type" : "string"},
"id_token": { "type" : "object"},
"user": { "type" : "object"}
},
"required" : ["access_token","id_token","user"]
}
}
},
"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 12: Access route w/o bearer token and go through the full OIDC Relying Party authentication process and validate claim successfully.
--- 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'])
-- Make the final call back to the original URI.
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
-- 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)
}
}
--- response_body_like
uri: /uri
cookie: .*
host: 127.0.0.1:1984
user-agent: .*
x-access-token: ey.*
x-id-token: ey.*
x-real-ip: 127.0.0.1
x-refresh-token: ey.*
x-userinfo: ey.*



=== TEST 13: Set up route with plugin matching URI `/*` and point plugin to local Keycloak instance and set claim validator with more strict schema.
--- 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,
"introspection_endpoint_auth_method": "client_secret_post",
"introspection_endpoint": "http://127.0.0.1:8080/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,
"set_refresh_token_header": true,
"claim_schema": {
"type": "object",
"properties": {
"access_token": { "type" : "string"},
"id_token": { "type" : "object"},
"user": { "type" : "object"},
"user1": { "type" : "object"}
},
"required" : ["access_token","id_token","user","user1"]
}
}
},
"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 14: Access route w/o bearer token and go through the full OIDC Relying Party authentication process and fail to validate claim.
--- 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'])
-- Make the final call back to the original URI.
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.say('here error',err)
-- No response, must be an error.
ngx.status = 500
ngx.say(err)
return
end

ngx.status = res.status
ngx.say(res.body)
}
}
--- error_code: 401
--- error_log
property "user1" is required
Loading