Kong 3.9 OIDC plugin based on revomatico/kong-oidc, with Keycloak compatibility fixes and enhancements. zmartzone/lua-resty-openidc is vendored and patched directly for fixes that cannot be applied at the plugin layer.
Implements OpenID Connect Authorization Code flow as a Kong plugin. Authenticates requests against an OIDC provider (Keycloak, Auth0, etc.) and forwards user identity to upstream services via HTTP headers.
Upstream headers set on authenticated requests:
| Header | Content |
|---|---|
X-Userinfo |
Base64-encoded userinfo JSON |
X-Access-Token |
Raw JWT string (or Bearer <token> if access_token_as_bearer=yes) |
X-ID-Token |
Base64-encoded ID token (with sig) |
X-Credential-Identifier |
sub claim from userinfo/ID token |
Groups are extracted from the token (default claim: groups) and set in kong.ctx.shared.authenticated_groups.
A pre-built image is available on Docker Hub: cnapcloud/kong-oidc:3.9.1
# Build image
make docker-build
# Quick rebuild after source changes
make docker-build-quick
# Publish multi-arch image to Docker Hub
make publishmake docker-build-quick
make testmake docker-build-quick
# host LAN IP — Kong container needs this to reach Keycloak
export IP=<host-lan-ip>
./bin/build-env.sh
# OIDC test: http://localhost:8000/httpbin (login: admin/password)
# Kong Manager: http://localhost:8002
# Teardown
./bin/teardown-env.shkong:
image: kong_oidc${KONG_TAG}
ports:
- 8000:8000
- 8443:8443
- 8001:8001
- 8002:8002
- 8444:8444
environment:
KONG_PLUGINS: bundled,oidc,cookies-to-headers
KONG_X_SESSION_STORAGE: redis
KONG_X_SESSION_REDIS_HOST: redis
KONG_X_SESSION_REDIS_PASSWORD: redis
KONG_X_SESSION_COMPRESSOR: zlib
KONG_NGINX_LARGE_CLIENT_HEADER_BUFFERS: "4 16k"
KONG_DATABASE: postgres
KONG_PG_HOST: postgres_db
KONG_PG_DATABASE: ${KONG_DB_NAME}
KONG_PG_USER: ${KONG_DB_USER}
KONG_PG_PASSWORD: ${KONG_DB_PW}
KONG_ADMIN_LISTEN: 0.0.0.0:${KONG_HTTP_ADMIN_PORT}
KONG_PROXY_LISTEN: 0.0.0.0:${KONG_HTTP_PROXY_PORT}
depends_on:
- postgres_db
- redisFull example:
test/integration/docker-compose.yml
curl -X POST http://localhost:8001/services/<service_id>/plugins \
-d "name=oidc" \
-d "config.client_id=kong-oidc" \
-d "config.client_secret=<secret>" \
-d "config.discovery=https://<oidc_provider>/.well-known/openid-configuration"curl -X POST http://localhost:8001/plugins \
-d "name=oidc" \
-d "config.client_id=kong-oidc" \
-d "config.client_secret=<secret>" \
-d "config.discovery=https://<oidc_provider>/.well-known/openid-configuration"| Parameter | Default | Required | Description |
|---|---|---|---|
config.client_id |
yes | OIDC Client ID | |
config.client_secret |
yes | OIDC Client secret | |
config.discovery |
https://.../.well-known/openid-configuration |
no | OIDC Discovery endpoint |
config.scope |
openid |
no | OAuth2 scope (must include openid) |
config.ssl_verify |
false |
no | Verify SSL cert of OIDC provider |
config.session_secret |
no | Secret for encrypting session cookie | |
config.introspection_endpoint |
no | Token introspection endpoint | |
config.introspection_endpoint_auth_method |
client_secret_basic |
no | Introspection auth method (client_secret_basic or client_secret_post) |
config.timeout |
no | Timeout for OIDC endpoint calls | |
config.bearer_only |
no |
no | Introspect tokens only, no redirect |
config.realm |
kong |
no | Realm for WWW-Authenticate header |
config.logout_path |
/logout |
no | Path to trigger OIDC logout |
config.unauth_action |
auth |
no | auth = redirect to login, deny = return 401 |
config.recovery_page_path |
no | Redirect path on error (use / to redirect home silently) |
|
config.redirect_uri |
no | URI the OP redirects to after authentication | |
config.ignore_auth_filters |
no | Comma-separated list of paths to bypass auth | |
config.userinfo_header_name |
X-Userinfo |
no | Upstream header for userinfo |
config.id_token_header_name |
X-ID-Token |
no | Upstream header for ID token |
config.access_token_header_name |
X-Access-Token |
no | Upstream header for access token |
config.access_token_as_bearer |
no |
no | Pass access token as Authorization: Bearer |
config.disable_userinfo_header |
no |
no | Disable forwarding userinfo header |
config.disable_id_token_header |
no |
no | Disable forwarding ID token header |
config.disable_access_token_header |
no |
no | Disable forwarding access token header |
config.groups_claim |
groups |
no | Token claim name to read groups from |
config.skip_already_auth_requests |
no |
no | Skip if credentials already set by a higher-priority plugin |
config.bearer_jwt_auth_enable |
no |
no | Validate JWT in Authorization: Bearer header |
config.bearer_jwt_auth_allowed_auds |
no | Allowed aud values for JWT validation (defaults to client_id) |
|
config.bearer_jwt_auth_signing_algs |
['RS256'] |
no | Allowed signing algorithms for JWT validation |
config.header_names |
no | Custom upstream header names mapped from claims (paired with header_claims) |
|
config.header_claims |
no | Claim names to map to custom headers (paired with header_names) |
|
config.http_proxy |
no | HTTP proxy URL | |
config.https_proxy |
no | HTTPS proxy URL (must use http:// scheme) |
|
config.idletime |
900 |
no | Session idle timeout in seconds |
config.lifetime |
3600 |
no | Session max lifetime in seconds |
config.renew |
600 |
no | Session renewal window in seconds |
To pass access token as a Bearer token:
config.access_token_header_name = Authorization
config.access_token_as_bearer = yes
Session name is set per-service: <route_id>_<plugin_name>_session
Set KONG_X_SESSION_STORAGE to one of: cookie (default), shm, memcache, redis, dshm
KONG_X_SESSION_COMPRESSOR=zlib # reduce cookie size
KONG_NGINX_LARGE_CLIENT_HEADER_BUFFERS='4 16k' # if cookie is too large
KONG_X_SESSION_STORAGE=shm
KONG_X_SESSION_SHM_STORE=oidc_sessions # default
KONG_X_SESSION_SHM_STORE_SIZE=5m # default
KONG_X_SESSION_STORAGE=memcache
KONG_X_SESSION_MEMCACHE_HOST=memcached # default
KONG_X_SESSION_MEMCACHE_PORT="'11211'" # note: numeric values must be double-quoted
KONG_X_SESSION_STORAGE=redis
KONG_X_SESSION_REDIS_HOST=redis
KONG_X_SESSION_REDIS_PASSWORD=<password>
KONG_X_SESSION_STORAGE=dshm
KONG_X_SESSION_DSHM_HOST=hazelcast # default
KONG_X_SESSION_DSHM_PORT=4321 # default
KONG_X_SESSION_DSHM_REGION=oidc_sessions # default
| Variable | Description |
|---|---|
KONG_X_SESSION_SECRET |
Session encryption secret (required). Set in nginx_kong.lua at build time |
KONG_X_SESSION_NAME |
Session cookie name (default: oidc_session) |
KONG_PLUGINS |
Enable plugins: bundled,oidc,cookies-to-headers |
KONG_X_NOLOG_LIST_FILE |
Path to file listing IPs to exclude from access log |
Numeric env vars must be double-quoted:
KONG_X_VAR="'1234'"
Exclude IPs from access log (KONG_X_NOLOG_LIST_FILE=/tmp/nolog.txt):
127.0.0.1 0;
Fixes and enhancements made while testing against Keycloak 26.5.
Extended from revomatico/kong-oidc with the following changes:
- Added:
store_enc_id_token = trueoption for proper logout support - Changed:
revoke_tokens_on_logoutdefault toyes - Changed:
logout_pathandredirect_after_logout_uriare now supported - Changed: session name is now scoped per-service:
<route_id>_<plugin_name>_session - Added:
idletime(default 900s),lifetime(default 3600s),renew(default 600s) config options - Added: session storage variables to
kong/templates/nginx_kong.lua - Changed: ID token header now forwards the original signed JWT (
header.payload.signature) instead of the re-encoded payload JSON (which has no signature)
zmartzone/lua-resty-openidc is vendored directly into resty/openidc.lua to apply the following patches that cannot be made via the plugin layer alone:
- Fixed:
end_session_endpointmissing from Keycloak discovery response — auto-fallback to<issuer>/protocol/openid-connect/logout - Fixed: JSON parse failure after Keycloak endpoint discovery that caused opts not being applied
- Removed redundant access token revocation on logout (Keycloak warns when access token is revoked after refresh token)
- Fixed: logout always uses
end_session_endpointwhen present, ignoringredirect_after_logout_with_id_token_hint - Fixed: browser back button after auth caused 500 error (stale
/cbwith no session state) — now redirects gracefully to/ - Fixed:
/cbURL added to browser history after auth — replaced HTTP 302 redirect withwindow.location.replace()so the callback URL is not recorded in history
- Added
Dockerfile-quick(make docker-build-quick) for fast rebuilds during development - Integration tests now use the locally built image via docker-compose