Skip to content

feat(openid-connect): make client_secret optional for local JWT verification modes#13472

Open
AlinsRan wants to merge 9 commits into
masterfrom
feat/oidc-optional-client-secret
Open

feat(openid-connect): make client_secret optional for local JWT verification modes#13472
AlinsRan wants to merge 9 commits into
masterfrom
feat/oidc-optional-client-secret

Conversation

@AlinsRan
Copy link
Copy Markdown
Contributor

@AlinsRan AlinsRan commented Jun 4, 2026

Background

Closes #10563
Closes #13397

In service-to-service scenarios where the gateway only validates an incoming Bearer token locally (via a configured public key or JWKS endpoint), client_secret plays no role — no call is made to the IdP's token or introspection endpoint. However, the plugin currently requires client_secret unconditionally, forcing users to supply a dummy value as a workaround.

Changes

apisix/plugins/openid-connect.lua

  • Remove client_secret from the schema's required array.

  • Add conditional enforcement in check_schema: client_secret is still required for all flows that need it (session/callback flow, introspection), but is now optional when:

    • bearer_only=true + public_key: local JWT verification with a configured public key
    • bearer_only=true + use_jwks=true: local JWT 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
  • Fix claim_schema not being enforced in the bearer-token path (openid-connect: claim_schema is not enforced in the bearer-token authentication path #13397): the schema is now applied directly to the flat JWT payload / introspection response in the bearer branch.

t/plugin/openid-connect.t

Add TEST 42–47 covering:

  • bearer_only=true + public_key → no client_secret required
  • bearer_only=true + use_jwks=true → no client_secret required
  • bearer_only=true + introspection endpoint (no local key) → client_secret still required
  • token_endpoint_auth_method=private_key_jwt → no client_secret required
  • use_pkce=true → no client_secret required
  • Session flow without special auth method → client_secret still required

Backward Compatibility

All existing configurations remain valid. The change only relaxes the requirement for specific scenarios; any config that previously worked continues to work unchanged.

@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. enhancement New feature or request labels Jun 4, 2026
…ication 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 <noreply@anthropic.com>
@AlinsRan AlinsRan force-pushed the feat/oidc-optional-client-secret branch from a222c37 to bdb5fa2 Compare June 4, 2026 09:05
AlinsRan and others added 7 commits June 4, 2026 17:18
…chema bearer tests

- Fix client_secret_optional logic: the private_key_jwt exemption for the
  token endpoint (non-bearer flow) was incorrectly applied to bearer mode.
  In bearer+introspection mode, client_secret is now only optional when
  introspection_endpoint_auth_method=private_key_jwt, not when
  token_endpoint_auth_method=private_key_jwt.

- Add TEST 47: bearer_only + introspection_endpoint_auth_method=private_key_jwt
  → client_secret optional.

- Add TEST 5-6 in openid-connect9.t: bearer_only + public_key + claim_schema
  that requires an absent field returns 401 with WWW-Authenticate header.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t in TEST 6

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rejection test

- Replace public key with a freshly generated RSA-512 key pair
- Replace JWT (which had line-wrap spaces and invalid signature) with a
  valid JWT signed by the corresponding private key
- Remove the location /hello override in TEST 6's --- config that was
  inadvertently bypassing APISIX routing, causing the plugin to never run

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Authorization header must be in --- more_headers, not in --- request block;
the latter is ignored by Test::Nginx for additional headers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…schema

Replace imperative check_schema Lua logic with declarative anyOf constraint.
Schema now requires either client_secret or an alternative auth mechanism
(public_key, use_jwks, use_pkce, or private_key_jwt method), making the
constraint visible to the control plane without custom validation code.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rror messages

Without 'required', JSON Schema 'properties' vacuously passes when the field
is absent, causing anyOf to always succeed. Add 'required' to each alternative
branch so absence of the field correctly fails the branch.

Update tests to expect the new anyOf failure message from lua-jsonschema.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@membphis
Copy link
Copy Markdown
Member

membphis commented Jun 5, 2026

[P2] The client_secret alternatives need to be scoped to the OIDC flow they actually replace.

The final schema-level anyOf accepts any of public_key, use_jwks, use_pkce, introspection_endpoint_auth_method=private_key_jwt, or token_endpoint_auth_method=private_key_jwt as a global replacement for client_secret. That is broader than the intended behavior: some alternatives only apply to bearer/local JWT verification, some only to bearer introspection, and some only to the non-bearer token endpoint flow.

For example, a bearer introspection config without client_secret can currently pass schema validation if it sets token_endpoint_auth_method=private_key_jwt, even though that field is not the auth method used for introspection. Similarly, a non-bearer session flow can be accepted via an alternative that is only meaningful for bearer validation.

Please either restore the mode-aware validation in check_schema, or express the condition in JSON Schema with bearer_only/flow-specific branches. It would also be good to add negative tests for cross-flow combinations, e.g. bearer_only=true + introspection + token_endpoint_auth_method=private_key_jwt without client_secret, and non-bearer session flow with an introspection-only alternative.

… flow

Revert the global anyOf schema constraint and restore mode-aware validation in
check_schema. The anyOf accepted any alternative (public_key, use_jwks, use_pkce,
private_key_jwt auth methods) as a global replacement for client_secret, but each
alternative only applies to a specific flow. For example a bearer introspection
config could pass by setting token_endpoint_auth_method=private_key_jwt, which is
not the auth method used for introspection.

Restore the bearer_only-aware check so each exemption is scoped to its flow, and
add negative tests for cross-flow combinations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@AlinsRan
Copy link
Copy Markdown
Contributor Author

AlinsRan commented Jun 5, 2026

@membphis Thanks for catching this. You're right that the global anyOf was too broad — it accepted cross-flow combinations where the alternative does not apply to the actual flow.

I've reverted the schema-level anyOf and restored the mode-aware validation in check_schema, scoping each exemption to its flow:

  • bearer_only=true: exempt only via public_key/use_jwks (local JWT verification) or introspection_endpoint_auth_method=private_key_jwt (introspection auth)
  • non-bearer (session/callback): exempt only via token_endpoint_auth_method=private_key_jwt or use_pkce

Also added negative tests for the cross-flow cases you mentioned:

  • bearer_only=true + introspection + token_endpoint_auth_method=private_key_jwt without client_secret → now rejected
  • non-bearer session flow with a bearer-only alternative (introspection private_key_jwt) without client_secret → now rejected

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

2 participants