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.
A second source of confusion is the operational model. Keycloak was a WildFly/JBoss-based server for most of its history, and the migration to Quarkus (Keycloak 17–18) changed how you start it, how you tune the JVM, how options are named (env vars now use KC_ prefixes), and how realm exports/imports behave. Tutorials and Stack Overflow answers written before 2022 frequently reference standalone.sh, standalone-ha.xml, and -Dkeycloak.migration.action — none of which apply to current Keycloak. If you copy commands from an old article, they will silently no-op or fail to start.
The third subtle gotcha is the difference between dev and prod start modes. kc.sh start-dev disables HTTPS, sets a fake hostname, and turns off many production guards so you can experiment. kc.sh start is the production mode and requires you to configure KC_HOSTNAME, a real DB, and either HTTPS or KC_PROXY=edge for TLS termination. Many “it works on my laptop, breaks on the server” reports come from running start-dev locally and start in prod without setting up the prod-only options.
Version History: Why Keycloak Configs Drift Between Tutorials
Keycloak ships fast — knowing which release introduced which feature saves a lot of debugging:
- Keycloak 17 (February 2022) was the first release based on the new Quarkus distribution. The old WildFly distribution was still available alongside it.
- Keycloak 18 (May 2022) removed the WildFly distribution entirely. From this point on, all options use
KC_env vars or thekc.shCLI, andstandalone.shis gone. If you’re following a guide that mentions JBoss CLI orstandalone-ha.xml, it’s pre-18 and won’t work. - Keycloak 19–21 (2022–2023) stabilized the Quarkus runtime, switched the default DB driver to Liquibase-based migrations, and tightened hostname handling. The
--hostname-strict/--hostname-strict-httpsflags became required to opt out of production guards. - Keycloak 22 (September 2023) shipped Admin UI v2 as default, removing the legacy Admin UI. Workflows in screenshots from older blog posts no longer match what you see in the console. The new UI also reorganizes where Web Origins, Capability config, and Authentication flows live.
- Keycloak 23 (December 2023) added native OpenTelemetry support — traces and metrics from inside Keycloak now follow OTel conventions. If your existing observability stack uses OTel, you no longer need a separate exporter.
- Keycloak 24 (February 2024) brought FAPI 2.0 conformance updates (relevant if you do open-banking-style flows), strengthened device authorization flows, and reorganized the password policy engine.
- Keycloak 25 (June 2024) introduced OIDC for User Profile — user attributes can be exposed through standard OIDC scopes without writing custom mappers — and made declarative UI customization easier.
- Keycloak 26 (October 2024) and the 26.x line through 2025 continued tightening the persistent user sessions design, kept the in-memory
H2warnings louder, and finalized lightweight access tokens.
If an article tells you to “go to Realm settings → Themes,” verify in your version. The path is correct in 22+; older versions split themes between server-level and realm-level settings, and Quarkus-era versions changed how custom themes are mounted.
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.
Failed to determine hostnameonstart. Keycloak 22+ refuses to start in prod mode without an explicitKC_HOSTNAME. Either set it, or for non-prod testing pass--hostname-strict=false. Don’t ship the relaxed flag to production — it’s documented as unsafe.--optimizedstart fails with “augmentation required.”start --optimizedskips the build step at startup. If you change any build-time option (DB type, features, providers), runkc.sh buildfirst. The Docker image’s pattern is build duringdocker buildthen start with--optimized.- Tokens look right but downstream apps reject them. Keycloak 25+ supports lightweight access tokens, where most claims live in
userinfoinstead of inline. If your downstream expects a specific claim in the access token, either disable lightweight tokens on that client or move verification to calluserinfo.
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.