OAuth2 and OpenID Connect, with Keycloak
How Prayog knows who is calling: a person in the web terminal or a bot running on someone's laptop. The gateway never sees a password. It checks a signed token issued by Keycloak. This page explains the flows, PKCE, what is inside a token and how the gateway will validate it.
1. The problem
- Humans sign in through a browser; bots have no browser and no human.
- The gateway must know who sent each order (to check ownership, roles and rate limits) on every request, quickly.
- Handling passwords in every service is risky. One service should do it well; the others should trust its word.
OAuth2 and OpenID Connect are the standard answer: a dedicated identity server (Keycloak) authenticates callers and issues short-lived, signed tokens that any service can verify on its own.
2. A business example: the hotel key card
- Reception (Keycloak) checks your ID once and programs a key card.
- The key card (access token) says which room and until when. It's encoded so it can't be forged.
- The room door (the gateway) reads the card itself. It doesn't phone reception on every swipe.
- The card expires at checkout; a lost card is only useful until then.
- The hotel's cleaning robot (a bot) doesn't show a passport; it has a machine credential (client id + secret) and gets a card for its own floor.
3. The four roles, and OAuth2 vs OIDC
| Role | In Prayog |
|---|---|
| Resource owner | The trader (or, for a bot, the bot's owner) |
| Client | The web terminal (prayog-web), a bot (prayog-bot-demo), the simulated traders (prayog-agents) |
| Authorization server | Keycloak, realm prayog |
| Resource server | The gateway (prayog-api), from S10 |
- OAuth2 is about access: a client gets an access token to call an API.
- OpenID Connect (OIDC) is a layer on top about identity: it adds the ID token (who signed in), standard claims (
sub,email), a/userinfoendpoint and a discovery document at/.well-known/openid-configurationlisting every endpoint and the signing keys.
4. Flows (grant types)
| Flow | For | Status |
|---|---|---|
| Authorization code + PKCE | Apps with a user: browser apps, mobile, CLI with a browser | The recommended flow for any user sign-in |
| Client credentials | Machine to machine: bots, services | Standard; the client authenticates as itself |
| Refresh token | Getting a new access token without signing in again | Used with authorization code |
| Implicit | Old browser apps (token in the URL) | Deprecated: tokens leak through history and logs |
| Resource owner password | The app collects the user's password itself | Deprecated: defeats the point of a separate identity server |
Prayog's realm enables exactly two: authorization code + PKCE for prayog-web, client credentials for the bots. Everything else is switched off per client.
5. PKCE, step by step
A browser app is a public client: its code is downloaded by everyone, so it can't keep a secret. Without one, anyone who intercepts the one-time authorization code (a malicious browser extension, a leaked redirect) could swap it for tokens. PKCE (Proof Key for Code Exchange, RFC 7636) fixes that:
- The app makes a random code verifier and keeps it in memory.
- It sends the user to Keycloak with only the code challenge = base64url(SHA-256(verifier)).
- The user signs in; Keycloak redirects back with an authorization code and remembers the challenge.
- The app exchanges the code for tokens, sending the verifier. Keycloak hashes it and compares it with the challenge.
- A thief with only the code can't produce the verifier, because the hash can't be reversed.
6. Tokens: JSON Web Tokens
Keycloak's access tokens are JWTs: three base64url parts joined by dots, header.payload.signature.
| Part | Holds |
|---|---|
| Header | Algorithm (RS256: RSA signature with SHA-256) and kid, which of Keycloak's keys signed it |
| Payload | Claims: iss issuer, sub subject, aud audience, exp/iat expiry and issue time, azp the client that asked, realm_access.roles |
| Signature | Keycloak's private-key signature over header and payload. Anyone can read a JWT; only Keycloak can make one. |
The gateway validates every token locally, with no call to Keycloak per request:
- Fetch Keycloak's public keys once from the JWKS endpoint (listed in the discovery document) and cache them.
- Check the signature with the key named by
kid. - Check
issis exactlyhttp://auth.prayog.localhost/realms/prayog. - Check
audcontainsprayog-api, so a token minted for some other app is refused. - Check
expis in the future (with a small allowance for clock skew). - Map roles (
trader,ops,bot) to permissions, andsubto a Prayog account.
7. Decode a token
Get a real one from your running stack:
source .env && curl -s -d grant_type=client_credentials -d client_id=prayog-bot-demo \
-d client_secret="$PRAYOG_BOT_DEMO_SECRET" \
http://auth.prayog.localhost/realms/prayog/protocol/openid-connect/token | python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])'
Paste it below. Decoding happens in your browser only; the page sends nothing anywhere. It checks the claims the gateway will check (not the signature, which needs Keycloak's public key).
8. Keycloak's terms
| Term | Meaning |
|---|---|
| Realm | An isolated set of users, clients and roles. master is for Keycloak's own admins; Prayog uses prayog. |
| Client | An application registered with the realm: its allowed flows, redirect URIs and (if confidential) its secret |
| Public / confidential | Can't / can keep a secret. Browser apps are public; bots and servers are confidential. |
| Service account | The user that a confidential client acts as in client credentials. Prayog gives bot service accounts the bot role. |
| Realm role | A role valid across the realm (trader, ops, bot), placed in realm_access.roles |
| Protocol mapper | Adds or shapes claims. Prayog's audience mapper adds prayog-api to aud. |
| Realm import | A JSON file loaded at start-up. Prayog's has ${...} placeholders filled from .env, and is imported only if the realm doesn't exist yet. |
9. In Prayog
| Client | Type | Flow | Who |
|---|---|---|---|
prayog-web | Public | Authorization code + PKCE (S256), redirects only to app.prayog.localhost | People in the web terminal (S18) |
prayog-api | No flows | None: it's the audience | The gateway (S10) |
prayog-bot-demo | Confidential | Client credentials | The sample bot (S12); "API key" = client id + secret |
prayog-agents | Confidential | Client credentials | Simulated traders (S13) |
make smoke already proves the important parts: the issuer, a client-credentials token with aud: prayog-api and role bot, a 401 for a wrong secret, the users' roles and PKCE on the web client.
10. Questions
If tokens are checked locally, how do you log someone out or block a bot?
Keep access tokens short-lived (Prayog: 5 minutes). Disabling the user or client in Keycloak stops new tokens; the old one expires soon after. For an instant stop, Prayog also has its own kill switch: ops can disable the account inside the exchange (S6).
Why not simple API keys for bots?
A client id + secret is an API key, but exchanged for a short-lived token. The secret travels only to Keycloak, and the gateway checks one kind of credential for humans and bots alike.
Where should a browser app keep tokens?
In memory, ideally, with a refresh token handled carefully. Local storage is readable by any script on the page, so a single XSS bug would leak tokens. Prayog's web app (S18) uses oidc-client-ts, which follows these practices.