# HAN Chat Keycloak Production-like Keycloak 26.1.4 image and realm for OTP-only phone authentication. The module is self-contained and does not publish host ports; root nginx must proxy `/auth/*` to `keycloak:8080`. ## Security contract - Realm `han-chat`; public client `han-chat-frontend`. - Authorization Code flow only, mandatory PKCE S256; implicit, password/direct, device and service-account grants are disabled. - Access tokens contain audience `han-chat-api`, canonical E.164 `phone_number` and boolean `phone_number_verified`. - Access token lifetime is 5 minutes. Refresh token rotation is enabled with max reuse `0`; SSO idle/max are 30/90 days. - Realm brute-force protection uses temporary bounded lockouts. - OTP challenges, send counters and security events are stored in provider-owned PostgreSQL tables in the Keycloak schema. Liquibase migration `han-otp-1.0.0` is applied by Keycloak's JPA entity provider. - OTP and phone values are never logged. Durable rate records use HMAC-SHA256 phone identifiers; challenge verification uses HMAC and constant-time comparison. - Settings are fetched only from `GET /internal/settings/v1/otp` with `Authorization: Bearer ${KEYCLOAK_SETTINGS_BRIDGE_TOKEN}`. ETag/cache and bounded last-known-good are supported; an empty or stale cache fails closed. - Mock mode is explicit. Startup rejects missing values, code `1234`, codes shorter than six characters, and HMAC keys shorter than 32 bytes. Disabling mock mode without a real delivery provider fails startup. ## Build and test Requires Java 21 and Maven 3.9: ```bash mvn -B -ntp clean verify docker build -t han-chat/keycloak:26.1.4-otp-1.0.0 . ``` The Maven build shades only libphonenumber into the provider JAR; Keycloak SPI dependencies remain provided by the pinned server image. ## Configuration Copy values from `.env.example` into the root backend `.env`; never commit `.env`. Generate independent random values for admin password, mock code, OTP HMAC key and settings bridge token. Before production deployment replace the explicit placeholder entries in `realm/han-chat-realm.json`: - `https://APP_LINK_HOST.example/auth/callback` - `https://APP_LINK_HOST.example/auth/logout` - `https://APP_WEB_ORIGIN.example` Use exact Expo universal/app links and web origins. Do not replace them with wildcards. `https://tohin.ru/auth/callback` and `han-chat://auth/callback` are already allow-listed. The JDBC URL must use the managed PostgreSQL private endpoint, TLS verification and `currentSchema=keycloak`. The database role must have privileges only on schema `keycloak`. ## Runtime For standalone validation: ```bash docker compose --env-file .env up --build ``` The service exposes only Docker-network ports: - application HTTP: `8080`, relative path `/auth`; - management health and metrics: `9000`; - readiness: `GET http://keycloak:9000/auth/health/ready`; - liveness: `GET http://keycloak:9000/auth/health/live`; - Prometheus metrics: `GET http://keycloak:9000/auth/metrics`. Only nginx may publish external ports. Preserve `Host`, `X-Forwarded-Proto=https`, `X-Forwarded-Host`, `X-Forwarded-Port=443` and the trusted client IP chain. ## Realm lifecycle `--import-realm` is suitable for a clean environment. It does not safely reconcile an existing production realm. For changes to a live realm: 1. take a managed PostgreSQL backup/PITR checkpoint and export the current realm without users/secrets; 2. compare the desired safe subset (clients, scopes, flows, token policy); 3. apply through a controlled admin job or Admin API procedure; 4. verify discovery issuer, JWKS, PKCE login, refresh rotation and logout; 5. retain old passive signing keys until all tokens signed by them expire. Private signing keys are generated and stored by Keycloak and are absent from the realm JSON. ## OTP data and operations Provider tables: - `han_otp_challenge`: expiring, one-time challenges with optimistic version and pessimistic verification lock; - `han_otp_send_counter`: durable 24-hour counter/cooldown per phone HMAC; - `han_otp_security_event`: append-only minimal outcomes without raw phone or OTP. Resend marks an earlier active challenge as superseded. Verification locks a challenge row, increments attempts, and atomically consumes a valid challenge, preventing replay and parallel double use. Expired challenge and old security-event retention should be removed by a scheduled database maintenance job executed with the Keycloak schema role. Recommended retention is 24 hours for expired challenges/counters and the legally approved audit retention for security events. Cleanup must run in bounded batches and must not alter standard Keycloak tables. ## Release and recovery Before upgrading Keycloak, read migration notes, rebuild the provider against the exact target SPI version, test on a database clone, and execute OTP login/refresh/logout contract tests. Do not skip major versions without a supported path. Backups must include the full Keycloak schema (realm signing keys and provider tables). After restore verify issuer `https://tohin.ru/auth/realms/han-chat`, JWKS, client redirects, browser flow binding, challenge persistence and refresh revocation before opening traffic.