Docs

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 port 8080, route ID keycloak-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-dev

Wait 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:

FieldValue
Usernamequickstart-user
Emailquickstart-user@example.com
First nameQuickstart
Last nameUser

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_SECRET

Keep 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_KEY

Troubleshoot

SymptomCauseFix
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).