Přeskočit na obsah

Keycloak Runbook

Keycloak je komplexní systém. Tato příručka pomáhá řešit nejčastější problémy.

Keycloak nemá na rozdíl od většiny ostatních služeb vlastní host port ani subdoménu — je dostupný výhradně přes Traefik pravidlo PathPrefix(/kc) na sdíleném web entrypointu (port 80, případně TRAEFIK_WEB_HOST_PORT, pokud je v .env přenastavený). Nevymýšlejte si pro něj port :8180 ani žádný jiný.

Příčina: Strapi admin panel posílá HS256 JWT, ale middleware čeká RS256 (z Keycloaku).

Řešení: KC middleware dekóduje header a přeskočí non-RS256 tokeny:

sencai.space/src/middlewares/keycloak-jwt.ts
const decoded = jwt.decode(token);
if (decoded?.header?.alg !== 'RS256') {
return next(); // Přeskočit JWKS validaci
}

Příčina: KC JWT má TTL 12 hodin. Refresh token vypršel.

Řešení: Frontend volá endpoint /auth/refresh:

async function refreshTokenIfNeeded() {
const decoded = jwtDecode(token.value);
const expiresIn = decoded.exp * 1000 - Date.now();
if (expiresIn < 5 * 60 * 1000) { // Méně než 5 minut
await $fetch('/api/auth/refresh', { method: 'POST' });
}
}

“audience mismatch” — 100% pádů přihlášení po přidání audience do jwt.verify()

Section titled ““audience mismatch” — 100% pádů přihlášení po přidání audience do jwt.verify()”

Příčina: Předání audience: 'sencai-frontend' do options jwt.verify() knihovny jsonwebtoken vypadá jako zjevný způsob, jak middleware svázat s frontend klientem — ve skutečnosti to ale rozbilo úplně každé přihlášení. Vestavěná kontrola audience v jsonwebtoken porovnává výhradně proti claimu aud — a výchozí aud u tokenů sencai-frontend v Keycloaku je vestavěný klient account, ne sencai-frontend. Klient, který token skutečně vyžádal, je zaznamenaný zvlášť, v claimu azp.

Řešení: Nepoužívat vestavěnou volbu audience v jsonwebtoken. Nejdřív dekódovat, pak klienta ověřit ručně proti oběma claimům, azp i aud:

// sencai.space/src/middlewares/keycloak-jwt.ts (~řádky 206-226)
// Vestavěná volba `audience` v jsonwebtoken kontroluje pouze claim `aud`,
// ale skutečný požadující klient je v `azp`. Vynucení `audience: 'sencai-frontend'`
// tady odmítlo úplně každý validní token (100% pádů přihlášení). Kontrola
// azp/aud se místo toho dělá ručně až po jwt.verify().
const azpMatches = decoded.azp === expectedClientId;
const audMatches = Array.isArray(decoded.aud)
? decoded.aud.includes(expectedClientId)
: decoded.aud === expectedClientId;
if (!azpMatches && !audMatches) {
logger.error('❌ Keycloak JWT audience/azp mismatch:', { azp: decoded.azp, aud: decoded.aud });
reject(new Error(`JWT audience/azp mismatch: expected client "${expectedClientId}"`));
}

Tato oprava je v keycloak-jwt.ts pořád doprovázená inline komentářem vysvětlujícím proč — neodstraňujte ruční kontrolu azp/aud a znovu nepřidávejte audience do options jwt.verify().

Příčina: User update ve Strapi → webhook → auth-service-consumer → KC sync → Strapi update → nekonečná smyčka.

Řešení: Použít guard runWithoutKcSync() postavený na AsyncLocalStorage:

src/lifecycles/user-kc-sync.ts
export function runWithoutKcSync<T>(fn: () => Promise<T>): Promise<T> {
return asyncLocalStorage.run({ skipKcSync: true }, fn);
}

“Username is email instead of KC UUID”

Section titled ““Username is email instead of KC UUID””

Příčina: Uživatel vytvořený před prvním KC přihlášením má username=email.

Řešení: KC JWT middleware automaticky aktualizuje username na KC sub:

if (decoded.preferred_username !== strapiUser.username) {
await strapi.entityService.update(
'plugin::users-permissions.user',
strapiUser.id,
{ data: { username: decoded.sub } }
);
}

Příčina: Keycloak se pomalu startuje (normální jev).

Řešení: Počkat 30–90 sekund. Health check:

Terminál
curl -f http://localhost/kc/health || exit 1

Příčina: KC_HTTP_RELATIVE_PATH není nastaveno na /kc.

Řešení: V docker-compose.auth.yml:

environment:
KC_HTTP_RELATIVE_PATH: "/kc"
KEYCLOAK_URL: "http://keycloak:8080/kc"

Příčina: Špatný grant type nebo realm.

Řešení: Použít password grant na realmu master s klientem admin-cli:

Terminál
POST /realms/master/protocol/openid-connect/token
client_id=admin-cli&
username=admin&
password=<password>&
grant_type=password

Příčina: Required action VERIFY_EMAIL není aktivní.

Řešení: Ve Strapi, když uživatel potvrdí email:

if (event.params.data.emailVerified) {
await removeKcRequiredAction(user.id, 'VERIFY_EMAIL');
}

“Can’t complete profile (UPDATE_PROFILE loop)”

Section titled ““Can’t complete profile (UPDATE_PROFILE loop)””

Příčina: Uživatel má required action UPDATE_PROFILE.

Řešení: Při vytváření KC uživatele nastavit firstName/lastName (fallback na '-'):

const kcUser = {
username: registration.email,
firstName: registration.name || '-',
lastName: registration.surname || '-',
requiredActions: ['VERIFY_EMAIL'], // Ne UPDATE_PROFILE
};

Příklady:

Terminál
# ŠPATNĚ — používá master realm
POST http://keycloak:8080/realms/master/...
# SPRÁVNĚ — pro user token (sencai realm)
POST http://keycloak:8080/realms/sencai/...
# SPRÁVNĚ — pro admin API (master realm)
POST http://keycloak:8080/realms/master/...
ClientRealmUse case
sencai-frontendsencaiPřihlášení v prohlížeči (public client)
auth-service-consumersencaiBackend volání KC admin API ze sync consumeru (confidential, client-secret)
admin-climasterAdmin API (password grant, master realm)

V importu realmu neexistuje žádný client sencai-backend — neodkazovat na něj. Import realmu žije v orchestration/config/keycloak/data/import/sencai-realm.json.

Řešení: Ověřit v Keycloak Admin Console:

  1. Jít na http://localhost/kc/admin
  2. Vybrat realm sencai
  3. Zkontrolovat Clients → existují sencai-frontend a auth-service-consumer

Řešení: Nakonfigurovat SMTP v Keycloaku:

  1. Realm sencai
  2. Realm settingsEmail
  3. Vyplnit SMTP: host, port, username, password
  4. Kliknout Save

Příčina: Redirect URI není nakonfigurovaný v KC clientu.

Řešení: V Keycloak Adminu je skutečný client sencai-frontend (z orchestration/config/keycloak/data/import/sencai-realm.json) nakonfigurovaný takto:

  1. Otevřít client sencai-frontend

  2. Valid redirect URIs:

    • http://localhost/* (přes Traefik — toto je primární lokální URL)
    • http://localhost:3000/* (přímý Nuxt dev server, mimo Traefik)
    • https://app.sencai.space/* (prod)
  3. Web Origins: nastaveno na wildcard +, který důvěřuje jakémukoliv originu implikovanému nakonfigurovanými redirect URI — nejde o ručně udržovaný seznam jednotlivých originů.

Řešení: Nejdřív zkontrolovat, jestli Web Origins stále drží wildcard +. Pokud někdo nahradil + explicitním seznamem originů, je tento seznam pravděpodobnou příčinou — zkontrolovat, jestli v něm nechybí origin, než přidávat nové. Na ruční vypisování originů přejít jen tehdy, pokud existuje konkrétní důvod, proč nelze použít +.

Terminál
docker logs -f sencai-keycloak-1 | grep -i error
Terminál
# Dekódovat token (bez verifikace)
echo "token123" | jq -R 'split(".") | .[1] | @base64d | fromjson'
Terminál
curl http://localhost/kc/health/ready
# 200 = ready
# 503 = startuje

Toto jsou problémy specifické pro auth-service-consumer (viz auth-service-consumer/CLAUDE.md), odlišné od middleware problémů na straně Strapi popsaných výše.

Příčina: KEYCLOAK_ADMIN_PASSWORD nastavené pro auth-service-consumer neodpovídá skutečnému admin heslu Keycloaku.

Řešení: Zajistit, aby KEYCLOAK_ADMIN_PASSWORD bylo identické mezi službou keycloak a auth-service-consumer v compose env (docker-compose.auth.yml / .env).

Příčina: Keycloak event dorazil do consumeru dřív, než odpovídající Strapi uživatel vůbec existoval (závod mezi registrací a zpracováním KC eventu).

Řešení: Consumer to zkouší znovu a navíc jako fallback hledá uživatele podle emailu, ne jen podle KC sub, než to vzdá.

Přetrvávající selhání po opakování

Section titled “Přetrvávající selhání po opakování”

Příčina: Zpracování dál selhává i po MAX_RETRY_ATTEMPTS (výchozí 5, RETRY_DELAY_MS výchozí 12000 ms).

Řešení: Zpráva skončí ve frontě user.fallback v RabbitMQ (durable: true, TTL 14 dní). Zkontrolovat ji tam a projít logy consumeru kvůli konkrétní chybě.

V současnosti neexistuje žádný instant push z Keycloaku do Strapi. Pokud je KC uživatel upravený přímo v Keycloak Admin Console, Strapi se to hned nedozví — sync probíhá pouze:

  • Při přihlášení, přes middleware keycloak-jwt (natáhne KC data uživatele a promítne je do Strapi), nebo
  • Na vyžádání, přes POST /api/auth/sync-from-kc (autentizovaný endpoint, stejná logika volaná ručně z frontendu).

Keycloak Event Listener SPI plugin (Java) pro instant KC→Strapi push je naplánovaný, ale neimplementovaný — je vedený jako odložená práce v auth-service-consumer/CLAUDE.md. Nepředpokládat, že úpravy provedené přímo v Admin Console se do Strapi propíšou dřív než při dalším přihlášení nebo explicitním sync volání.

  1. Běží KC?curl http://localhost/kc/health
  2. JDBC konekce? → Zkontrolovat docker logs sencai-keycloak-db-1
  3. Správný realm? → Ověřit v Admin Console
  4. Client existuje? → Zkontrolovat Clients (sencai-frontend, auth-service-consumer)
  5. JWT formát? → Dekódovat a ověřit iss, sub, exp, azp
  6. Email SMTP? → Zkontrolovat Realm settingsEmail
  7. Sync loop? → Ověřit runWithoutKcSync() v lifecycle
  8. Sync consumer zdravý? → Zkontrolovat logy auth-service-consumer a frontu user.fallback

Kompletní architekturu Keycloaku viz Root CLAUDE.md, implementaci syncu auth-service-consumer/.