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:34 UTC

(apisix) branch feat/oidc-optional-client-secret created (now a222c3760)

This is an automated email from the ASF dual-hosted git repository.

AlinsRan pushed a change to branch feat/oidc-optional-client-secret
in repository https://gitbox.apache.org/repos/asf/apisix.git


      at a222c3760 feat(openid-connect): make client_secret optional for local JWT verification modes

This branch includes the following new commits:

     new a222c3760 feat(openid-connect): make client_secret optional for local JWT verification modes

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(apisix) 01/01: feat(openid-connect): make client_secret optional for local JWT verification modes

Posted by al...@apache.org.
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