OIDC Integration in APISIX#
APISIX implements full OAuth 2.0 / OpenID Connect flows through the openid-connect plugin, which wraps the lua-resty-openidc library. It handles authorization code grant (browser-based SSO), client credentials grant (M2M), and password grant — with session management, token refresh, and logout built in .
The plugin requires three fields: client_id, client_secret, and discovery . The discovery URL points to the IdP's .well-known/openid-configuration endpoint, from which the plugin auto-discovers token, introspection, and JWKS endpoints.
Key Behavioral Modes#
The plugin operates in one of two modes depending on whether bearer_only is set:
| Mode | bearer_only | Use case | Token source |
|---|---|---|---|
| Full OIDC / session | false (default) | Browser SSO, auth code grant | Session cookie; redirects to IdP when absent |
| Bearer-only | true | APIs / M2M | Authorization: Bearer or X-Access-Token header; returns 401 when absent |
When bearer_only is false, the plugin calls openidc.authenticate() , which validates any session cookie and, if none is found, redirects the browser to the IdP's authorization endpoint. A session.secret (min 16 chars) is required for cookie encryption; APISIX auto-generates one with a warning if it is omitted .
When bearer_only is true or introspection_endpoint / public_key / use_jwks is set, the plugin calls the internal introspect() function , which routes validation to one of three paths:
- JWKS:
openidc.bearer_jwt_verify()— validates against the IdP's public key set . - Public key: same call path using a static
public_keyvalue . - Introspection endpoint:
openidc.introspect()— calls the IdP's introspection endpoint on each request .
Token Header Injection#
After successful validation the plugin injects upstream headers (all configurable, all defaulting to true or false) :
| Header | Default on? | Config key |
|---|---|---|
X-Access-Token | ✅ | set_access_token_header |
X-ID-Token | ✅ | set_id_token_header |
X-Userinfo | ✅ | set_userinfo_header |
X-Refresh-Token | ❌ | set_refresh_token_header |
Access token can alternatively be forwarded in the Authorization header via access_token_in_authorization_header: true .
Session & Token Lifecycle#
renew_access_token_on_expiry(defaulttrue) — silently renews the access token using the refresh token without re-authentication .refresh_session_interval— refresh the ID token without full re-auth .iat_slack(default 120s) — clock skew tolerance oniatclaim .jwk_expires_in(default 86400s) — JWK cache TTL .introspection_interval(default 0) — TTL for caching introspection results; 0 means no caching .revoke_tokens_on_logout(defaultfalse) — notifies the IdP when the user hitslogout_path.
The logout_path (default /logout) triggers logout; post_logout_redirect_uri controls where the user lands after logout .
Claim Validation#
claim_validator enables fine-grained JWT claim checks :
issuer.valid_issuers— allowlist of accepted issuers; falls back to the discovery doc'sissuer.audience.required— enforce presence of audience claim .audience.match_with_client_id— requireaudto include or equalclient_id.
required_scopes rejects tokens that don't carry every listed scope .
Keycloak Configuration Pattern#
For Keycloak, the discovery URL is http://<keycloak-host>/realms/<realm-name>/.well-known/openid-configuration .
Authorization code grant (browser SSO):
"openid-connect": {
"bearer_only": false,
"session": { "secret": "<min-16-char-key>" },
"client_id": "<keycloak-client-id>",
"client_secret": "<keycloak-client-secret>",
"discovery": "http://<keycloak-host>/realms/<realm>/.well-known/openid-configuration",
"scope": "openid profile",
"redirect_uri": "http://<apisix-host>/anything/callback"
}
Enable PKCE by adding "use_pkce": true . Keycloak must have the PKCE challenge method configured on the client.
Client credentials / M2M (JWKS validation):
"openid-connect": {
"bearer_only": false,
"use_jwks": true,
...
}
Client credentials / M2M (introspection):
"openid-connect": {
"bearer_only": true,
...
}
Breaking change (APISIX 3.16.0):
ssl_verifydefault changed fromfalsetotrue. Set"ssl_verify": falsewhen using Keycloak with self-signed certificates .
openid-connect vs jwt-auth#
openid-connect | jwt-auth | |
|---|---|---|
| Architecture | Stateful (session / IdP calls) | Stateless (local signature verification) |
| Token issuance | Handled by the IdP, APISIX acts as relying party | Tokens issued via APISIX's /apisix/plugin/jwt/sign endpoint |
| Consumer config | Plugin config on route/service | Consumer in etcd keyed by key claim |
| Revocation | Introspection or revoke_tokens_on_logout | No revocation (stateless) |
| SSO / login redirect | Yes (bearer_only: false) | No |
| Algorithms | Determined by IdP / JWKS | HS256, HS512, RS256, ES256 |
| Primary use case | Browser-facing SSO, delegated auth (OAuth/OIDC) | Internal/service APIs with pre-issued tokens |
Use openid-connect when APISIX is acting as an OIDC relying party (full login flow, SSO, Keycloak/Auth0/Okta integration). Use jwt-auth when you control token issuance yourself and need lightweight, no-IdP-call verification at the gateway.