Реализована интеграция с СМС провайдером
This commit is contained in:
@@ -29,11 +29,13 @@
|
||||
| `BITRIX_API_FORWARD_TOKEN` | — | `bitrix-local-app` (исходящий) | то же | `Authorization: Bearer` |
|
||||
| `BITRIX_SYNC_SERVICE_TOKEN` | `bitrix-sync` | ops / мониторинг | `GET /internal/sync/v1/*` | `Authorization: Bearer` или `X-Service-Token` |
|
||||
| `KEYCLOAK_SETTINGS_BRIDGE_TOKEN` | `api-backend` | Keycloak SPI | `GET /internal/settings/v1/otp` | `Authorization: Bearer` |
|
||||
| `SMS_SERVICE_TOKEN` | `sms-service` | Keycloak SPI | `POST/GET /internal/sms/v1/*` | `Authorization: Bearer` |
|
||||
|
||||
Пары значений (должны совпадать):
|
||||
|
||||
- `BITRIX_LOCAL_APP_INTERNAL_TOKEN` (api-backend) = `BITRIX_INTERNAL_API_TOKEN` (bitrix-local-app)
|
||||
- `BITRIX_API_FORWARD_TOKEN` (bitrix-local-app) = `BITRIX_API_INBOX_TOKEN` (api-backend)
|
||||
- `KEYCLOAK_SMS_SERVICE_TOKEN` (Keycloak) = `SMS_SERVICE_TOKEN` (`sms-service`)
|
||||
|
||||
Генерация: `openssl rand -hex 32`. Секреты не коммитить.
|
||||
|
||||
@@ -340,6 +342,7 @@ Transport: **WebSocket** over HTTPS (`wss://`), JWT в query `?access_token=` и
|
||||
| `message-safety` | `message-safety/openapi.yaml` | нет (internal) |
|
||||
| `bitrix-local-app` | `bitrix-local-app/openapi.yaml` | частично (`/bitrix/*`, health) |
|
||||
| `bitrix-sync` | `bitrix-sync/openapi.yaml` | нет (internal + webhook) |
|
||||
| `sms-service` | `sms-service/openapi.yaml` + callback JSON Schema | internal send/read; публичен только exact callback |
|
||||
|
||||
Правила:
|
||||
|
||||
@@ -353,9 +356,58 @@ Keycloak SPI получает product limits OTP из `app_settings` через
|
||||
|
||||
| Контракт | Владелец | Потребитель | Назначение | Защита |
|
||||
|---|---|---|---|---|
|
||||
| `GET /internal/settings/v1/otp` | `api-backend` | Keycloak SPI | `otp.phone.max_send_attempts_per_24h`, `otp.phone.min_seconds_between_attempts`, `otp.phone.max_verify_attempts`, cache metadata | internal network + Bearer `KEYCLOAK_SETTINGS_BRIDGE_TOKEN` |
|
||||
| `GET /internal/settings/v1/otp` | `api-backend` | Keycloak SPI | OTP limits + `code_length`, `ttl_seconds`, `sms_order_timeout_ms`, cache metadata | internal network + Bearer `KEYCLOAK_SETTINGS_BRIDGE_TOKEN` |
|
||||
|
||||
Ответ не содержит секретов и PII. При недоступности endpoint Keycloak SPI использует последнее валидное cached value; если cache пустой — fail-closed для выдачи OTP.
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"max_send_attempts_per_24h": 3,
|
||||
"min_seconds_between_attempts": 30,
|
||||
"max_verify_attempts": 5,
|
||||
"code_length": 6,
|
||||
"ttl_seconds": 60,
|
||||
"sms_order_timeout_ms": 3000,
|
||||
"version": "2026-07-22T14:00:00Z",
|
||||
"cache_ttl_seconds": 60
|
||||
}
|
||||
```
|
||||
|
||||
Ответ не содержит секретов и PII. Challenge сохраняет snapshot `code_length`, `ttl_seconds` и `version`. При недоступности endpoint Keycloak SPI использует последнее валидное cached value; если cache пустой — fail-closed для выдачи нового OTP.
|
||||
|
||||
## Keycloak SPI ↔ `sms-service`
|
||||
|
||||
Контракт действует в real mode; в mock mode Keycloak не вызывает `sms-service`. API доступен только в закрытой сети `backend`, Bearer token — парные `KEYCLOAK_SMS_SERVICE_TOKEN`/`SMS_SERVICE_TOKEN`. Caller v1 фиксирован как `keycloak`, process/template — `auth_otp`, channel — `SMS`, provider — `idgtl`; эти поля не доверяются request body.
|
||||
|
||||
### `POST /internal/sms/v1/send`
|
||||
|
||||
```json
|
||||
{
|
||||
"idempotency_key": "keycloak:challenge:<CHALLENGE_ID>",
|
||||
"template_code": "auth_otp",
|
||||
"locale": "ru",
|
||||
"phone_e164": "+79001234567",
|
||||
"substitutions": {"code": "<OTP>", "ttl_min": "<TTL_MIN>"},
|
||||
"customer_ref": "<CHALLENGE_ID>",
|
||||
"message_ttl_sec": 60
|
||||
}
|
||||
```
|
||||
|
||||
- Строгая проверка E.164, TTL Direct `60..86400`, locale и точного набора placeholders; неизвестный/пропущенный placeholder → `422 sms_request_invalid`.
|
||||
- В одной transaction рендерится active approved `sms_template` и создаётся `sms_outbound_message` (`pending`/`unknown`); внешний Direct API в request handler не вызывается.
|
||||
- Новый durable order → `202` с `sms_message_id`, `ordered_at`; идемпотентный повтор с тем же fingerprint → `200` и тот же id; тот же key с другим payload → `409 idempotency_key_reused`.
|
||||
- Остальные коды: `401 unauthorized`, `429 rate_limit_exceeded`, `503 sms_service_unavailable`; envelope общий для arch-02.
|
||||
- Keycloak считает заказ успешным только при `200/202` и валидном `sms_message_id`, сохраняет его в challenge/event и не запрашивает provider status.
|
||||
|
||||
### `GET /internal/sms/v1/messages/{sms_message_id}`
|
||||
|
||||
Диагностический read для Keycloak только по собственному `requester_service`. Телефон всегда masked; OTP, substitutions и `body_rendered` не возвращаются.
|
||||
|
||||
### `POST /callbacks/idgtl/sms`
|
||||
|
||||
Единственный публичный SMS endpoint. Только HTTPS и POST через root nginx; source IP `185.203.96.7` повторно сверяется перед production, применяется allowlist. Direct передаёт Basic auth, проверяемый `sms-service` по `IDGTL_SMS_CALLBACK_USERNAME`/`IDGTL_SMS_CALLBACK_PASSWORD`; credentials/Authorization не логируются.
|
||||
|
||||
Callback body — массив; items валидируются и дедуплицируются по `(message_uuid, callback_event, status, status_time)`. Повторы и out-of-order события ожидаемы. Callback обновляет только delivery fields журнала после DB commit, не уведомляет Keycloak и не влияет на OTP verify. Transient DB failure → 5xx для повтора Direct.
|
||||
|
||||
## Frontend ↔ Keycloak
|
||||
|
||||
@@ -368,14 +420,14 @@ Keycloak **обязателен** в production-like контуре с перв
|
||||
| OIDC logout | Keycloak | Expo frontend | Завершение сессии Keycloak, очистка tokens |
|
||||
| OIDC Discovery (`/.well-known/openid-configuration`) | Keycloak | Expo frontend, `api-backend` | issuer, token/jwks endpoints |
|
||||
| JWKS | Keycloak | `api-backend` | Проверка подписи access token (issuer, audience, exp) |
|
||||
| OTP authenticator / SPI | Keycloak | — | Проверка OTP; product limits `otp.phone.*`; mock или SMS |
|
||||
| OTP authenticator / SPI | Keycloak | — | Генерация/локальная проверка OTP, product limits, challenge lifecycle и вызов `sms-service` в real mode |
|
||||
| PostgreSQL schema `keycloak` | Keycloak | Managed PostgreSQL | Учётные записи IdP |
|
||||
|
||||
Frontend не обращается напрямую к Keycloak DB и не хранит парольные credentials. Парольная авторизация в MVP отключена.
|
||||
|
||||
**`api-backend` ↔ Keycloak:** только **валидация JWT** по JWKS/discovery (кэш ключей). Admin REST / User API Keycloak в hot path **не** используются. Телефон и `sub` для `bootstrap` берутся из claims access token.
|
||||
|
||||
**OTP (Keycloak):** единственный канал первичной авторизации — телефон. OTP-flow нужен, когда refresh token отсутствует или истёк. При действующем refresh token frontend использует **Refresh Token Grant** и не показывает OTP. После ввода кода **Keycloak проверяет OTP**: при `KEYCLOAK_OTP_MOCK_ENABLED=true` — сверка с `KEYCLOAK_OTP_MOCK_CODE` (`.env`); при `false` — сверка с OTP от SMS-провайдера (post-MVP, [`!Backlog.md`](../../HAN_chat/!Backlog.md)). Счётчики и product limits OTP — **только** Keycloak/SPI (+ nginx edge); `api-backend` OTP **не** проверяет и **не** ведёт OTP counters в Redis.
|
||||
**OTP (Keycloak):** единственный канал первичной авторизации — телефон. При действующем refresh token OTP не показывается. Keycloak всегда является источником истины verify: mock сравнивает secret-код, real mode — локальный HMAC случайного OTP. `sms-service` только принимает durable order, рендерит шаблон, отправляет через Direct worker и ведёт provider journal. API верификации Direct `/verifier/send` и `/verifier/check` запрещён. Счётчики и product limits — только Keycloak/SPI (+ nginx edge).
|
||||
|
||||
**Clients в realm (MVP):**
|
||||
|
||||
|
||||
Reference in New Issue
Block a user