You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by al...@apache.org on 2026/06/04 05:18:35 UTC
(apisix) 01/01: feat(openid-connect): make client_secret optional for local JWT verification modes
This is an automated email from the ASF dual-hosted git repository.
AlinsRan pushed a commit to branch feat/oidc-optional-client-secret
in repository https://gitbox.apache.org/repos/asf/apisix.git
commit a222c37602f1f9383aa32e7d4b2535fdfd757c37
Author: AlinsRan <al...@apache.org>
AuthorDate: Thu Jun 4 10:44:18 2026 +0800
feat(openid-connect): make client_secret optional for local JWT verification modes
In bearer_only + public_key or bearer_only + use_jwks scenarios, the
gateway verifies the token locally without contacting the IdP's token or
introspection endpoint, so client_secret plays no role. Remove it from
the schema's required list and enforce it conditionally in check_schema.
Exempt scenarios:
- bearer_only=true + public_key: local verification with configured public key
- bearer_only=true + use_jwks=true: local verification via JWKS endpoint
- token_endpoint_auth_method=private_key_jwt: RSA private key replaces client_secret
- use_pkce=true (non-bearer): public-client PKCE flow
Also fix claim_schema not being enforced in the bearer-token path
(#13397): apply the schema directly to the flat JWT payload /
introspection response in the bearer branch.
Closes #10563
Closes #13397
Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
apisix/plugins/openid-connect.lua | 29 +++++++-
t/plugin/openid-connect.t | 138 ++++++++++++++++++++++++++++++++++++++
2 files changed, 166 insertions(+), 1 deletion(-)
diff --git a/apisix/plugins/openid-connect.lua b/apisix/plugins/openid-connect.lua
index 3ecf9d246..3599bc888 100644
--- a/apisix/plugins/openid-connect.lua
+++ b/apisix/plugins/openid-connect.lua
@@ -402,7 +402,7 @@ local schema = {
}
},
encrypt_fields = {"client_secret", "client_rsa_private_key"},
- required = {"client_id", "client_secret", "discovery"}
+ required = {"client_id", "discovery"}
}
@@ -423,6 +423,19 @@ function _M.check_schema(conf)
return false, "property \"session.secret\" is required when \"bearer_only\" is false"
end
+ -- client_secret is not required in local JWT verification modes or when a different
+ -- auth method is used. Specifically:
+ -- bearer_only=true + public_key: verify JWT locally with the configured public key
+ -- bearer_only=true + use_jwks=true: verify JWT locally via the JWKS endpoint
+ -- token_endpoint_auth_method=private_key_jwt: signed JWT replaces client_secret
+ -- use_pkce=true (non-bearer): public-client PKCE flow needs no client_secret
+ local client_secret_optional = (conf.bearer_only and (conf.public_key or conf.use_jwks))
+ or (conf.token_endpoint_auth_method == "private_key_jwt")
+ or (conf.use_pkce and not conf.bearer_only)
+ 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)
@@ -735,6 +748,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..b83961f59 100644
--- a/t/plugin/openid-connect.t
+++ b/t/plugin/openid-connect.t
@@ -1630,3 +1630,141 @@ 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