Files
han-app/modules/module-08-keycloak.md
T

704 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# module-08. Проектная спецификация `keycloak`
> Статус: целевая production-спецификация MVP; реальный SMS provider не входит в scope.
> Источники: [`README.md`](README.md), [`arch-00-glossary.md`](arch-00-glossary.md), [`arch-01-system-architecture.md`](arch-01-system-architecture.md), [`arch-02-api-contracts.md`](arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md), [`module-01-api-backend.md`](module-01-api-backend.md), [`module-02-frontend-test-site.md`](module-02-frontend-test-site.md), [`module-03-nginx.md`](module-03-nginx.md), [`../../HAN_chat/deploy/init-managed-postgres.py`](../../HAN_chat/deploy/init-managed-postgres.py).
## 1. Назначение и границы
Keycloak — единственный IdP HAN Chat. MVP предоставляет регистрацию/вход только по подтверждённому номеру телефона и OTP, OIDC tokens, refresh/logout, discovery/JWKS и защиту auth flow.
Keycloak отвечает за:
- realm, users, credentials, auth sessions и token lifecycle;
- Authorization Code Flow with PKCE для Expo web/iOS/Android;
- нормализацию/уникальность телефона и claims;
- OTP authenticator/SPI, mock verification и продуктовые limits;
- brute-force, sessions, logout/revocation;
- keys/JWKS rotation и health/metrics.
Не отвечает за:
- `api-backend` bootstrap/consents/UserIdentity;
- App DB/profile/chat и CRM sync;
- API service-to-service tokens;
- пользовательскую UX-сессию;
- реальную отправку SMS в MVP.
Реальный SMS provider — строго extension point/TBD. Mock code является секретом окружения, не контентом UI и не логируется.
## 2. Топология и публичный URL
Keycloak работает за единственным root nginx:
```text
Client HTTPS https://tohin.ru/auth/*
→ nginx TLS termination
→ HTTP keycloak:8080 в закрытой Docker network
→ managed PostgreSQL schema keycloak по TLS
```
Публичный issuer обязан быть стабильным:
```text
https://tohin.ru/auth/realms/han-chat
```
OIDC discovery:
```text
https://tohin.ru/auth/realms/han-chat/.well-known/openid-configuration
```
JWKS — URI из discovery. `api-backend` проверяет `iss`, audience, signature, `exp/nbf` и `sub`, не вызывает Admin API в hot path.
## 3. Версия, image и providers
- Keycloak Quarkus distribution, поддерживаемая LTS/stable версия, закреплённая image digest.
- PostgreSQL JDBC driver из image.
- Custom Java provider JAR для phone OTP authenticator/settings bridge/counters.
- Сборка provider reproducible, зависимости pinned, SBOM/signature/security scan.
- Build-stage выполняет `kc.sh build`; runtime image immutable/non-root.
- Перед upgrade читаются Keycloak migration notes и SPI compatibility.
Версия Keycloak фиксируется в deployment manifest; `latest` запрещён.
## 4. Realm и clients
Realm: `han-chat`. Master realm не используется приложением.
### 4.1. Public frontend client
Канонический client id:
```text
han-chat-frontend
```
Настройки:
- public client; client authentication off;
- standard flow on;
- Authorization Code + PKCE `S256` обязательно;
- implicit flow off;
- direct access grants/password grant off;
- service accounts off;
- device flow off, если не нужен;
- consent screen Keycloak не заменяет продуктовые согласия API;
- exact redirect URIs и web origins;
- full scope allowed off; только назначенные scopes/mappers.
Примеры redirect URI должны перечисляться отдельно:
```text
https://tohin.ru/auth/callback
han-chat://auth/callback
<Expo native scheme/callback, exact value после сборки>
```
Production redirect URI задаются exact; wildcard не используется до отдельного security review. Development localhost origins/redirects находятся в отдельном dev realm/client либо profile и запрещены production.
### 4.2. API audience
Audience:
```text
han-chat-api
```
Client scope/audience mapper добавляет `aud=han-chat-api` в access token frontend. `api-backend` не принимает token только по `azp` без audience.
### 4.3. Optional confidential client
`han-chat-backend` можно импортировать disabled/optional для будущих admin/ops S2S:
- client authentication on, service account only при явном включении;
- secret не хранится в realm export;
- минимальные roles;
- не используется между текущими сервисами и не требуется для JWT validation;
- не участвует в пользовательском hot path.
Internal API по-прежнему используют service tokens из arch-02.
## 5. OTP-only phone flow
### 5.1. Browser flow
Отдельный flow `han-phone-otp-browser`:
1. Cookie/SSO authenticator проверяет действующую Keycloak session.
2. При отсутствии session показывается форма телефона.
3. `Phone Identity Authenticator` нормализует номер.
4. Проверяются realm brute-force и product send limits.
5. Создаётся/находится user по canonical phone identity.
6. `Phone OTP Challenge` инициирует mock/provider send.
7. Показывается форма OTP.
8. Проверяются TTL/attempt limits/constant-time hash or mock compare.
9. При успехе user enabled/phone verified, flow завершается code.
10. Frontend меняет code+verifier на tokens.
Password form, registration password, reset password, email OTP, social login и magic link отсутствуют.
### 5.2. Регистрация/find-or-create
До выдачи OTP новый user может существовать как short-lived pending identity либо создаваться после успешной проверки. Предпочтительное решение:
- normalized phone reservation/counter создаётся в SPI store;
- permanent Keycloak user создаётся/активируется только после успешного OTP;
- concurrent flow защищён unique phone index/transaction;
- abandoned pending challenges очищаются TTL.
Если Keycloak storage не позволяет безопасный custom unique index в managed schema, user создаётся disabled с deterministic username и очищается job; точная реализация покрывается concurrency tests.
### 5.3. Required actions
Используются только при реальной необходимости:
- `VERIFY_PHONE` — если user импортирован/номер изменён вне текущего verified flow;
- `UPDATE_PHONE` — будущий controlled flow с повторной OTP;
- terms/product consents не required action: версии и факт согласия хранит `api-backend`.
Required actions не должны предлагать пароль/email. После phone OTP обычный вход завершается без лишнего profile screen.
## 6. Нормализация и уникальная identity
Телефон парсится libphonenumber:
- Unicode digits/NFKC input normalization;
- default region `RU` допустим только для национального ввода; международные номера поддерживаются по product policy;
- canonical storage/claim — E.164, например `+79001234567`;
- invalid/impossible number отклоняется до send;
- отображение только masked;
- canonical phone comparison exact.
Рекомендуемая модель:
- `username` = canonical E.164 либо irreversible deterministic identifier;
- user attribute `phone_number` = E.164;
- `phone_number_verified=true`;
- unique phone enforced storage-level, не только pre-check;
- email nullable/не используется.
Утечка существования номера запрещена: initiate/challenge возвращают одинаковый внешний текст/timing class для нового/существующего пользователя. Один verified phone соответствует одному active `sub`. Merge/reassignment — отдельная administrative policy, не автоматический side effect login.
Изменение телефона требует re-auth + OTP нового номера и invalidation sessions/tokens по policy. `api-backend` обновляет cached phone при следующем bootstrap/login claim; прямого вызова Keycloak DB нет.
## 7. Mock OTP
Env:
```text
KEYCLOAK_OTP_MOCK_ENABLED=true
KEYCLOAK_OTP_MOCK_CODE=<secret>
```
Правила:
- mock разрешён MVP production-like только как явно принятый риск;
- пустой/default `1234` запрещён startup policy для production-like, если не согласован secret;
- code не входит в realm import, frontend config, HTML hint, API response, logs, metrics, traces или audit;
- сравнение constant-time;
- challenge всё равно имеет TTL, max verify attempts и counters, чтобы flow был близок production;
- code не сохраняется per-user в открытом виде;
- UI сообщает только «тестовый режим», без кода;
- `KEYCLOAK_OTP_MOCK_ENABLED=false` при отсутствии configured provider делает OTP flow fail-closed/not-ready, а не пропускает проверку.
Реальный provider interface:
```java
interface OtpDeliveryProvider {
DeliveryResult send(E164Phone phone, String otp, Duration ttl, Correlation ctx);
}
```
Будущий provider обязан вернуть `provider_message_id`; raw OTP не логируется. Выбор provider, template, sender, delivery status webhook и vendor credentials — TBD.
## 8. OTP challenge и counters
Даже в mock:
- challenge id random ≥128 bit;
- OTP не хранится raw; production-generated code — keyed hash/HMAC с challenge salt/pepper;
- TTL (предлагается 5 минут) — technical security parameter;
- one-time use; success atomically consumes challenge;
- max verification attempts per challenge;
- resend invalidates либо version-binds предыдущий challenge;
- replay/parallel verify безопасны;
- destination stored masked/hash where possible.
Audit fields по arch-05: provider message id (для mock — synthetic non-secret), sent_at, destination_masked, otp_hash/reference, attempts, outcome. Никогда raw code.
### 8.1. Product send limits bridge
SPI вызывает:
```text
GET http://api-backend:8000/internal/settings/v1/otp
Authorization: Bearer ${KEYCLOAK_SETTINGS_BRIDGE_TOKEN}
```
Ответ:
```json
{
"max_send_attempts_per_24h": 3,
"min_seconds_between_attempts": 30,
"version": "2026-07-10T08:00:00Z",
"cache_ttl_seconds": 60
}
```
Это единственный путь к `otp.phone.*`; Keycloak не получает GRANT на `han_app`. SPI поддерживает ETag/cache, single-flight refresh. Bridge down:
- использовать last-known-good до bounded max stale;
- если cache пуст/слишком стар — fail-closed для send;
- verify уже выданного challenge может продолжаться по snapshot, с которым challenge создан.
Token name точно `KEYCLOAK_SETTINGS_BRIDGE_TOKEN`, endpoint точно `/internal/settings/v1/otp`.
### 8.2. Где хранятся counters
Решение MVP: counters/challenges хранятся в Keycloak-owned PostgreSQL tables/provider storage в схеме `keycloak`, а не в Redis API и не в `han_app`.
Причины:
- durable across restart;
- одна transaction для reserve/send-attempt/consume;
- не добавляет Keycloak credentials к общему Redis;
- соответствует границе «счётчики в зоне Keycloak/SPI».
Используются phone HMAC, не E.164 в key/index для rate data. Tables provider-owned создаются versioned migration provider-а, не ручным DDL-on-start.
Минимальные records:
- `han_otp_challenge`: id, phone_hmac, otp_hash/mock marker, created/expires/consumed, verify attempts, settings version, provider id/status;
- `han_otp_send_counter`: phone_hmac, window_start, count, last_sent_at;
- `han_otp_security_event`: append-only minimal outcome/retention.
Indexes: unique active challenge policy, `(phone_hmac,window_start)`, `(expires_at)`. Cleanup bounded job. Доступ только `keycloak_user`.
## 9. Brute-force и abuse
Слои:
1. nginx `/auth` IP rate limit (`NGINX_RATE_LIMIT_AUTH`);
2. Keycloak realm brute-force detection;
3. SPI product send limits per phone HMAC;
4. verify-attempt limit per challenge/phone/IP hash;
5. cooldown after repeated failures;
6. CAPTCHA/risk engine — future extension.
Realm включает brute-force protection с temporary lockout и bounded wait. Permanent lockout для consumer phone login без recovery runbook нежелателен. Error messages не различают unknown phone/wrong code/locked account сверх безопасной UX причины. `Retry-After`/remaining time выдаётся только если не помогает enumeration.
IP берётся только из trusted proxy chain; Keycloak настроен доверять forwarded headers от root nginx.
## 10. Claims и token contract
Access token минимум:
| Claim | Значение |
|---|---|
| `iss` | `https://tohin.ru/auth/realms/han-chat` |
| `sub` | immutable Keycloak user id |
| `aud` | включает `han-chat-api` |
| `azp` | `han-chat-frontend` |
| `exp`, `iat`, `nbf` | стандартные |
| `sid` | session id, если поддерживается |
| `auth_time` | время auth |
| `acr`/`amr` | отражает phone OTP |
| `phone_number` | canonical E.164 |
| `phone_number_verified` | `true` |
| `scope` | only allowed scopes |
`preferred_username` может совпадать с phone для compatibility, но канонический claim API — `phone_number`; module-01 допускает fallback только если E.164.
ID token предназначен client login state; API принимает access token, не ID token. Refresh token непрозрачен для приложения и хранится frontend secure storage.
PII minimization: full phone нужен API bootstrap по зафиксированному контракту, но не добавляется в service tokens/metrics/logs. Roles/groups выдаются только если используются authorization policy.
## 11. Signing keys, JWKS и rotation
- asymmetric signing, RS256 MVP; `none`/HS algorithms запрещены;
- active signing key + passive previous keys до истечения всех выпущенных tokens/grace;
- keys генерируются/хранятся Keycloak, private material не в realm export/repo;
- JWKS публичен через issuer;
- rotation rehearsed; `kid` меняется, API controlled-refresh cache;
- emergency compromise: disable key, revoke sessions, force re-login, alert/runbook;
- backup/restore учитывает realm keys.
Rotation interval и HSM/keystore — ops TBD. Изменение algorithm требует совместного rollout API verifier.
## 12. Token и session lifecycle
Предлагаемые MVP значения, окончательно принять security/product review:
- access token lifespan: 5 минут;
- SSO session idle: 30 дней;
- SSO session max: 90 дней;
- refresh token следует session limits;
- authorization code: 1 минута;
- login action: 5 минут;
- client session idle/max согласованы с SSO;
- clock skew минимальный.
Refresh:
- revoke refresh token on use / refresh token rotation включены;
- max reuse `0` или минимально поддерживаемое значение;
- frontend применяет single-flight, поэтому parallel refresh не требуется;
- reuse старого refresh token → `invalid_grant`, возможная session revocation/security event;
- offline tokens не выдаются.
Access token не хранится server-side и живёт до exp; критическая блокировка пользователя сопровождается logout/revocation/not-before policy.
## 13. Logout, revocation и browser cookies
Frontend вызывает OIDC end-session/logout с valid post-logout redirect, затем всегда очищает local tokens. Back-channel logout можно включить для clients, которые его поддержат; API JWT hot path не хранит browser session.
Cookies Keycloak:
- `Secure`, `HttpOnly`;
- SameSite согласно redirect/iframe requirements, по умолчанию `Lax`;
- domain/path минимальны (`/auth`/host);
- third-party cookie dependency не закладывается;
- session fixation предотвращается Keycloak;
- admin console cookies не расширяются на frontend origins.
Front-channel iframe checks не должны заставлять ослабить CSP всего сайта. Native logout использует system browser и app-link/custom scheme validation.
## 14. CORS, origins и redirects
- exact `Web Origins`: `https://tohin.ru`;
- no wildcard `*` with credentials;
- native apps не получают произвольные web origins;
- valid redirects exact/safely scoped;
- redirect URI comparison не допускает open redirect;
- post-logout redirects отдельно allow-listed;
- nginx и Keycloak CORS не должны дублировать противоречащие headers;
- token endpoint используется PKCE client без client secret;
- admin endpoints не CORS-доступны приложению.
Любой новый environment имеет отдельный host/client config, а не production wildcard.
## 15. Reverse proxy и hostname
Ключевые настройки (точные CLI names проверяются по закреплённой версии):
```text
KC_HTTP_ENABLED=true
KC_HTTP_PORT=8080
KC_PROXY_HEADERS=xforwarded
KC_HOSTNAME=https://tohin.ru/auth
KC_HTTP_RELATIVE_PATH=/auth
KC_HOSTNAME_STRICT=true
KC_HOSTNAME_STRICT_HTTPS=true
```
Если выбран другой поддержанный pattern (`hostname` без path + relative path), итоговые issuer/endpoints обязаны совпасть с `KEYCLOAK_PUBLIC_URL`.
Nginx передаёт trusted `Host`, `X-Forwarded-Proto=https`, `X-Forwarded-Host`, `X-Forwarded-Port=443`, real IP. Keycloak не доступен напрямую с host/public network, поэтому spoofed forwarded headers не принимаются извне.
Admin hostname/path рекомендуется ограничить ops network/VPN; публично нужны только realm/OIDC/login assets. Если разделить admin hostname невозможно MVP, admin console защищается network allow-list и сильным admin auth.
## 16. PostgreSQL `keycloak`
Используется:
```text
KEYCLOAK_DB_URL=jdbc:postgresql://.../han_chat?...&currentSchema=keycloak
KC_DB_URL_PROPERTIES=currentSchema=keycloak
```
Role `keycloak_user` имеет доступ только к schema `keycloak`; нет доступа `han_app`, `bitrix_*`, `message_safety`. Connection только private VPC + TLS verify.
Pool:
- bounded initial/min/max;
- acquisition/query/connect timeout;
- leak detection/metrics;
- pool max определяется load test и managed PG limit;
- `application_name=keycloak`.
Keycloak управляет своей стандартной schema migration. Custom provider tables имеют отдельную versioned migration strategy, совместимую с startup/rolling upgrade; DDL не выполняется бесконтрольно каждым replica.
Нельзя редактировать стандартные Keycloak tables вручную или Alembic-миграциями Python-сервисов.
## 17. Admin bootstrap и realm import
### 17.1. Bootstrap admin
- `KC_BOOTSTRAP_ADMIN_USERNAME`/password или актуальный bootstrap mechanism только на первом запуске;
- password генерируется strong secret, не коммитится и после bootstrap ротируется/удаляется из runtime env;
- admin user не используется приложением;
- отдельные named admin accounts/least privilege для ops;
- MFA для admin обязательно до production, независимо от consumer phone flow;
- admin events audit включён.
### 17.2. Realm import
Репозиторий:
```text
keycloak/
realm/han-chat-realm.json.template
providers/han-phone-otp-provider.jar
themes/han-phone/
migrations/
scripts/{render-realm,validate-realm,export-realm}.sh
tests/
Dockerfile
docker-compose.yml
```
Export/template содержит realm/client/flow/scopes/policies, но не:
- client/admin/provider secrets;
- mock code;
- private signing keys;
- environment-specific production credentials.
Import автоматически допустим для clean local/test. Production changes применяются controlled declarative job/Admin API procedure с diff/backup, не `--import-realm` поверх живого realm без проверки. Drift detection сравнивает безопасный desired subset.
## 18. Settings и secrets
Канонические:
```text
KEYCLOAK_PUBLIC_URL=https://tohin.ru/auth
KEYCLOAK_INTERNAL_URL=http://keycloak:8080
KEYCLOAK_REALM=han-chat
KEYCLOAK_AUDIENCE=han-chat-api
KEYCLOAK_DB_URL=jdbc:postgresql://...
KC_DB_URL_PROPERTIES=currentSchema=keycloak
KEYCLOAK_OTP_MOCK_ENABLED=true
KEYCLOAK_OTP_MOCK_CODE=<secret>
KEYCLOAK_SETTINGS_BRIDGE_TOKEN=<secret>
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
```
Дополнительные Keycloak-standard env version-specific (`KC_DB`, hostname/proxy/health/metrics/pool) фиксируются в `.env.example` после выбора image. Секреты только root `.env`/secret mounts с минимальными permissions.
Product limits `otp.phone.*` не дублируются env. OTP TTL/max verify attempts — security technical config provider-а; их имена нужно добавить в arch-04 до реализации, например:
```text
KEYCLOAK_OTP_TTL_SEC=300
KEYCLOAK_OTP_MAX_VERIFY_ATTEMPTS=5
KEYCLOAK_OTP_SETTINGS_MAX_STALE_SEC=300
KEYCLOAK_OTP_HMAC_KEY=<secret>
```
## 19. Health, readiness и startup
Keycloak management health endpoints включены. Compose проверяет liveness/startup; readiness требует:
- server started;
- DB reachable/schema migration complete;
- realm/client/auth flow/provider loaded;
- active signing key;
- settings bridge last-known-good для OTP send;
- mock enabled с valid secret либо реальный provider configured.
Стандартный Keycloak health сам не знает business provider state; custom provider readiness check/sidecar/synthetic internal check дополняет его. Public synthetic проверяет discovery/JWKS и authorization endpoint без отправки OTP.
DB/settings failure не должен приводить к выдаче tokens без OTP. OTEL/metrics outage не блокирует login.
## 20. Logging, metrics, tracing и audit
### Logs
JSON/stdout:
- service/version/environment, event/category;
- request/trace id, realm/client, safe flow step;
- result/error code, duration;
- phone только HMAC/masked при необходимости.
Запрещены raw OTP/mock code, phone, access/refresh/code, cookies, Authorization, client/admin secret, password, form body, redirect query с `code`, DB URL.
Keycloak access log должен редактировать sensitive query. TRACE/DEBUG production выключены.
### Events/audit
Включаются login/login_error, logout, refresh/revoke, user create/disable, phone verify/change, brute-force/OTP limit, admin config changes. Retention/consumer определяется ops/legal; event payload минимален.
### Metrics
- login/OTP send/verify success/failure/latency;
- limit/lockout rejects;
- settings cache age/refresh failures;
- active sessions/token refresh/error;
- DB pool/JVM/GC/HTTP;
- JWKS/key age;
- provider mode info (`mock`, later vendor), без phone labels.
### Tracing
OTEL support зависит от версии; HTTP/provider/settings bridge spans добавляются instrumentation без secrets. Если native tracing недостаточно, сохраняются request/trace correlation headers. Наблюдаемость не меняет auth outcome.
## 21. Backup, restore и disaster recovery
- managed PostgreSQL daily backup + PITR;
- realm config export хранится versioned и secret-free;
- signing key/private realm state входит в protected DB backup;
- provider JAR/theme/image reproducible из repo/artifacts;
- restore rehearsal в isolated environment;
- после restore проверяются issuer, keys/JWKS, clients/flows, user/session consistency, provider tables;
- RPO/RTO фиксируются ops до production.
При восстановлении в другой host нельзя случайно выдать production tokens с неверным issuer. DNS/TLS/hostname проверяются до открытия traffic. Backup encrypted/access-controlled; OTP expired rows очищаются по TTL.
## 22. Миграции и upgrades
Порядок:
1. прочитать release notes и supported DB upgrade path;
2. backup/PITR checkpoint;
3. проверить provider SPI/API compatibility и пересобрать JAR;
4. прогнать upgrade clone БД;
5. contract/E2E login+refresh+logout;
6. staged maintenance/rolling rollout только если версия поддерживает cluster compatibility;
7. проверить schema migration, realm drift, JWKS;
8. rollback приложения возможен только если DB schema backward-compatible; иначе restore/forward-fix runbook.
Нельзя пропускать major versions произвольно. Realm changes versioned отдельно. Custom provider migration имеет собственный version table/compatibility matrix.
## 23. Docker/runtime hardening
- service `keycloak`, `expose: 8080` и management port только internal;
- networks `public` (только nginx access при необходимости), `backend`, `observability`;
- без host `ports`;
- non-root, read-only rootfs где совместимо, tmpfs для temp;
- no-new-privileges/drop capabilities;
- memory/CPU/JVM heap limits, graceful termination;
- startup/readiness probes с достаточным initial period;
- immutable provider/theme mounts/image;
- no local persistent DB volume.
TLS client→nginx; Keycloak→managed PG TLS. Internal nginx→Keycloak HTTP допустим только на закрытой сети одной VM.
## 24. Failure semantics
| Сбой | Поведение |
|---|---|
| DB down | not-ready; login/refresh fail; existing access tokens проверяются API до exp по cached JWKS |
| settings bridge down, cache valid | send limits по last-known-good |
| settings bridge down, cache empty/stale | new OTP send fail-closed |
| mock secret missing/invalid | startup/not-ready; OTP не bypass |
| SMS mode без provider | not-ready `otp_provider_unconfigured` |
| wrong OTP | generic error, increment counter |
| too many sends/verifies | temporary reject/lockout, safe UX |
| token signing key rotation | old keys passive в JWKS grace |
| OTEL down | auth работает, telemetry drop metric/local log |
| provider exception | flow fail-closed, generic error/request id |
Не должно быть fallback на password или «успешный OTP» при инфраструктурной ошибке.
## 25. Тестовая матрица
### Unit provider
- E.164 normalization across RU/international/Unicode;
- invalid/impossible phone;
- unique concurrent reservation;
- mock constant-time compare/redaction;
- challenge TTL/one-time/replay/concurrent verify;
- send/verify limits and window boundaries;
- settings cache/ETag/stale/fail-closed;
- phone HMAC/counter cleanup;
- provider SPI error mapping.
### Realm/config contract
- only standard code+PKCE S256;
- password/direct/implicit/social disabled;
- exact origins/redirect/logout URIs;
- audience/claims/issuer;
- token/session TTL and refresh rotation;
- browser flow executions/required actions;
- no secrets/private keys in realm export.
### Integration
- managed/test PostgreSQL schema/currentSchema/TLS;
- restart preserves counters/challenges;
- Keycloak upgrade/provider migration;
- settings bridge token/path with module-01;
- JWKS rotation and API validation;
- disabled user/revocation/not-before;
- brute-force lockout/recovery;
- proxy hostname/path builds correct external URLs.
### E2E
- new phone → mock OTP → PKCE tokens → API bootstrap;
- existing user login; valid refresh without OTP;
- expired/revoked/rotated refresh → re-auth;
- wrong/expired/replayed code;
- max sends/min interval/max verifies;
- concurrent tabs/refresh single-flight assumptions;
- logout web/native;
- DB/settings outage;
- no phone/OTP/token in logs, URLs or metrics;
- admin endpoint inaccessible publicly.
### Security
- redirect/open redirect, PKCE downgrade, state/nonce;
- user enumeration/timing;
- cookie flags/CSRF on auth forms;
- forwarded header spoofing;
- brute-force/IP/phone distributed attempts;
- JWT alg/aud/iss/kid attacks;
- secret scanning/image/SBOM/provider dependency review.
## 26. Definition of Done
- Keycloak доступен за `/auth`, issuer/discovery/JWKS стабильны;
- realm/client topology и PKCE S256 зафиксированы declaratively;
- только phone OTP; password/implicit/direct/social отключены;
- phone canonical E.164 и storage-level unique;
- claims соответствуют module-01 (`sub`, `phone_number`, audience);
- mock secret only env, не логируется/не отдаётся;
- OTP challenges/counters durable в Keycloak schema;
- product limits читаются только через canonical settings bridge/token;
- brute-force, TTL, verify attempts и enumeration protection работают;
- refresh rotation/reuse detection/logout/revocation покрыты;
- proxy/redirect/origin/CORS/cookies/TLS boundaries проверены;
- DB role/schema/backup/restore/upgrade runbooks готовы;
- health/metrics/logging/tracing не раскрывают secrets/PII;
- container hardening/root Compose без published port;
- test matrix зелёная;
- реальный SMS явно остаётся extension point, не скрытой заглушкой.
## 27. Решения, допущения и TBD
**Решения:**
- K1: realm `han-chat`, public client `han-chat-frontend`, audience `han-chat-api`.
- K2: Authorization Code + PKCE S256; остальные user grants выключены.
- K3: canonical identity/claim — E.164 `phone_number`; `sub` immutable.
- K4: OTP authenticator/provider SPI; mock code только secret env.
- K5: counters/challenges в provider-owned PostgreSQL schema `keycloak`, не API Redis.
- K6: product limits только `/internal/settings/v1/otp` + `KEYCLOAK_SETTINGS_BRIDGE_TOKEN`.
- K7: refresh rotation/revoke-on-use; frontend single-flight.
- K8: real SMS provider — extension point/TBD.
**Допущения:**
- A1: единый public host `tohin.ru` и relative path `/auth`.
- A2: Keycloak version поддерживает нужные hostname/proxy/health options; точные names pin после выбора image.
- A3: product допускает mock OTP в первой production-like среде как временный риск.
- A4: телефон в access token необходим API bootstrap и защищён TLS/short token TTL.
**TBD:**
- K-TBD1: выбрать/pin Keycloak version и проверить custom SPI compatibility.
- K-TBD2: окончательные redirect URI для Expo iOS/Android и universal/app links.
- K-TBD3: финальные token/session TTL и brute-force thresholds после security review.
- K-TBD4: exact schema/migration mechanism provider tables без вмешательства в standard schema.
- K-TBD5: admin MFA/ops access topology и отдельный admin hostname.
- K-TBD6: signing-key rotation interval/HSM и emergency revocation.
- K-TBD7: RPO/RTO/event retention/legal deletion.
- K-TBD8: SMS vendor, credentials, templates, sender, delivery receipts and failover.
- K-TBD9: CAPTCHA/risk scoring после mock.
- K-TBD10: добавить proposed OTP technical env в arch-04 до реализации.