Fix: Keycloak Not Working — Realm/Client Setup, OIDC, Token Verification, and CORS
Quick Answer
How to fix Keycloak errors — realm vs client configuration, redirect URI mismatch, OIDC vs SAML choice, JWT signature verification with JWKS, CORS Web Origins, service accounts, and database persistence.
The Error
You set up a Keycloak client and the login redirect fails:
Invalid parameter: redirect_uriOr token verification fails in your backend:
JsonWebTokenError: invalid signatureOr the OIDC discovery endpoint returns CORS errors:
Access to fetch at 'https://keycloak.example.com/realms/myrealm/.well-known/openid-configuration'
from origin 'https://app.example.com' has been blocked by CORS policy.Or after restarting Keycloak, all your users and clients are gone:
$ docker compose restart keycloak
# Login → "Invalid username or password" — but you know the password!Why This Happens
Keycloak is a full identity provider with several layers:
- Realm = an isolated tenant. Each realm has its own users, clients, roles. The default
masterrealm is for admins; create separate realms for apps. - Client = a relying party (your app). Confidential (with secret) or public (no secret, mobile/SPA).
- Token signing = JWT signed with Keycloak’s private key. Your app verifies with the public key from the JWKS endpoint.
- Persistence = Keycloak stores data in a database. Default H2 (in-memory) is for dev only — data evaporates on restart. Production needs Postgres/MySQL.
Most issues come from misconfiguring one of these layers.
Fix 1: Create a Realm and Configure a Client
- Login as admin at
https://keycloak.example.com/admin(default credentialsadmin/adminon first start — change immediately). - Create a realm named
myrealm(any non-mastername). - Add a client under that realm.
Client settings:
Client ID: my-app
Client type: OpenID Connect
Client authentication: ON (for confidential clients with secret) / OFF (for public)
Standard flow: ON (for browser auth code flow)
Direct access grants: OFF (resource owner password — only for dev)
Service accounts roles: ON (if the app needs M2M token via client credentials)
Valid redirect URIs: https://app.example.com/auth/callback
http://localhost:3000/auth/callback (for dev)
Valid post logout URIs: https://app.example.com/*
Web origins: https://app.example.com
+ (for "allow all of the redirect URIs")Copy the Client secret (under Credentials tab) for backend use. Never expose it in client-side code.
Pro Tip: Use + in Web Origins to auto-allow the same origins listed under “Valid redirect URIs.” Avoids drift between the two.
Fix 2: Use OIDC (Not SAML, Usually)
OIDC (OpenID Connect) is JSON-based, modern, integrates easily with oidc-client-ts, next-auth, @auth/core, Passport.js. SAML is XML-based, legacy, used by some older enterprises.
Pick OIDC unless you have a reason — Keycloak supports both, but OIDC is much easier to integrate.
OIDC endpoints (auto-discoverable):
Discovery: https://keycloak.example.com/realms/myrealm/.well-known/openid-configuration
Authorization: https://keycloak.example.com/realms/myrealm/protocol/openid-connect/auth
Token: https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token
UserInfo: https://keycloak.example.com/realms/myrealm/protocol/openid-connect/userinfo
JWKS: https://keycloak.example.com/realms/myrealm/protocol/openid-connect/certs
Logout: https://keycloak.example.com/realms/myrealm/protocol/openid-connect/logoutFor Node with Passport:
import passport from "passport";
import { Strategy as OidcStrategy } from "passport-openidconnect";
passport.use(
new OidcStrategy(
{
issuer: "https://keycloak.example.com/realms/myrealm",
authorizationURL: "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/auth",
tokenURL: "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token",
userInfoURL: "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/userinfo",
clientID: process.env.KEYCLOAK_CLIENT_ID,
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
callbackURL: "https://app.example.com/auth/callback",
scope: ["openid", "profile", "email"],
},
(issuer, profile, done) => done(null, profile),
),
);For Auth.js (NextAuth.js v5):
import { Keycloak } from "@auth/core/providers/keycloak";
export const { auth, handlers } = NextAuth({
providers: [
Keycloak({
clientId: process.env.KEYCLOAK_ID,
clientSecret: process.env.KEYCLOAK_SECRET,
issuer: "https://keycloak.example.com/realms/myrealm",
}),
],
});issuer is just the realm URL — Auth.js discovers endpoints from .well-known.
Fix 3: Match Redirect URIs Exactly
Keycloak rejects redirect URIs that don’t match. Common mistakes:
- Trailing slash mismatch.
https://app.example.com/callbackvshttps://app.example.com/callback/are different. - Port mismatch.
http://localhost:3000vshttp://localhost(port 80 implicit). - HTTP vs HTTPS. Production HTTPS but dev HTTP.
- Path-based. Configure
https://app.example.com/auth/callback, but your code redirects tohttps://app.example.com/api/auth/callback/keycloak.
Add all the URIs your app may use, including dev/staging/preview:
https://app.example.com/api/auth/callback/keycloak
https://staging.example.com/api/auth/callback/keycloak
https://preview-*.vercel.app/api/auth/callback/keycloak
http://localhost:3000/api/auth/callback/keycloakWildcards * work:
https://app.example.com/*— any path under the host.https://preview-*.vercel.app/*— any preview deploy.
Common Mistake: Adding * alone (without a host pattern). Keycloak may reject “wildcard too broad” or silently fail.
Fix 4: Verify JWT Signatures
Keycloak signs tokens with RS256. Your backend should verify using the public key from the JWKS endpoint:
import { jwtVerify, createRemoteJWKSet } from "jose";
const JWKS = createRemoteJWKSet(
new URL("https://keycloak.example.com/realms/myrealm/protocol/openid-connect/certs"),
);
async function verifyToken(token: string) {
const { payload } = await jwtVerify(token, JWKS, {
issuer: "https://keycloak.example.com/realms/myrealm",
audience: "my-app", // Optional — match the client ID
});
return payload;
}createRemoteJWKSet caches the keys. Keycloak’s signing keys rotate periodically; the cache handles it transparently.
For shorter-lived caching (faster key rotation):
const JWKS = createRemoteJWKSet(jwksUrl, { cacheMaxAge: 60 * 1000 });Common Mistake: Hardcoding the public key from Keycloak. When Keycloak rotates keys (or you change realms), tokens fail verification. Always fetch from JWKS.
For Express + express-oauth2-jwt-bearer:
import { auth } from "express-oauth2-jwt-bearer";
const checkJwt = auth({
audience: "my-app",
issuerBaseURL: "https://keycloak.example.com/realms/myrealm",
tokenSigningAlg: "RS256",
});
app.get("/api/protected", checkJwt, (req, res) => {
res.json({ user: req.auth?.payload });
});The library handles JWKS fetching, signature, audience, and expiration checks.
Fix 5: CORS — Web Origins
Keycloak’s discovery and JWKS endpoints have their own CORS rules. To allow browser-side calls from your app:
- Realm → Client settings → Web Origins → add your app’s origin.
- For multiple origins, list each (one per line).
- Use
+to mirror redirect URIs (recommended for most setups).
For the userinfo endpoint specifically (if called from the browser):
- Realm → Realm settings → Advanced → CORS settings.
For backend-only flows (your server calls Keycloak), CORS doesn’t apply — the server makes HTTP calls server-to-server.
Common Mistake: Web Origins set to *. Keycloak’s CORS doesn’t accept * for credentialed requests (cookies). List actual origins or use +.
Fix 6: Persistence — Don’t Use H2 in Production
The default Keycloak Docker image uses an embedded H2 database. Restarting the container = data loss.
For production, use PostgreSQL:
# docker-compose.yml
services:
postgres:
image: postgres:16
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: changeme
volumes:
- postgres_data:/var/lib/postgresql/data
keycloak:
image: quay.io/keycloak/keycloak:25.0.0
command: ["start", "--optimized"]
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: changeme
KC_HOSTNAME: keycloak.example.com
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: changeme
KC_PROXY: edge # If behind nginx/cloudflare
ports:
- "8080:8080"
depends_on:
- postgres
volumes:
postgres_data:Three production-critical settings:
KC_DB=postgres(ormysql,mariadb).KC_HOSTNAME= your public hostname. Without it, generated URLs may be wrong.KC_PROXY=edgeif Keycloak is behind a reverse proxy that terminates TLS.
For high-availability, run multiple Keycloak nodes behind a load balancer with sticky sessions or Infinispan clustering.
Pro Tip: Always back up Keycloak’s database. Realm exports (kc.sh export ...) are an additional safety net but not a substitute for proper DB backups.
Fix 7: Service Accounts and Machine-to-Machine
For services that authenticate without a user (e.g. background workers):
- In the client, enable Client authentication + Service accounts roles.
- Use the client credentials flow:
const tokenResponse = await fetch(
"https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token",
{
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: "my-service",
client_secret: process.env.KEYCLOAK_CLIENT_SECRET!,
}),
},
);
const { access_token } = await tokenResponse.json();
// Use access_token for API calls.The returned token has the client’s service account as the subject. Grant roles to the service account in the client → Service accounts roles tab.
Common Mistake: Using password grant type (resource owner password credentials) for M2M. That’s for users with usernames/passwords — clients should use client_credentials.
Fix 8: User Federation and Identity Providers
For SSO with Google, GitHub, Microsoft, etc.:
- Realm → Identity providers → Add.
- Select the provider, paste OAuth/OIDC credentials.
- Users get a “Sign in with Google” button on the login page.
For LDAP / Active Directory backends:
- Realm → User federation → Add LDAP provider.
- Configure connection (URL, bind credentials, user DN search).
- Users are looked up from LDAP at login.
Keycloak can act as both an OIDC provider (for your apps) and an OIDC client (chaining to upstream providers). Useful for unifying SSO across multiple auth sources.
Pro Tip: For “users can sign in via Google or username/password,” configure Google as an identity provider. Keycloak handles the broker dance — your app just sees Keycloak.
Still Not Working?
A few less-obvious failures:
Invalid tokendespite correct configuration. Clock skew between Keycloak and your backend. Tokens haveiat/exp; differences over 30 seconds cause issues. Sync via NTP.Cookie too largeafter Keycloak login. Session cookies grow with roles. Limit roles per token or use opaque tokens (less common with Keycloak but possible via configuration).Realm not found. Realm name is case-sensitive.MyRealm≠myrealm.- First admin login after fresh install fails. Default credentials changed in newer versions. Check the Docker logs:
docker logs keycloakshows the admin password ifKEYCLOAK_ADMIN_PASSWORDisn’t set. Permission deniedfor admin actions. You’re logged into the wrong realm (e.g. the app realm, notmaster). Switch realms via the dropdown.- Session timeouts during long actions. Realm settings → Tokens → SSO Session Idle. Default ~30 min. Bump for kiosk-style apps.
- Themes not loading. Custom themes in
/opt/keycloak/themes/. Mount via Docker volume; requirestart --optimizedrebuild. - CORS works for some endpoints, not others. UserInfo and token endpoints have separate CORS settings. Web Origins handles authentication-related endpoints; advanced settings affect others.
For related authentication and identity issues, see Auth.js not working, Clerk not working, Passkey/WebAuthn not working, and CORS access-control-allow-origin.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Passkey / WebAuthn Not Working — rpId, Origin, Conditional UI, and Cross-Device Sign-In
How to fix passkey and WebAuthn errors — rpId mismatch, NotAllowedError, Conditional UI autofill not showing, attestation vs assertion, Safari userVerification quirks, and SimpleWebAuthn library integration.
Fix: gh CLI Not Working — Auth Scopes, Multiple Accounts, PR Create Errors, and Enterprise Hosts
How to fix GitHub CLI errors — gh auth login token scopes missing, multiple accounts switching, gh pr create permission denied, GHE host auth, gh repo clone vs git clone, and API rate limits.
Fix: NextAuth.js Not Working — Session Null, Callback Errors, or OAuth Redirect Issues
How to fix NextAuth.js (Auth.js) issues — session undefined in server components, OAuth callback URL mismatch, JWT vs database sessions, middleware protection, and credentials provider.