Files
han-app/codebase/backend/keycloak/README.md
T

99 lines
6.4 KiB
Markdown

# 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 migrations are 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.
- Every challenge snapshots code length, TTL, SMS-order timeout and settings version. Runtime OTP values are not read from environment variables.
- Mock mode is explicit and retains the configured test code. SMS mode generates a cryptographically secure numeric OTP, stores only its HMAC and orders delivery through `POST /internal/sms/v1/send`; Keycloak never calls or polls the provider.
- SMS mode requires `KEYCLOAK_SMS_SERVICE_URL` and an independent `KEYCLOAK_SMS_SERVICE_TOKEN`. No real credentials are committed.
## 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 explicit ordering/active/final statuses, settings snapshot and optional `sms_message_id`;
- `han_otp_send_counter`: durable 24-hour counter/cooldown per phone HMAC;
- `han_otp_security_event`: append-only send/verify outcomes with SMS correlation and validated device audit metadata, without raw phone or OTP.
Resend creates a new durable order and marks earlier active/ordering challenges as superseded. Verification accepts only active, unexpired challenges, locks the row, increments attempts, and atomically consumes a valid code. Provider delivery status never participates in verification.
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.
The provider schedules a once-per-minute expiry update and also performs lazy expiry on send and verify. The theme renders digit inputs and countdown from the challenge snapshot, submits a real resend action and carries optional `han_*` device metadata.
## SMS order behavior
`200` or `202` with a valid UUID `sms_message_id` and ISO-8601 `ordered_at` activates a real-mode challenge. Timeout, I/O failure or 5xx is retried once with the same `keycloak:challenge:{id}` idempotency key; final failure marks that challenge `order_failed`. The retry creates neither another challenge nor another send-counter increment.
Reserve, SMS HTTP order, and activation/order-failure run as separate transaction phases. The HTTP call holds no challenge/counter database lock, and every retry retains the same challenge id.
## 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.