Skip to main content

Authorize M2M Requests with Keycloak

Keycloak can issue access tokens to service accounts associated with confidential OIDC clients. Background services, scheduled jobs, command-line tools, and automation can use these tokens to call APIs without an end user signing in.

The OAuth 2.0 Client Credentials grant is designed for this type of machine-to-machine (M2M) communication. Apache APISIX can protect the target API by validating each token's signature, issuer, audience, and granted scope before proxying the request.

The guide uses one Keycloak client to represent the API protected by APISIX and another service-account client to represent the calling service. The service account receives a client scope and requests an access token whose audience identifies the protected API. APISIX validates both restrictions and removes authentication data before forwarding the request to the sample upstream. The primary configuration validates tokens locally with Keycloak's JSON Web Key Set (JWKS); an alternative configuration uses Keycloak's token introspection endpoint.

Prerequisite(s)

Configure Keycloak

Start a local Keycloak server, then create a realm, a protected-API client, a client scope, and a service-account client for M2M authorization.

Start Keycloak

If Keycloak and quickstart-realm are already available from Set Up SSO with Keycloak, reuse them and continue with Create an API Scope.

Otherwise, start Keycloak in development mode with a temporary administrator account:

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

Development mode and the example credentials are intended only for local testing. For a production deployment, use HTTPS, a production database, and a permanent administrator account.

Open http://localhost:8080/admin/ and sign in with the administrator username quickstart-admin and password quickstart-admin-pass.

Create a Realm

Create an isolated realm for this guide:

  1. Select Manage realms → Create realm.
  2. Enter quickstart-realm as the realm name.
  3. Select Create.

Create a realm in Keycloak

Create an API Scope

Create the scope that APISIX will require from M2M access tokens:

  1. Select Client scopes → Create client scope.
  2. Enter apisix.read as the name and keep Protocol set to OpenID Connect.
  3. Turn on Include in token scope and select Save.

Create a Protected API Client

Create a confidential client that represents the API protected by APISIX:

  1. Select Clients → Create client.
  2. Keep Client type set to OpenID Connect, enter apisix-protected-api as the client ID, and select Next.
  3. Turn on Client authentication. Leave all authentication flows off and select Save.

APISIX uses this client ID as the expected token audience. The client secret is needed only for the optional token-introspection configuration.

Create an M2M Client

Create a confidential client with a Keycloak service account:

  1. Select Clients → Create client.
  2. Keep Client type set to OpenID Connect, enter apisix-m2m-client as the client ID, and select Next.
  3. Turn on Client authentication and Service account roles. Leave Standard flow and the other authentication flows off, then select Save.
  4. Open the Client scopes tab and select Add client scope.
  5. Select apisix.read, select Add, and add it as an optional client scope.

The service account allows this client to obtain tokens through the Client Credentials grant. A redirect URI is not required because the flow does not redirect a browser or authenticate an end user.

Add the Token Audience

Add the protected API client as the audience of access tokens issued to the M2M client:

  1. Open Clients → apisix-m2m-client → Client scopes.
  2. Select apisix-m2m-client-dedicated and then select Add mapper → By configuration → Audience.
  3. Enter apisix-audience as the name and select apisix-protected-api as the included client audience.
  4. Keep Add to access token on and select Save.

Add the protected API client to the Keycloak token audience

The access token will contain apisix-protected-api in its aud claim. This separates the calling service from the protected API and allows APISIX to reject tokens issued for a different audience.

Save the OAuth Configuration

Select Realm settings. On the General tab, find Endpoints and open OpenID Endpoint Configuration. The APISIX container must be able to reach Keycloak at the discovery document's hostname.

Save a reachable host address and the discovery endpoint as environment variables, replacing the example address:

export KEYCLOAK_HOST=192.168.1.100
export KEYCLOAK_DISCOVERY="http://${KEYCLOAK_HOST}:8080/realms/quickstart-realm/.well-known/openid-configuration"

Select Clients → apisix-m2m-client → Credentials and copy the calling client's secret. Then open Clients → apisix-protected-api → Credentials and copy the protected API client's secret.

Save the client IDs, secrets, and required scope as environment variables:

export KEYCLOAK_API_CLIENT_ID=apisix-protected-api
export KEYCLOAK_API_CLIENT_SECRET=replace-with-your-protected-api-client-secret
export KEYCLOAK_M2M_CLIENT_ID=apisix-m2m-client
export KEYCLOAK_M2M_CLIENT_SECRET=replace-with-your-client-secret
export KEYCLOAK_M2M_SCOPE=apisix.read

Keep the client secret confidential. Store production credentials in a secret manager and rotate them according to the organization's credential-rotation policy.

Configure Local JWT Validation

Configure a route that validates Keycloak bearer tokens locally before forwarding requests to httpbin.org, a public HTTP request and response service. The /anything/m2m/* endpoint returns request details for verification.

Choose either the Admin API or ADC to configure the route.

Create a route that validates bearer tokens with Keycloak's JWKS:

curl "http://127.0.0.1:9180/apisix/admin/routes/keycloak-m2m" -X PUT \
--data-binary @- <<EOF
{
"uri": "/anything/m2m/*",
"plugins": {
"openid-connect": {
"client_id": "$KEYCLOAK_API_CLIENT_ID",
"discovery": "$KEYCLOAK_DISCOVERY",
"bearer_only": true,
"use_jwks": true,
"claim_validator": {
"audience": {
"required": true,
"match_with_client_id": true
}
},
"required_scopes": ["$KEYCLOAK_M2M_SCOPE"],
"set_access_token_header": false,
"set_id_token_header": false,
"set_userinfo_header": false
},
"proxy-rewrite": {
"headers": {
"remove": ["Authorization"]
}
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}
EOF

bearer_only and use_jwks: Require a bearer access token and validate its JWT signature locally with the public keys published by Keycloak.

claim_validator.audience: Requires the token's aud claim to contain the protected API client ID configured as client_id.

required_scopes: Requires the access token to include the API scope assigned to the service-account client.

set_access_token_header, set_id_token_header, and set_userinfo_header: Set to false to prevent APISIX from adding the access token, ID token, and token claims to upstream request headers.

proxy-rewrite.headers.remove: Removes the original bearer token before APISIX proxies the request. Review these header settings if the upstream application must receive the access token or its claims.

Verify M2M Authorization

Request an access token from Keycloak. The --user option sends the client ID and secret using HTTP Basic authentication:

export KEYCLOAK_ACCESS_TOKEN="$(
curl -sS "http://${KEYCLOAK_HOST}:8080/realms/quickstart-realm/protocol/openid-connect/token" \
--user "${KEYCLOAK_M2M_CLIENT_ID}:${KEYCLOAK_M2M_CLIENT_SECRET}" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "scope=${KEYCLOAK_M2M_SCOPE}" | \
jq -er '.access_token'
)"

Send the access token to the protected route:

curl -i "http://127.0.0.1:9080/anything/m2m/get" \
-H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}"

An HTTP/1.1 200 OK response verifies that APISIX accepted a Keycloak access token for the configured audience and scope. The response body should contain fields similar to these:

{
"args": {},
"data": "",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Host": "127.0.0.1",
"User-Agent": "curl/8.7.1",
"X-Amzn-Trace-Id": "Root=1-...",
"X-Forwarded-Host": "127.0.0.1:9080"
},
"json": null,
"method": "GET",
"origin": "192.168.155.1, xxx.xxx.xxx.xxx",
"url": "http://127.0.0.1:9080/anything/m2m/get"
}

Header values and the reported origin address vary by environment. The upstream request headers should not include the bearer token, access token, ID token, or token claims.

Request a token without the required scope and send it to the route:

TOKEN_WITHOUT_SCOPE="$(
curl -sS "http://${KEYCLOAK_HOST}:8080/realms/quickstart-realm/protocol/openid-connect/token" \
--user "${KEYCLOAK_M2M_CLIENT_ID}:${KEYCLOAK_M2M_CLIENT_SECRET}" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=client_credentials" | \
jq -er '.access_token'
)"

curl -i "http://127.0.0.1:9080/anything/m2m/get" \
-H "Authorization: Bearer ${TOKEN_WITHOUT_SCOPE}"

APISIX returns HTTP/1.1 403 Forbidden because the token does not include apisix.read.

Send requests without a token and with a malformed token:

curl -i "http://127.0.0.1:9080/anything/m2m/get"

curl -i "http://127.0.0.1:9080/anything/m2m/get" \
-H "Authorization: Bearer invalid-access-token"

APISIX returns HTTP/1.1 401 Unauthorized for both requests.

Validate Tokens with Introspection

Local JWT validation avoids a provider request for each token check. If immediate token-state checks are required, APISIX can instead send the bearer token to Keycloak's introspection endpoint. The audience mapper configured earlier allows the protected API client to introspect the M2M client's tokens.

Choose either the Admin API or ADC to replace the route configuration.

Create the route without use_jwks and provide the client secret for introspection authentication:

curl "http://127.0.0.1:9180/apisix/admin/routes/keycloak-m2m" -X PUT \
--data-binary @- <<EOF
{
"uri": "/anything/m2m/*",
"plugins": {
"openid-connect": {
"client_id": "$KEYCLOAK_API_CLIENT_ID",
"client_secret": "$KEYCLOAK_API_CLIENT_SECRET",
"discovery": "$KEYCLOAK_DISCOVERY",
"bearer_only": true,
"claim_validator": {
"audience": {
"required": true,
"match_with_client_id": true
}
},
"required_scopes": ["$KEYCLOAK_M2M_SCOPE"],
"set_access_token_header": false,
"set_id_token_header": false,
"set_userinfo_header": false
},
"proxy-rewrite": {
"headers": {
"remove": ["Authorization"]
}
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}
EOF

client_secret: Authenticates APISIX as the protected API client to Keycloak's introspection endpoint. Without use_jwks, APISIX obtains that endpoint from the discovery document and validates bearer tokens remotely.

Request a new access token with the required scope and repeat the protected-route request. An HTTP/1.1 200 OK response verifies that Keycloak reported the token as active and APISIX accepted its audience and scope.

Next Steps

You have now configured APISIX to authorize M2M requests with Keycloak. To authenticate browser users with the same identity provider, see Set Up SSO with Keycloak. See the openid-connect plugin reference for more configuration options.