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

Reply via email to