Set Up SSO with Keycloak
Configure Apache APISIX with Keycloak for browser-based single sign-on using the OpenID Connect Authorization Code flow with PKCE.
Applies to:Apache APISIX 3.18.0
Roles:Platform engineerSecurity engineerTime:30 minutesImpact:No restart, no traffic change
Goal
A route that redirects unauthenticated browser requests to Keycloak, completes the OpenID Connect (OIDC) Authorization Code flow with Proof Key for Code Exchange (PKCE), keeps tokens in an encrypted APISIX session, and removes authentication data before proxying upstream.
Before you begin
- You have Docker, cURL, jq, and OpenSSL.
- You have an Apache APISIX installation. Get APISIX installs APISIX in Docker or Kubernetes.
- Your browser can reach
http://localhost:9080, and the browser and APISIX runtime can reach one common hostname for the local Keycloak server. - The container name
apisix-keycloak, TCP port8080, route IDkeycloak-sso, and path/anything/user/*are unused.
Confirm that the container name is unused:
docker ps -a --filter 'name=^/apisix-keycloak$' --format '{{.Names}}'Expected: no output. If the name or port is already in use, stop and choose isolated values rather than replacing an existing container.
Set the APISIX variables and verify the route is unused:
export APISIX_ADMIN_URL="http://127.0.0.1:9180"
export GATEWAY_URL="http://localhost:9080"
export SSO_CALLBACK_URL="${GATEWAY_URL}/anything/user/callback"
read -rsp "Admin API key: " APISIX_ADMIN_KEY && export APISIX_ADMIN_KEY && echo
curl -s -o /dev/null -w '%{http_code}\n' \
"${APISIX_ADMIN_URL}/apisix/admin/routes/keycloak-sso" \
-H "X-API-KEY: ${APISIX_ADMIN_KEY}"
curl -s "${APISIX_ADMIN_URL}/apisix/admin/routes" \
-H "X-API-KEY: ${APISIX_ADMIN_KEY}" | \
jq -r '.list[] | select(.value.uri == "/anything/user/*") | .key'Expected: 404 from the route lookup and no output from the path lookup. A 200 or listed key means a collision; stop and choose a unique route ID and path.
Steps
Five steps start Keycloak, create an isolated realm and user, register APISIX, capture the OIDC settings, and create the protected route.
Step 1: Start Keycloak
Start the fixed Keycloak version used by this guide in development mode:
docker run -d --name apisix-keycloak \
-e 'KC_BOOTSTRAP_ADMIN_USERNAME=quickstart-admin' \
-e 'KC_BOOTSTRAP_ADMIN_PASSWORD=quickstart-admin-pass' \
-p 8080:8080 \
quay.io/keycloak/keycloak:26.7.3 start-devWait until http://localhost:8080/admin/ opens, then sign in as quickstart-admin with password quickstart-admin-pass.
Development mode, HTTP, and the example administrator credential are for this isolated local exercise only. Use TLS, a production database, and managed credentials for a real deployment.
Expected: the Keycloak Admin Console opens and accepts the temporary administrator.
What changed: a disposable Keycloak 26.7.3 development server is listening on local port 8080.
Step 2: Create the realm and test user
Select Manage realms → Create realm, name the realm quickstart-realm, and create it. In that realm, select Users → Create new user and enter:
| Field | Value |
|---|---|
| Username | quickstart-user |
quickstart-user@example.com | |
| First name | Quickstart |
| Last name | User |
Create the user. On its Credentials tab, set the password to quickstart-user-pass, turn off Temporary, and confirm the change.
Expected: the isolated realm contains one enabled user with a permanent test password.
What changed: the guide has a separate security domain and account for browser sign-in.
Step 3: Register APISIX as an OIDC client
Select Clients → Create client, keep Client type as OpenID Connect, enter apisix-quickstart-client, and continue. Enable Client authentication and Standard flow, leave other authentication flows off, and continue.
Add the exact value of ${SSO_CALLBACK_URL}—http://localhost:9080/anything/user/callback for this guide—to Valid redirect URIs. Save, enable Require PKCE in Capability config, select S256, and save again.
Expected: the confidential client enables only Standard flow, requires S256 PKCE, and has the exact APISIX callback URI.
What changed: Keycloak can return an authorization code to APISIX and requires the matching PKCE verifier during code exchange.
Step 4: Save the OIDC configuration
The browser and APISIX runtime must use the same reachable Keycloak hostname. Enter that host without a scheme or port, then capture the client secret from Clients → apisix-quickstart-client → Credentials:
read -rp "Keycloak host reachable by the browser and APISIX: " KEYCLOAK_HOST && export KEYCLOAK_HOST
export KEYCLOAK_CLIENT_ID="apisix-quickstart-client"
read -rsp "Keycloak client secret: " KEYCLOAK_CLIENT_SECRET && export KEYCLOAK_CLIENT_SECRET && echo
export KEYCLOAK_DISCOVERY="http://${KEYCLOAK_HOST}:8080/realms/quickstart-realm/.well-known/openid-configuration"
APISIX_SESSION_SECRET="$(openssl rand -hex 32)" && export APISIX_SESSION_SECRETKeep the client and session secrets out of shell history, source control, logs, and screenshots. Confirm that Keycloak publishes authorization and token endpoints at the shared hostname:
curl -sS "${KEYCLOAK_DISCOVERY}" | jq -er '.authorization_endpoint, .token_endpoint'Expected: two URLs whose host matches ${KEYCLOAK_HOST}.
What changed: the shell holds the confidential-client configuration and a fresh 256-bit secret for encrypting and authenticating the APISIX session cookie.
Step 5: Create the protected route
Create a route that starts browser authentication, uses PKCE, and strips credentials before proxying to httpbin.org:
curl -s "${APISIX_ADMIN_URL}/apisix/admin/routes/keycloak-sso" -X PUT \
-H "X-API-KEY: ${APISIX_ADMIN_KEY}" \
-H "Content-Type: application/json" \
--data-binary @- <<EOF
{
"uri": "/anything/user/*",
"plugins": {
"openid-connect": {
"client_id": "${KEYCLOAK_CLIENT_ID}",
"client_secret": "${KEYCLOAK_CLIENT_SECRET}",
"discovery": "${KEYCLOAK_DISCOVERY}",
"redirect_uri": "${SSO_CALLBACK_URL}",
"bearer_only": false,
"use_pkce": true,
"scope": "openid profile email",
"session": {"secret": "${APISIX_SESSION_SECRET}"},
"set_access_token_header": false,
"set_id_token_header": false,
"set_userinfo_header": false
},
"proxy-rewrite": {
"headers": {"remove": ["Authorization", "Cookie"]}
}
},
"upstream": {
"type": "roundrobin",
"nodes": {"httpbin.org:80": 1}
}
}
EOF{ "value": { "id": "keycloak-sso", "uri": "/anything/user/*", "plugins": { "openid-connect": { "bearer_only": false, "use_pkce": true, … } }, … } }What changed: /anything/user/* now requires a Keycloak-backed browser session. APISIX removes the original authorization and cookie headers and does not add token or user-information headers upstream. If the upstream application needs its own cookies, narrow the Cookie removal policy before production use.
Validate
Open ${GATEWAY_URL}/anything/user/get in a private browser window. APISIX redirects the browser to Keycloak. Sign in as quickstart-user with password quickstart-user-pass.
Expected: Keycloak returns the browser to ${SSO_CALLBACK_URL}, APISIX resumes the original request, and httpbin returns a JSON response for /anything/user/get.
In the response's headers object, confirm that Authorization, Cookie, X-Access-Token, X-Id-Token, and X-Userinfo are absent. Reload the protected URL.
Expected: the reload succeeds through the APISIX session without another Keycloak sign-in prompt.
Roll back / Clean up
Delete the APISIX route before deleting the Keycloak client or realm:
curl -s -X DELETE "${APISIX_ADMIN_URL}/apisix/admin/routes/keycloak-sso" \
-H "X-API-KEY: ${APISIX_ADMIN_KEY}"Expected: the Admin API returns a successful deletion response, and the protected path no longer matches this route.
Delete quickstart-realm in Keycloak if the follow-on PAR and DPoP guide does not need it, then remove the disposable container and unset the secrets:
docker rm -f apisix-keycloak
unset KEYCLOAK_CLIENT_SECRET APISIX_SESSION_SECRET APISIX_ADMIN_KEYTroubleshoot
| Symptom | Cause | Fix |
|---|---|---|
| Keycloak rejects the redirect URI. | The client's Valid redirect URI and APISIX redirect_uri differ. | Copy ${SSO_CALLBACK_URL} exactly into Valid redirect URIs and recreate the route if its value changed. |
| The browser reaches Keycloak but APISIX cannot exchange the code. | APISIX cannot resolve the advertised Keycloak host, or the client secret is wrong. | Fetch ${KEYCLOAK_DISCOVERY} from the APISIX runtime and recopy the client secret. Use one hostname reachable by both browser and APISIX. |
| Keycloak rejects the code exchange because PKCE is missing or invalid. | The Keycloak client and APISIX route disagree about PKCE. | Require S256 on the client and keep use_pkce set to true on the route. |
| Sign-in completes but APISIX reports no session state. | The browser did not return the APISIX session cookie, or the gateway origin changed during the flow. | Use one consistent gateway origin, allow cookies for it, and restart the flow. See No Session State Found (opens in Plugin Hub docs). |
Related
- Authorize M2M Requests with Keycloak — protect service-to-service requests with Keycloak access tokens.
- Secure OIDC with PAR and DPoP — extend this exact realm and client with stronger authorization-request and token protections.
openid-connectplugin overview (opens in Plugin Hub docs) — understand the authorization-code flow and common failures.openid-connectplugin reference (opens in Plugin Hub docs) — session, redirect, PKCE, and upstream-header options.- Keycloak server administration — Keycloak's current realm, client, user, and PKCE guidance.