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

Reply via email to