Skip to content

Fix: Keycloak Not Working — Realm/Client Setup, OIDC, Token Verification, and CORS

FixDevs ·

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_uri

Or token verification fails in your backend:

JsonWebTokenError: invalid signature

Or 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 master realm 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

  1. Login as admin at https://keycloak.example.com/admin (default credentials admin/admin on first start — change immediately).
  2. Create a realm named myrealm (any non-master name).
  3. 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/logout

For 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/callback vs https://app.example.com/callback/ are different.
  • Port mismatch. http://localhost:3000 vs http://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 to https://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/keycloak

Wildcards * 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:

  1. Realm → Client settings → Web Origins → add your app’s origin.
  2. For multiple origins, list each (one per line).
  3. 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 (or mysql, mariadb).
  • KC_HOSTNAME = your public hostname. Without it, generated URLs may be wrong.
  • KC_PROXY = edge if 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):

  1. In the client, enable Client authentication + Service accounts roles.
  2. 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.:

  1. Realm → Identity providers → Add.
  2. Select the provider, paste OAuth/OIDC credentials.
  3. Users get a “Sign in with Google” button on the login page.

For LDAP / Active Directory backends:

  1. Realm → User federation → Add LDAP provider.
  2. Configure connection (URL, bind credentials, user DN search).
  3. 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 token despite correct configuration. Clock skew between Keycloak and your backend. Tokens have iat/exp; differences over 30 seconds cause issues. Sync via NTP.
  • Cookie too large after 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. MyRealmmyrealm.
  • First admin login after fresh install fails. Default credentials changed in newer versions. Check the Docker logs: docker logs keycloak shows the admin password if KEYCLOAK_ADMIN_PASSWORD isn’t set.
  • Permission denied for admin actions. You’re logged into the wrong realm (e.g. the app realm, not master). 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; require start --optimized rebuild.
  • 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles