This is an automated email from the ASF dual-hosted git repository.
AlinsRan 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 4cc1eaf3f feat(openid-connect): make client_secret optional for local
JWT verification modes (#13472)
4cc1eaf3f is described below
commit 4cc1eaf3f9ab78dba103d4c5d04d818085a68475
Author: AlinsRan <[email protected]>
AuthorDate: Mon Jun 8 17:39:25 2026 +0800
feat(openid-connect): make client_secret optional for local JWT
verification modes (#13472)
---
apisix/plugins/openid-connect.lua | 36 ++++++-
t/plugin/openid-connect.t | 212 ++++++++++++++++++++++++++++++++++++++
t/plugin/openid-connect9.t | 62 +++++++++++
3 files changed, 309 insertions(+), 1 deletion(-)
diff --git a/apisix/plugins/openid-connect.lua
b/apisix/plugins/openid-connect.lua
index 4a7bf53d4..0fa3379fa 100644
--- a/apisix/plugins/openid-connect.lua
+++ b/apisix/plugins/openid-connect.lua
@@ -403,7 +403,7 @@ local schema = {
},
encrypt_fields = {"client_secret", "client_rsa_private_key",
"session.secret", "session.redis.password"},
- required = {"client_id", "client_secret", "discovery"}
+ required = {"client_id", "discovery"}
}
@@ -424,6 +424,26 @@ function _M.check_schema(conf)
return false, "property \"session.secret\" is required when
\"bearer_only\" is false"
end
+ -- client_secret is not required in certain authentication modes. The
exemption
+ -- is scoped to the flow each alternative actually applies to:
+ -- bearer_only=true + public_key/use_jwks: local JWT verification, no
IdP call needed
+ -- bearer_only=true +
introspection_endpoint_auth_method=private_key_jwt: introspection
+ -- endpoint authenticates via signed JWT instead of client_secret
+ -- token_endpoint_auth_method=private_key_jwt (non-bearer): token
endpoint uses signed
+ -- JWT; this exemption applies only to the session/callback flow, not
bearer mode
+ -- use_pkce=true (non-bearer): public-client PKCE flow needs no
client_secret
+ local client_secret_optional
+ if conf.bearer_only then
+ client_secret_optional = (conf.public_key or conf.use_jwks)
+ or (conf.introspection_endpoint_auth_method == "private_key_jwt")
+ else
+ client_secret_optional = (conf.token_endpoint_auth_method ==
"private_key_jwt")
+ or conf.use_pkce
+ end
+ if not client_secret_optional and not conf.client_secret then
+ return false, "property \"client_secret\" is required"
+ end
+
local check = {"discovery", "introspection_endpoint", "redirect_uri",
"post_logout_redirect_uri", "proxy_opts.http_proxy",
"proxy_opts.https_proxy"}
core.utils.check_https(check, conf, plugin_name)
@@ -736,6 +756,20 @@ function _M.rewrite(plugin_conf, ctx)
end
end
+ -- Validate bearer-path claims against claim_schema when
configured.
+ -- The schema is applied directly to the flat JWT payload /
introspection
+ -- response, which is different from the session-flow structure
+ -- {user, access_token, id_token}.
+ if conf.claim_schema then
+ local ok, err = core.schema.check(conf.claim_schema, 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
+ end
+
-- Add configured access token header, maybe.
add_access_token_header(ctx, conf, access_token)
diff --git a/t/plugin/openid-connect.t b/t/plugin/openid-connect.t
index c018dc977..d133ed39e 100644
--- a/t/plugin/openid-connect.t
+++ b/t/plugin/openid-connect.t
@@ -1630,3 +1630,215 @@ token validate successfully by jwks
--- response_body
property "session.secret" is required when "bearer_only" is false
done
+
+
+
+=== TEST 42: client_secret is optional when bearer_only=true and public_key is
set.
+--- config
+ location /t {
+ content_by_lua_block {
+ local plugin = require("apisix.plugins.openid-connect")
+ local ok, err = plugin.check_schema({
+ client_id = "a",
+ discovery =
"https://example.com/.well-known/openid-configuration",
+ bearer_only = true,
+ public_key = "-----BEGIN PUBLIC
KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2a2rwplBQLzHPZe5TNJF\n-----END
PUBLIC KEY-----",
+ token_signing_alg_values_expected = "RS256",
+ })
+ if not ok then
+ ngx.say(err)
+ end
+ ngx.say("done")
+ }
+ }
+--- response_body
+done
+
+
+
+=== TEST 43: client_secret is optional when bearer_only=true and use_jwks=true.
+--- config
+ location /t {
+ content_by_lua_block {
+ local plugin = require("apisix.plugins.openid-connect")
+ local ok, err = plugin.check_schema({
+ client_id = "a",
+ discovery =
"https://example.com/.well-known/openid-configuration",
+ bearer_only = true,
+ use_jwks = true,
+ })
+ if not ok then
+ ngx.say(err)
+ end
+ ngx.say("done")
+ }
+ }
+--- response_body
+done
+
+
+
+=== TEST 44: client_secret is required when bearer_only=true but neither
public_key nor use_jwks is set (introspection mode).
+--- config
+ location /t {
+ content_by_lua_block {
+ local plugin = require("apisix.plugins.openid-connect")
+ local ok, err = plugin.check_schema({
+ client_id = "a",
+ discovery =
"https://example.com/.well-known/openid-configuration",
+ bearer_only = true,
+ introspection_endpoint = "https://example.com/introspect",
+ })
+ if not ok then
+ ngx.say(err)
+ end
+ ngx.say("done")
+ }
+ }
+--- response_body
+property "client_secret" is required
+done
+
+
+
+=== TEST 45: client_secret is optional when
token_endpoint_auth_method=private_key_jwt.
+--- config
+ location /t {
+ content_by_lua_block {
+ local plugin = require("apisix.plugins.openid-connect")
+ local ok, err = plugin.check_schema({
+ client_id = "a",
+ discovery =
"https://example.com/.well-known/openid-configuration",
+ bearer_only = false,
+ token_endpoint_auth_method = "private_key_jwt",
+ client_rsa_private_key = "-----BEGIN RSA PRIVATE
KEY-----\nMIIEowIBAAK\n-----END RSA PRIVATE KEY-----",
+ session = { secret = "jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK" },
+ })
+ if not ok then
+ ngx.say(err)
+ end
+ ngx.say("done")
+ }
+ }
+--- response_body
+done
+
+
+
+=== TEST 46: client_secret is optional when use_pkce=true (non-bearer PKCE
flow).
+--- config
+ location /t {
+ content_by_lua_block {
+ local plugin = require("apisix.plugins.openid-connect")
+ local ok, err = plugin.check_schema({
+ client_id = "a",
+ discovery =
"https://example.com/.well-known/openid-configuration",
+ bearer_only = false,
+ use_pkce = true,
+ session = { secret = "jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK" },
+ })
+ if not ok then
+ ngx.say(err)
+ end
+ ngx.say("done")
+ }
+ }
+--- response_body
+done
+
+
+
+=== TEST 47: client_secret is still required for non-bearer session flow
without special auth method.
+--- config
+ location /t {
+ content_by_lua_block {
+ local plugin = require("apisix.plugins.openid-connect")
+ local ok, err = plugin.check_schema({
+ client_id = "a",
+ discovery =
"https://example.com/.well-known/openid-configuration",
+ bearer_only = false,
+ session = { secret = "jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK" },
+ })
+ if not ok then
+ ngx.say(err)
+ end
+ ngx.say("done")
+ }
+ }
+--- response_body
+property "client_secret" is required
+done
+
+
+
+=== TEST 48: client_secret is optional when bearer_only=true and
introspection_endpoint_auth_method=private_key_jwt.
+--- config
+ location /t {
+ content_by_lua_block {
+ local plugin = require("apisix.plugins.openid-connect")
+ local ok, err = plugin.check_schema({
+ client_id = "a",
+ discovery =
"https://example.com/.well-known/openid-configuration",
+ bearer_only = true,
+ introspection_endpoint = "https://example.com/introspect",
+ introspection_endpoint_auth_method = "private_key_jwt",
+ client_rsa_private_key = "-----BEGIN RSA PRIVATE
KEY-----\nMIIEowIBAAK\n-----END RSA PRIVATE KEY-----",
+ })
+ if not ok then
+ ngx.say(err)
+ end
+ ngx.say("done")
+ }
+ }
+--- response_body
+done
+
+
+
+=== TEST 49: client_secret stays required for bearer introspection even with
token_endpoint_auth_method=private_key_jwt (cross-flow: that method does not
apply to introspection).
+--- config
+ location /t {
+ content_by_lua_block {
+ local plugin = require("apisix.plugins.openid-connect")
+ local ok, err = plugin.check_schema({
+ client_id = "a",
+ discovery =
"https://example.com/.well-known/openid-configuration",
+ bearer_only = true,
+ introspection_endpoint = "https://example.com/introspect",
+ token_endpoint_auth_method = "private_key_jwt",
+ client_rsa_private_key = "-----BEGIN RSA PRIVATE
KEY-----\nMIIEowIBAAK\n-----END RSA PRIVATE KEY-----",
+ })
+ if not ok then
+ ngx.say(err)
+ end
+ ngx.say("done")
+ }
+ }
+--- response_body
+property "client_secret" is required
+done
+
+
+
+=== TEST 50: client_secret stays required for non-bearer session flow with a
bearer-only alternative (introspection private_key_jwt does not apply here).
+--- config
+ location /t {
+ content_by_lua_block {
+ local plugin = require("apisix.plugins.openid-connect")
+ local ok, err = plugin.check_schema({
+ client_id = "a",
+ discovery =
"https://example.com/.well-known/openid-configuration",
+ bearer_only = false,
+ introspection_endpoint_auth_method = "private_key_jwt",
+ client_rsa_private_key = "-----BEGIN RSA PRIVATE
KEY-----\nMIIEowIBAAK\n-----END RSA PRIVATE KEY-----",
+ session = { secret = "jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK" },
+ })
+ if not ok then
+ ngx.say(err)
+ end
+ ngx.say("done")
+ }
+ }
+--- response_body
+property "client_secret" is required
+done
diff --git a/t/plugin/openid-connect9.t b/t/plugin/openid-connect9.t
index f6d6f5c7b..db3f5661d 100644
--- a/t/plugin/openid-connect9.t
+++ b/t/plugin/openid-connect9.t
@@ -190,3 +190,65 @@ GET /hello HTTP/1.1
Authorization: Bearer
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3NhbXBsZXMuYXV0aDAuY29tLyIsInN1YiI6InRlc3Qtc3ViamVjdCIsImF1ZCI6ImtieXVG
RGlkTExtMjgwTEl3VkZpYXpPcWpPM3R5OEtIIiwic2NvcGUiOiJhcGlzaXgiLCJpYXQiOjEwMDAwMDAwLCJleHAiOjI1MDAwMDAwMDB9.bfcZsd4ABgo0GoLT8EwfnKgf
AWbnJZbZ3kOtqyeSkXYqGlSmgMNW3q5Kx1SGjMNhEKVG_KrFfsPrQmcTljSPZA
--- response_body
success
+
+
+
+=== TEST 5: configure route with bearer_only + public_key + claim_schema that
requires an absent field
+--- 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": "kbyuFDidLLm280LIwVFiazOqjO3ty8KH",
+ "discovery":
"https://samples.auth0.com/.well-known/openid-configuration",
+ "ssl_verify": false,
+ "bearer_only": true,
+ "public_key": "-----BEGIN PUBLIC
KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAO6oZg+4sbTPa0oeKcfsJf2bx7N7JkGB\ngVqJeCkMHJ7lKLCTpg6P3UpTfNx5K+pKXsDucQbhjQqmjMwTBEe44EsCAwEAAQ==\n-----END
PUBLIC KEY-----",
+ "token_signing_alg_values_expected": "RS256",
+ "claim_schema": {
+ "type": "object",
+ "required": ["email"]
+ }
+ }
+ },
+ "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 6: bearer-path claim_schema rejection returns 401 with
WWW-Authenticate header
+--- request
+GET /hello HTTP/1.1
+--- more_headers
+Authorization: Bearer
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3NhbXBsZXMuYXV0aDAuY29tLyIsInN1YiI6InRlc3Qtc3ViamVjdCIsImF1ZCI6ImtieXVGRGlkTExtMjgwTEl3VkZpYXpPcWpPM3R5OEtIIiwic2NvcGUiOiJhcGlzaXgiLCJpYXQiOjEwMDAwMDAwLCJleHAiOjI1MDAwMDAwMDB9.yWPMyXHuhiBP3q0xUkg3Iwu8dvXWlaVGBqPC8y8hC1MYoCcj687X85o9mvw1Mz_kGgKHNvDYrl5EQ3B3LAM4OA
+--- error_code: 401
+--- response_headers_like
+WWW-Authenticate: Bearer realm="apisix", error="invalid_token".*
+--- no_error_log
+[crit]
+[alert]
+[emerg]
+--- grep_error_log eval
+qr/OIDC claim validation failed/
+--- grep_error_log_out
+OIDC claim validation failed