Start Keycloak software
cd keycloak
docker compose up -dAccess keycloak with admin credentials http://host.docker.internal:8080/ (admin/admin)
Create the realm for the project (test)
The Client Credentials flow is an OAuth2 grant used for machine-to-machine authentication without a user. A confidential client (like a backend service) authenticates to the authorization server using its client ID and secret. In exchange, it gets an access token that represents the client itself (not a user). This token is then used to call protected APIs, typically with predefined roles or scopes.
Create client:
- Client ID: m2m-client
- Client type: OpenID Connect
- Client authentication: On (confidential)
- Authorization: Off (you don’t need UMA here)
- Standard Flow / Direct Access Grants / Device: Off
- Service accounts roles: On
Assign some service account roles
Get the access token
SECRET=g9krYSyK8c2LJcLWoszHBFyrjs667Dh3
BASIC=$(printf '%s' "m2m-client:${CLIENT_SECRET}" | base64)
curl -s -X POST \
-H "Authorization: Basic $BASIC" \
-d "grant_type=client_credentials" \
http://host.docker.internal:8080/realms/test/protocol/openid-connect/token | jq .Verify the signature http://host.docker.internal:8080/realms/test/protocol/openid-connect/certs
The Direct Access Grants flow (OAuth2 password grant) allows a client to exchange a user’s username and password directly for an access token. It bypasses browser redirects and the Keycloak login screen, returning tokens in a single call to the token endpoint. This flow is simple and useful for trusted apps or CLIs, but not recommended in production since the client handles raw user credentials.
Create client:
- Client ID: direct-client
- Client type: OpenID Connect
- Client authentication: Off
- Authorization: Off (you don’t need UMA here)
- Direct Access Grants: On
- Other flows: Off
Create a user
Get the access token
#!/bin/bash
REALM="test"
CLIENT_ID="direct-client"
USERNAME="alice"
PASSWORD="alice123"
curl -s -X POST \
-d "grant_type=password" \
-d "client_id=${CLIENT_ID}" \
-d "username=${USERNAME}" \
-d "password=${PASSWORD}" \
http://host.docker.internal:8080/realms/${REALM}/protocol/openid-connect/token | jq .Create client
- Client ID: backend-confidential
- Client type: OpenID Connect
- Client authentication: On (confidential)
- Standard Flow: On (others Off)
- Valid Redirect URIs: http://host.docker.internal:4000/callback
Add client secret to .env file
Start demo server
cd auth_code_demo
npm install
cp .env.example .env
npm startOAuth2/OIDC login flow designed for public clients (like SPAs and mobile apps) that can’t keep a client secret. The app starts a browser redirect, sending a code_challenge; after the user logs in, it exchanges the returned authorization code plus the matching code_verifier for tokens. PKCE (typically S256) binds the code to the original client, blocking code interception/replay attacks. The result is standard ID/Access (and optionally Refresh) tokens—all without storing a client secret in the browser.
Create client
- Client ID: spa-client
- Client type: OpenID Connect
- Client authentication: Off (public client)
- Standard Flow: ON
- Direct Access Grants / Implicit / Device: OFF (for this demo)
- Proof Key for Code Exchange: Required (S256)
- Valid Redirect URIs: http://host.docker.internal:3000/
- Web Origins: http://host.docker.internal:3000
generate Verifier code and challenge code
VERIFIER=$(LC_ALL=C tr -dc '[:alnum:]-._~' </dev/urandom | head -c 64)
CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -binary -sha256 | openssl base64 -A | tr '+/' '-_' | tr -d '=')
printf 'code_verifier=%s\ncode_challenge=%s\n' "$VERIFIER" "$CHALLENGE"copy the values into the demo site
pkce: {
method: "S256", // "plain" or "S256" (must match client setting in Keycloak)
codeVerifier: "LAs6SCcWWV5IqqXhC.eNViJ8~oPDlwi~bQ0HNaOf2uraTERDWjKF2w7.1IChQqxd",
codeChallenge: "b8qvyEog82iBL2xizjHp5Aq3d8Az5WQru-nzsuJ19dw" // if method=S256 paste the precomputed challenge here
},
Start demo web site
cd pkce_demo
npx serve -s . -l 3000Launch apisix and Context broker service
cd orion
docker compose up -d
cd apisix
docker compose up -dAccess Apisix admin portal (host.docker.internal:9000 admin/admin)
Create the upstream
- host.docker.internal:1026
Create initial route
- Enable openid plugin:
{
"discovery": "http://host.docker.internal:8080/realms/test/.well-known/openid-configuration",
"client_id": "direct-client",
"client_secret": "",
"bearer_only": true,
"unauth_action": "deny",
"use_jwks": true,
"claim_validator": {
"audience": {
"match_with_client_id": false
}
},
"set_userinfo_header": false
}
Get an access token from direct client and test a query with and without Authorization header
Configure OPA policy with role and client ID
Launch Open Policy Agent
cd opa
docker compose up -dConfigure OPA plugin for policy validation
{
"host": "http://host.docker.internal:8181",
"policy": "apisix/authz",
"timeout": 3000,
"keepalive": true,
"with_route": false,
"with_service": false,
"with_consumer": false
}