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 <[email protected]> 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 <[email protected]> --- 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
