Interactive guide to OAuth 2.0, OpenID Connect, and Keycloak
"Who are you?"
Proves your identity — typically via username/password, MFA, biometrics, or SSO tokens.
"What can you do?"
Determines which resources you can access — enforced via roles, permissions, or scopes.
Keycloak sits between your users and your backend — it handles all authentication and issues tokens your backend validates.
Isolated tenant spaces. Each realm has its own users, clients, roles, and identity providers — completely separate from other realms.
Applications that use Keycloak for auth. A client can be a SPA, mobile app, backend service, or even another Keycloak instance.
Users are identities. Roles are collections of permissions. Assign roles to users or groups to control access.
Delegate authentication to external providers like Google, GitHub, LDAP, or SAML. Keycloak acts as a broker.
| Flow | Use Case | Refresh Token? | Status |
|---|---|---|---|
| Authorization Code + PKCE | SPAs, mobile apps, web apps | ✅ Yes | ✅ Use This |
| Client Credentials | Machine-to-machine, daemons | ⚠️ Optional | ✅ Use This |
| Device Authorization | TVs, IoT, input-limited devices | ✅ Yes | ✅ Use This |
| Resource Owner Password | Legacy / migration only | ✅ Yes | ❌ Avoid |
| Implicit | None (deprecated in OAuth 2.1) | ❌ No | ❌ Deprecated |
The only recommended flow for SPAs and mobile apps in OAuth 2.1. PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks.
Without PKCE, a malicious app could intercept the authorization code from the redirect URI. PKCE binds the code exchange to the client that initiated it using a cryptographic challenge.
Client Protocol: openid-connect
Access Type: public
Standard Flow: ✅ Enabled
Implicit Flow: ❌ Disabled
Direct Access: ❌ Disabled
Valid Redirect URIs:
http://localhost:3000/*
myapp://callback
Web Origins:
http://localhost:3000
const codeVerifier = generateRandomString(64);
const codeChallenge = sha256(codeVerifier)
.replace(/\+/g, '-')
.replace(/\//g, '_');
const params = {
client_id: 'my-spa',
redirect_uri: 'http://localhost:3000/callback',
response_type: 'code',
code_challenge_method: 'S256',
code_challenge: codeChallenge,
scope: 'openid profile email'
};
window.location = authUrl + '?' +
new URLSearchParams(params);
POST /realms/myrealm/protocol/
openid-connect/token
Body:
grant_type=authorization_code
code=
redirect_uri=http://localhost:3000/callback
client_id=my-spa
code_verifier=
Response: { access_token, refresh_token,
id_token, expires_in }
For backend services talking to each other — no user involved. The client authenticates directly with its ID + secret and receives an access token.
Never use this flow in a browser or mobile app. The client secret would be exposed. Only use from secure backend environments.
🔒 stored in vault
Returns tokens directly in the URL fragment — tokens leak into browser history, referrer headers, and analytics scripts.
// ❌ BAD: Implicit flow — token in URL
https://app.com/callback#
access_token=eyJ...&
token_type=Bearer&
expires_in=3600
✅ Do this instead: Authorization Code + PKCE
Any JavaScript on the page can read localStorage — including third-party scripts, browser extensions, and XSS payloads.
// ❌ BAD: Token exposed to XSS
localStorage.setItem('token', accessToken);
// ❌ BAD: Also exposed to XSS
sessionStorage.setItem('token', accessToken);
✅ Do this instead: In-memory variable + refresh token in httpOnly secure cookie
Your app collects the user's password directly — breaks the entire point of OAuth. The user can't revoke access to just your app. Deprecated in OAuth 2.1.
// ❌ BAD: App handles raw password
POST /token
grant_type=password
username=alice
password=hunter2
Client secrets, signing keys, and database passwords should never appear in source code or client-side code. Use environment variables or a secrets manager.
// ❌ BAD: Secret in source
const CLIENT_SECRET = "abc123-secret";
// ✅ GOOD: From env
const CLIENT_SECRET = process.env
.KEYCLOAK_CLIENT_SECRET;
Every backend endpoint must validate: signature (RS256/HS256), expiry (exp), issuer (iss), audience (aud), and not-before (nbf).
// ❌ BAD: No validation
const token = req.headers.authorization;
// ✅ GOOD: Full validation
const decoded = jwt.verify(
token,
publicKey,
{ algorithms: ['RS256'],
issuer: 'https://keycloak/realms/myrealm',
audience: 'my-backend' }
);
Access tokens should live 5–15 minutes. Use refresh token rotation to get new ones — each refresh invalidates the previous refresh token.
// ❌ BAD: 24h access token
accessTokenLifespan: 86400
// ✅ GOOD: 5min access + rotating refresh
accessTokenLifespan: 300
refreshTokenMaxReuse: 0
clientOfflineSessionMaxLifespan: 0
Short-lived (5–15 min). Contains claims about the user/client and permissions. Sent to your API as Bearer token.
{
"iss": "https://keycloak/realms/myrealm",
"sub": "user-uuid",
"aud": "my-backend",
"exp": 1722900000,
"realm_access": {
"roles": ["user", "admin"]
}
}
Contains user profile info. For the client app only — never send this to your API. Use the access token instead.
{
"iss": "https://keycloak/realms/myrealm",
"sub": "user-uuid",
"aud": "my-spa",
"name": "Alice",
"email": "alice@example.com",
"email_verified": true
}
Longer-lived. Used to get new access tokens without re-login. Must use rotation — each refresh invalidates the previous token.
Special refresh token for offline access (mobile apps, background jobs). Survives user logout and password changes unless explicitly revoked.
iss, aud, exp, nbf on every request