perf(X-Pack): OIDC 认证设置 Realm非必填

This commit is contained in:
fit2cloud-chenyw 2024-07-11 16:48:00 +08:00
parent ff9669845b
commit 4c73a69cdb
3 changed files with 421 additions and 1 deletions

@ -1 +1 @@
Subproject commit d21daef323ca920d8f2ca467b5a69f018d30384b
Subproject commit 60f2ec78667120b19816a4aa7570fc861870d209

View File

@ -0,0 +1,419 @@
--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements. See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0
-- (the "License"); you may not use this file except in compliance with
-- the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
local core = require("apisix.core")
local ngx_re = require("ngx.re")
local openidc = require("resty.openidc")
local random = require("resty.random")
local string = string
local ngx = ngx
local ngx_encode_base64 = ngx.encode_base64
local plugin_name = "openid-connect"
local schema = {
type = "object",
properties = {
client_id = {type = "string"},
client_secret = {type = "string"},
discovery = {type = "string"},
scope = {
type = "string",
default = "openid",
},
ssl_verify = {
type = "boolean",
default = false,
},
timeout = {
type = "integer",
minimum = 1,
default = 3,
description = "timeout in seconds",
},
introspection_endpoint = {
type = "string"
},
introspection_endpoint_auth_method = {
type = "string",
default = "client_secret_basic"
},
bearer_only = {
type = "boolean",
default = false,
},
session = {
type = "object",
properties = {
secret = {
type = "string",
description = "the key used for the encrypt and HMAC calculation",
minLength = 16,
},
},
required = {"secret"},
additionalProperties = false,
},
realm = {
type = "string",
default = "",
},
logout_path = {
type = "string",
default = "/logout",
},
redirect_uri = {
type = "string",
description = "use ngx.var.request_uri if not configured"
},
post_logout_redirect_uri = {
type = "string",
description = "the URI will be redirect when request logout_path",
},
unauth_action = {
type = "string",
default = "auth",
enum = {"auth", "deny", "pass"},
description = "The action performed when client is not authorized. Use auth to " ..
"redirect user to identity provider, deny to respond with 401 Unauthorized, and " ..
"pass to allow the request regardless."
},
public_key = {type = "string"},
token_signing_alg_values_expected = {type = "string"},
use_pkce = {
description = "when set to true the PKEC(Proof Key for Code Exchange) will be used.",
type = "boolean",
default = false
},
set_access_token_header = {
description = "Whether the access token should be added as a header to the request " ..
"for downstream",
type = "boolean",
default = true
},
access_token_in_authorization_header = {
description = "Whether the access token should be added in the Authorization " ..
"header as opposed to the X-Access-Token header.",
type = "boolean",
default = false
},
set_id_token_header = {
description = "Whether the ID token should be added in the X-ID-Token header to " ..
"the request for downstream.",
type = "boolean",
default = true
},
set_userinfo_header = {
description = "Whether the user info token should be added in the X-Userinfo " ..
"header to the request for downstream.",
type = "boolean",
default = true
},
set_refresh_token_header = {
description = "Whether the refresh token should be added in the X-Refresh-Token " ..
"header to the request for downstream.",
type = "boolean",
default = false
},
proxy_opts = {
description = "HTTP proxy server be used to access identity server.",
type = "object",
properties = {
http_proxy = {
type = "string",
description = "HTTP proxy like: http://proxy-server:80.",
},
https_proxy = {
type = "string",
description = "HTTPS proxy like: http://proxy-server:80.",
},
http_proxy_authorization = {
type = "string",
description = "Basic [base64 username:password].",
},
https_proxy_authorization = {
type = "string",
description = "Basic [base64 username:password].",
},
no_proxy = {
type = "string",
description = "Comma separated list of hosts that should not be proxied.",
}
},
}
},
encrypt_fields = {"client_secret"},
required = {"client_id", "client_secret", "discovery"}
}
local _M = {
version = 0.2,
priority = 2599,
name = plugin_name,
schema = schema,
}
function _M.check_schema(conf)
if conf.ssl_verify == "no" then
-- we used to set 'ssl_verify' to "no"
conf.ssl_verify = false
end
if not conf.bearer_only and not conf.session then
core.log.warn("when bearer_only = false, " ..
"you'd better complete the session configuration manually")
conf.session = {
-- generate a secret when bearer_only = false and no secret is configured
secret = ngx_encode_base64(random.bytes(32, true) or random.bytes(32))
}
end
local ok, err = core.schema.check(schema, conf)
if not ok then
return false, err
end
return true
end
local function get_bearer_access_token(ctx)
-- Get Authorization header, maybe.
local auth_header = core.request.header(ctx, "Authorization")
if not auth_header then
-- No Authorization header, get X-Access-Token header, maybe.
local access_token_header = core.request.header(ctx, "X-Access-Token")
if not access_token_header then
-- No X-Access-Token header neither.
return false, nil, nil
end
-- Return extracted header value.
return true, access_token_header, nil
end
-- Check format of Authorization header.
local res, err = ngx_re.split(auth_header, " ", nil, nil, 2)
if not res then
-- No result was returned.
return false, nil, err
elseif #res < 2 then
-- Header doesn't split into enough tokens.
return false, nil, "Invalid Authorization header format."
end
if string.lower(res[1]) == "bearer" then
-- Return extracted token.
return true, res[2], nil
end
return false, nil, nil
end
local function introspect(ctx, conf)
-- Extract token, maybe.
local has_token, token, err = get_bearer_access_token(ctx)
if err then
return ngx.HTTP_BAD_REQUEST, err, nil, nil
end
if not has_token then
-- Could not find token.
if conf.bearer_only then
-- Token strictly required in request.
ngx.header["WWW-Authenticate"] = 'Bearer realm="' .. conf.realm .. '"'
return ngx.HTTP_UNAUTHORIZED, "No bearer token found in request.", nil, nil
else
-- Return empty result.
return nil, nil, nil, nil
end
end
-- If we get here, token was found in request.
if conf.public_key or conf.use_jwks then
-- Validate token against public key or jwks document of the oidc provider.
-- TODO: In the called method, the openidc module will try to extract
-- the token by itself again -- from a request header or session cookie.
-- It is inefficient that we also need to extract it (just from headers)
-- so we can add it in the configured header. Find a way to use openidc
-- module's internal methods to extract the token.
local res, err = openidc.bearer_jwt_verify(conf)
if err then
-- Error while validating or token invalid.
ngx.header["WWW-Authenticate"] = 'Bearer realm="' .. conf.realm ..
'", error="invalid_token", error_description="' .. err .. '"'
return ngx.HTTP_UNAUTHORIZED, err, nil, nil
end
-- Token successfully validated.
local method = (conf.public_key and "public_key") or (conf.use_jwks and "jwks")
core.log.debug("token validate successfully by ", method)
return res, err, token, res
else
-- Validate token against introspection endpoint.
-- TODO: Same as above for public key validation.
local res, err = openidc.introspect(conf)
if err then
ngx.header["WWW-Authenticate"] = 'Bearer realm="' .. conf.realm ..
'", error="invalid_token", error_description="' .. err .. '"'
return ngx.HTTP_UNAUTHORIZED, err, nil, nil
end
-- Token successfully validated and response from the introspection
-- endpoint contains the userinfo.
core.log.debug("token validate successfully by introspection")
return res, err, token, res
end
end
local function add_access_token_header(ctx, conf, token)
if token then
-- Add Authorization or X-Access-Token header, respectively, if not already set.
if conf.set_access_token_header then
if conf.access_token_in_authorization_header then
if not core.request.header(ctx, "Authorization") then
-- Add Authorization header.
core.request.set_header(ctx, "Authorization", "Bearer " .. token)
end
else
if not core.request.header(ctx, "X-Access-Token") then
-- Add X-Access-Token header.
core.request.set_header(ctx, "X-Access-Token", token)
end
end
end
end
end
function _M.rewrite(plugin_conf, ctx)
local conf = core.table.clone(plugin_conf)
-- Previously, we multiply conf.timeout before storing it in etcd.
-- If the timeout is too large, we should not multiply it again.
if not (conf.timeout >= 1000 and conf.timeout % 1000 == 0) then
conf.timeout = conf.timeout * 1000
end
if not conf.redirect_uri then
conf.redirect_uri = ctx.var.request_uri
end
if not conf.ssl_verify then
-- openidc use "no" to disable ssl verification
conf.ssl_verify = "no"
end
local response, err, session, _
if conf.bearer_only or conf.introspection_endpoint or conf.public_key then
-- An introspection endpoint or a public key has been configured. Try to
-- validate the access token from the request, if it is present in a
-- request header. Otherwise, return a nil response. See below for
-- handling of the case where the access token is stored in a session cookie.
local access_token, userinfo
response, err, access_token, userinfo = introspect(ctx, conf)
if err then
-- Error while validating token or invalid token.
core.log.error("OIDC introspection failed: ", err)
return response
end
if response then
-- Add configured access token header, maybe.
add_access_token_header(ctx, conf, access_token)
if userinfo and conf.set_userinfo_header then
-- Set X-Userinfo header to introspection endpoint response.
core.request.set_header(ctx, "X-Userinfo",
ngx_encode_base64(core.json.encode(userinfo)))
end
end
end
if not response then
-- Either token validation via introspection endpoint or public key is
-- not configured, and/or token could not be extracted from the request.
local unauth_action = conf.unauth_action
if unauth_action ~= "auth" then
unauth_action = "deny"
end
-- Authenticate the request. This will validate the access token if it
-- is stored in a session cookie, and also renew the token if required.
-- If no token can be extracted, the response will redirect to the ID
-- provider's authorization endpoint to initiate the Relying Party flow.
-- This code path also handles when the ID provider then redirects to
-- the configured redirect URI after successful authentication.
response, err, _, session = openidc.authenticate(conf, nil, unauth_action, conf.session)
if err then
if err == "unauthorized request" then
if conf.unauth_action == "pass" then
return nil
end
return 401
end
core.log.error("OIDC authentication failed: ", err)
return 500
end
if response then
-- If the openidc module has returned a response, it may contain,
-- respectively, the access token, the ID token, the refresh token,
-- and the userinfo.
-- Add respective headers to the request, if so configured.
-- Add configured access token header, maybe.
add_access_token_header(ctx, conf, response.access_token)
-- Add X-ID-Token header, maybe.
if response.id_token and conf.set_id_token_header then
local token = core.json.encode(response.id_token)
core.request.set_header(ctx, "X-ID-Token", ngx.encode_base64(token))
end
-- Add X-Userinfo header, maybe.
if response.user and conf.set_userinfo_header then
core.request.set_header(ctx, "X-Userinfo",
ngx_encode_base64(core.json.encode(response.user)))
end
-- Add X-Refresh-Token header, maybe.
if session.data.refresh_token and conf.set_refresh_token_header then
core.request.set_header(ctx, "X-Refresh-Token", session.data.refresh_token)
end
end
end
end
return _M

View File

@ -22,6 +22,7 @@ services:
- ${DE_BASE}/dataease2.0/apisix/apisix_conf/config.yaml:/usr/local/apisix/conf/config.yaml:ro
- ${DE_BASE}/dataease2.0/apisix/logs:/usr/local/apisix/logs
- ${DE_BASE}/dataease2.0/apisix/plugins/cas-auth.lua:/usr/local/apisix/apisix/plugins/cas-auth.lua
- ${DE_BASE}/dataease2.0/apisix/plugins/openid-connect.lua:/usr/local/apisix/apisix/plugins/openid-connect.lua
depends_on:
- etcd
ports: