Set Up SSO with Keycloak
OpenID Connect (OIDC) is a simple identity layer on top of the OAuth 2.0 protocol. It allows clients to verify the identity of end users based on the authentication performed by the identity provider, as well as to obtain basic profile information about end users in an interoperable and REST-like manner. With APISIX and Keycloak, you can implement OIDC-based authentication processes to protect your APIs and enable single sign-on (SSO).
Keycloak is an open-source identity and access management solution for modern applications and services. Keycloak supports single sign-on (SSO), which enables services to interface with Keycloak through protocols such as OIDC and OAuth 2.0. In addition, Keycloak also supports delegating authentication to third party identity providers such as Facebook and Google.
The guide will show you how to integrate APISIX with Keycloak using authorization code grant, client credentials grant, and password grant, using the openid-connect plugin.

Prerequisite(s)
- Install Docker.
- Install cURL to send requests to the services for validation.
- Follow the Getting Started tutorial to start a new APISIX instance in Docker or on Kubernetes.
Configure Keycloak
Start a Keycloak instance named apisix-quickstart-keycloak with the administrator name quickstart-admin and password quickstart-admin-pass in development mode:
docker run -d --name "apisix-quickstart-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.1 start-dev
Open http://localhost:8080/admin/ and sign in with the administrator username quickstart-admin and password quickstart-admin-pass. Keep the Admin Console session open for the following steps.
Create a Realm
Realms in Keycloak are workspaces for managing resources such as users, credentials, and roles. Resources in different realms are isolated from each other. Create a realm named quickstart-realm for APISIX:
- Select Manage realms in the left menu, then select Create realm.
- Enter
quickstart-realmin Realm name. - Select Create.

Create a Client
Clients in Keycloak are applications and services that request authentication. APISIX acts as a client when it starts an OIDC authentication flow. Create a client named apisix-quickstart-client:
-
Select Clients in the left menu, then select Create client.
-
Keep Client type set to OpenID Connect, enter
apisix-quickstart-clientin Client ID, and select Next.
-
In Capability config, turn on Client authentication. Keep Standard flow selected, then select Direct access grants and Service account roles. These capabilities enable the flows demonstrated in this guide.

-
Select Next, enter
http://localhost:9080/anything/callbackin Valid redirect URIs, and select Save.
Leave Require PKCE off while testing the standard authorization code flow. The PKCE variant shows when to turn it on.
Create a User
Users in Keycloak are entities that can log in. They can have attributes such as a username, email address, and name.
If you are only implementing client credentials grant, you can skip this section.
Create a user whose profile satisfies the default Keycloak user-profile requirements:
-
Select Users in the left menu, then select Create new user.
-
Configure the following fields and select Create:
Field Value Username quickstart-userEmail quickstart-user@example.comFirst name QuickstartLast name User
-
Open the Credentials tab and select Set password.
-
Enter
quickstart-user-passin Password and Password confirmation, turn off Temporary, and select Save. -
Select Save password in the confirmation dialog.

Obtain the OIDC Configuration
In this section, you will obtain the OIDC configuration from Keycloak and define it as shell variables. Later steps use these variables in shell commands.
Open a separate terminal and define the shell variables there. Run the later shell commands in the same terminal.
Get Discovery Endpoint
Select Realm settings in the left menu. In the General tab, find Endpoints, then copy the link for OpenID Endpoint Configuration.

The link should be the same as the following:
http://localhost:8080/realms/quickstart-realm/.well-known/openid-configuration
Configuration values exposed with this endpoint are required during OIDC authentication.
Replace the address with a host IP that the APISIX container can reach, then save the values to environment variables:
export KEYCLOAK_IP=192.168.42.145 # replace with your host IP
export OIDC_DISCOVERY="http://${KEYCLOAK_IP}:8080/realms/quickstart-realm/.well-known/openid-configuration"
Get Client ID and Secret
Select Clients > apisix-quickstart-client > Credentials, then copy the value in Client Secret.

Save the OIDC client ID and secret to environment variables:
export OIDC_CLIENT_ID=apisix-quickstart-client
export OIDC_CLIENT_SECRET=replace-with-your-client-secret
Implement Authorization Code Grant
The authorization code grant is used by web and mobile applications. When a user requests a protected resource, APISIX redirects the browser to Keycloak for sign-in. Keycloak returns a short-lived authorization code, which APISIX exchanges for tokens and stores in a cookie-backed session.
To implement authorization code grant, create a route with the openid-connect plugin as follows:
- Admin API
- ADC
curl -i "http://127.0.0.1:9180/apisix/admin/routes" -X PUT --data-binary @- <<EOF
{
"id": "auth-with-oidc",
"uri": "/anything/*",
"plugins": {
"openid-connect": {
"bearer_only": false,
"session": {
"secret": "f86cf31663a9c9fa0a28c2cc78badef1"
},
"client_id": "$OIDC_CLIENT_ID",
"client_secret": "$OIDC_CLIENT_SECRET",
"discovery": "$OIDC_DISCOVERY",
"scope": "openid profile",
"redirect_uri": "http://localhost:9080/anything/callback"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}
EOF
❶ bearer_only: Set to false for authorization code grant.
❷ session.secret: Replace with your key used for session encryption and HMAC operation. Required when bearer_only is false.
❸ client_id: Keycloak client ID.
❹ client_secret: Keycloak client secret.
❺ discovery: URI to discovery document.
❻ redirect_uri: URI to redirect to after authentication with Keycloak. See Access settings.
services:
- name: httpbin Service
routes:
- uris:
- /anything/*
name: auth-with-oidc
plugins:
openid-connect:
bearer_only: false
session:
secret: "f86cf31663a9c9fa0a28c2cc78badef1"
client_id: ${OIDC_CLIENT_ID}
client_secret: ${OIDC_CLIENT_SECRET}
discovery: ${OIDC_DISCOVERY}
scope: openid profile
redirect_uri: "http://localhost:9080/anything/callback"
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
❶ bearer_only: Set to false for authorization code grant.
❷ session.secret: Replace with your key used for session encryption and HMAC operation. Required when bearer_only is false.
❸ client_id: Keycloak client ID.
❹ client_secret: Keycloak client secret.
❺ discovery: URI to discovery document.
❻ redirect_uri: URI to redirect to after authentication with Keycloak. See Access settings.
Synchronize the configuration to APISIX:
adc sync -f adc.yaml
Verify Standard Authorization Code Grant
Navigate to http://localhost:9080/anything/test in a browser. APISIX redirects the request to the Keycloak sign-in page:

Sign in with the username quickstart-user and password quickstart-user-pass. Keycloak returns an authorization code, APISIX exchanges it for tokens, and the request is forwarded to httpbin.org. You should receive a response similar to the following:
{
"args": {},
"data": "",
"files": {},
"form": {},
"headers": {
"Accept": "text/html..."
},
"json": null,
"method": "GET",
"origin": "127.0.0.1",
"url": "http://127.0.0.1/anything/test"
}
Refresh the page or open another URL under /anything/. APISIX reuses the browser session cookie, so Keycloak does not prompt you to sign in again while the session remains valid.
To implement authorization code grant with PKCE, first open Clients > apisix-quickstart-client > Settings > Capability config, turn on Require PKCE, and select Save. Then create a route similar to the previous example, but enable use_pkce:
- Admin API
- ADC
curl -i "http://127.0.0.1:9180/apisix/admin/routes" -X PUT --data-binary @- <<EOF
{
"id": "auth-with-oidc",
"uri": "/anything/*",
"plugins": {
"openid-connect": {
"bearer_only": false,
"session": {
"secret": "f86cf31663a9c9fa0a28c2cc78badef1"
},
"use_pkce": true,
"client_id": "$OIDC_CLIENT_ID",
"client_secret": "$OIDC_CLIENT_SECRET",
"discovery": "$OIDC_DISCOVERY",
"scope": "openid profile",
"redirect_uri": "http://localhost:9080/anything/callback"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}
EOF
❶ use_pkce: Set to true to send an S256 PKCE challenge during authorization.
services:
- name: httpbin Service
routes:
- uris:
- /anything/*
name: auth-with-oidc
plugins:
openid-connect:
bearer_only: false
session:
secret: "f86cf31663a9c9fa0a28c2cc78badef1"
use_pkce: true
client_id: ${OIDC_CLIENT_ID}
client_secret: ${OIDC_CLIENT_SECRET}
discovery: ${OIDC_DISCOVERY}
scope: openid profile
redirect_uri: "http://localhost:9080/anything/callback"
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
❶ use_pkce: Set to true to send an S256 PKCE challenge during authorization.
Synchronize the configuration to APISIX:
adc sync -f adc.yaml
Verify Authorization Code Grant With PKCE
Open a new browser session and navigate to http://localhost:9080/anything/test. Sign in with the username quickstart-user and password quickstart-user-pass. Keycloak requires PKCE for this client, so an HTTP/1.1 200 OK response from the upstream verifies that APISIX sent an S256 challenge and completed the code exchange.
Verify with Invalid Credentials
Open another new private browser session, navigate to http://localhost:9080/anything/test, and sign in with the wrong credentials. You should see an authentication failure:

Implement Client Credentials Grant
In client credentials grant, clients obtain access tokens without any users involved. It is typically used in machine-to-machine (M2M) communications.
To implement client credentials grant, create a route with the openid-connect plugin as follows:
- Admin API
- ADC
curl -i "http://127.0.0.1:9180/apisix/admin/routes" -X PUT --data-binary @- <<EOF
{
"id": "auth-with-oidc",
"uri": "/anything/*",
"plugins": {
"openid-connect": {
"bearer_only": true,
"use_jwks": true,
"client_id": "$OIDC_CLIENT_ID",
"discovery": "$OIDC_DISCOVERY"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}
EOF
❶ Verify bearer tokens locally with the JWKS endpoint from the discovery document. APISIX does not call the token or introspection endpoint in this mode. Therefore, the route does not need client_secret or session configuration.
services:
- name: httpbin Service
routes:
- uris:
- /anything/*
name: auth-with-oidc
plugins:
openid-connect:
bearer_only: true
use_jwks: true
client_id: ${OIDC_CLIENT_ID}
discovery: ${OIDC_DISCOVERY}
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
❶ Verify bearer tokens locally with the JWKS endpoint from the discovery document. APISIX does not call the token or introspection endpoint in this mode. Therefore, the route does not need client_secret or session configuration.
Synchronize the configuration to APISIX:
adc sync -f adc.yaml
The client still uses $OIDC_CLIENT_SECRET when it requests an access token from Keycloak. The secret is omitted only from the APISIX route because APISIX validates the issued JWT through JWKS.
Alternatively, you can use Keycloak's introspection endpoint to verify the token. Keycloak 26.7.1 requires the client performing introspection to be included in the token audience. Add an audience mapper before configuring APISIX:
- Open Clients > apisix-quickstart-client > Client scopes.
- Select apisix-quickstart-client-dedicated > Configure a new mapper > Audience.
- Enter
apisix-audiencein Name and selectapisix-quickstart-clientin Included Client Audience. - Keep Add to access token on and select Save.

Create the route without use_jwks so that APISIX uses the introspection endpoint from the discovery document:
- Admin API
- ADC
curl -i "http://127.0.0.1:9180/apisix/admin/routes" -X PUT --data-binary @- <<EOF
{
"id": "auth-with-oidc",
"uri": "/anything/*",
"plugins": {
"openid-connect": {
"bearer_only": true,
"client_id": "$OIDC_CLIENT_ID",
"client_secret": "$OIDC_CLIENT_SECRET",
"discovery": "$OIDC_DISCOVERY"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}
EOF
services:
- name: httpbin Service
routes:
- uris:
- /anything/*
name: auth-with-oidc
plugins:
openid-connect:
bearer_only: true
client_id: ${OIDC_CLIENT_ID}
client_secret: ${OIDC_CLIENT_SECRET}
discovery: ${OIDC_DISCOVERY}
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
Synchronize the configuration to APISIX:
adc sync -f adc.yaml
APISIX obtains the introspection endpoint from the discovery document.
Verify With Valid Access Token
Obtain an access token for the Keycloak server at the token endpoint:
curl -i "http://${KEYCLOAK_IP}:8080/realms/quickstart-realm/protocol/openid-connect/token" -X POST \
-d 'grant_type=client_credentials' \
-d "client_id=$OIDC_CLIENT_ID" \
-d "client_secret=$OIDC_CLIENT_SECRET"
The expected response is similar to the following:
{
"access_token": "eyJ...",
"expires_in": 300,
"refresh_expires_in": 0,
"token_type": "Bearer",
"scope": "email profile"
}
If you configured the introspection variant, the access token includes apisix-quickstart-client in its aud claim.
Save the access token to an environment variable:
# replace with your access token
export ACCESS_TOKEN="replace-with-your-access-token"
Send a request to the route with the valid access token:
curl -i "http://127.0.0.1:9080/anything/test" -H "Authorization: Bearer $ACCESS_TOKEN"
An HTTP/1.1 200 OK response verifies that the request to the upstream resource was authorized.
Verify With Invalid Access Token
Send a request to the route with invalid access token:
curl -i "http://127.0.0.1:9080/anything/test" -H "Authorization: Bearer invalid-access-token"
An HTTP/1.1 401 Unauthorized response verifies that the OIDC plugin rejects requests with invalid access token.
Verify without Access Token
Send a request to the route without access token:
curl -i "http://127.0.0.1:9080/anything/test"
An HTTP/1.1 401 Unauthorized response verifies that the OIDC plugin rejects requests without access token.
Implement Password Grant
Password grant is a legacy approach to exchange user credentials for an access token.
To implement password grant, create a route with the openid-connect plugin as follows:
- Admin API
- ADC
curl -i "http://127.0.0.1:9180/apisix/admin/routes" -X PUT --data-binary @- <<EOF
{
"id": "auth-with-oidc",
"uri": "/anything/*",
"plugins": {
"openid-connect": {
"bearer_only": true,
"use_jwks": true,
"client_id": "$OIDC_CLIENT_ID",
"client_secret": "$OIDC_CLIENT_SECRET",
"discovery": "$OIDC_DISCOVERY",
"scope": "openid profile"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}
EOF
❶ bearer_only: Set to true so APISIX rejects requests without a bearer token instead of starting an authorization code flow.
❷ use_jwks: Verify bearer tokens locally with the JWKS endpoint from the discovery document.
services:
- name: httpbin Service
routes:
- uris:
- /anything/*
name: auth-with-oidc
plugins:
openid-connect:
bearer_only: true
use_jwks: true
client_id: ${OIDC_CLIENT_ID}
client_secret: ${OIDC_CLIENT_SECRET}
discovery: ${OIDC_DISCOVERY}
scope: openid profile
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
❶ bearer_only: Set to true so APISIX rejects requests without a bearer token instead of starting an authorization code flow.
❷ use_jwks: Verify bearer tokens locally with the JWKS endpoint from the discovery document.
Synchronize the configuration to APISIX:
adc sync -f adc.yaml
Verify With Valid Access Token
Obtain an access token for the Keycloak server at the token endpoint:
OIDC_USER=quickstart-user
OIDC_PASSWORD=quickstart-user-pass
curl -i "http://${KEYCLOAK_IP}:8080/realms/quickstart-realm/protocol/openid-connect/token" -X POST \
-d 'grant_type=password' \
-d "client_id=$OIDC_CLIENT_ID" \
-d "client_secret=$OIDC_CLIENT_SECRET" \
-d "username=$OIDC_USER" \
-d "password=$OIDC_PASSWORD"
The expected response is similar to the following:
{
"access_token": "eyJ...",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "eyJ...",
"token_type": "Bearer",
"scope": "email profile"
}
Save the access token and refresh token to environment variables. The refresh token will be used in the refresh token step.
# replace with your access token
export ACCESS_TOKEN="replace-with-your-access-token"
export REFRESH_TOKEN="replace-with-your-refresh-token"
Send a request to the route with the valid access token:
curl -i "http://127.0.0.1:9080/anything/test" -H "Authorization: Bearer $ACCESS_TOKEN"
An HTTP/1.1 200 OK response verifies that the request to the upstream resource was authorized.
Verify With Invalid Access Token
Send a request to the route with invalid access token:
curl -i "http://127.0.0.1:9080/anything/test" -H "Authorization: Bearer invalid-access-token"
An HTTP/1.1 401 Unauthorized response verifies that the OIDC plugin rejects requests with invalid access token.
Verify without Access Token
Send a request to the route without access token:
curl -i "http://127.0.0.1:9080/anything/test"
An HTTP/1.1 401 Unauthorized response verifies that the OIDC plugin rejects requests without access token.
Refresh Token
To refresh the access token, send the following request to the Keycloak token endpoint:
curl -i "http://${KEYCLOAK_IP}:8080/realms/quickstart-realm/protocol/openid-connect/token" -X POST \
-d 'grant_type=refresh_token' \
-d "client_id=$OIDC_CLIENT_ID" \
-d "client_secret=$OIDC_CLIENT_SECRET" \
-d "refresh_token=$REFRESH_TOKEN"
You should see a response with a new access token and refresh token:
{
"access_token": "eyJ...",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "eyJ...",
"token_type": "Bearer",
"scope": "email profile"
}
Replace both environment variables with the newly returned values before sending subsequent requests or refreshing the token again:
export ACCESS_TOKEN="replace-with-your-new-access-token"
export REFRESH_TOKEN="replace-with-your-new-refresh-token"
Next Steps
To harden the authorization code flow with PAR, DPoP, PKCE, and private-key JWT authentication, see Secure OIDC with PAR and DPoP.
APISIX supports the integration with many other OIDC identity providers, such as Okta, Auth0, Authgear, and Microsoft Entra ID (Azure AD).
In addition, APISIX also supports built-in authentication approaches such as key authentication, basic authentication, and JWT.