food-market/docs/openiddict-keys.md
nns 422b7ad5ea feat(auth): prod X509-ключи OpenIddict с persistent self-signed (P0-1)
OpenIddictKeyConfigurator: dev — прежний RSA-ключ в App_Data (поведение не
менялось, шифрование access-token выключено); prod/stage — отдельные X509
сертификаты подписи и шифрования из конфига (OpenIddict:SigningCertPath /
EncryptionCertPath / CertPassword, можно env). Нет файла → генерируется
persistent self-signed (RSA 2048, 5 лет) и сохраняется в App_Data (volume),
а не dev-ephemeral — токены переживают рестарт.

Проверено: prod выдаёт 5-сегментный JWE, /api/me 200; рестарт → те же
сертификаты (fingerprint совпал), pre-restart токен валиден. dev — 3-сегментный
JWT, /api/me 200. docs/openiddict-keys.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 02:47:00 +05:00

66 lines
4.4 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.

# Ключи OpenIddict (подпись и шифрование токенов)
Токены доступа/обновления подписываются (и в проде шифруются) ключами OpenIddict.
Конфигурация ключей — в `OpenIddictKeyConfigurator` (`src/food-market.api/Infrastructure/Security/`),
вызывается из `Program.cs` внутри `AddServer(...)`.
## Development
- Persistent RSA-ключ в `src/food-market.api/App_Data/openiddict-dev-key.xml`
(один и тот же для подписи и шифрования).
- Переживает рестарты — выданные токены остаются валидными между перезапусками.
- **Шифрование access-token выключено** (`DisableAccessTokenEncryption`) — токен это
обычный 3-сегментный JWT, удобно дебажить (можно прочитать на jwt.io).
- Файл `App_Data/` в `.gitignore` — ключ не коммитится.
## Production / Stage
- Отдельные **X509-сертификаты** для подписи и шифрования. Access-token шифруется
(5-сегментный JWE).
- Путь к сертификатам — из конфигурации:
| Ключ конфига | Env-переменная | Назначение | Дефолт |
|---|---|---|---|
| `OpenIddict:SigningCertPath` | `OpenIddict__SigningCertPath` | сертификат подписи | `App_Data/openiddict-signing.pfx` |
| `OpenIddict:EncryptionCertPath` | `OpenIddict__EncryptionCertPath` | сертификат шифрования | `App_Data/openiddict-encryption.pfx` |
| `OpenIddict:CertPassword` | `OpenIddict__CertPassword` | пароль PFX (опц.) | — |
- **Если файла нет** — генерируется persistent self-signed сертификат (RSA 2048, срок 5 лет)
и сохраняется по пути. При следующем старте берётся тот же файл, поэтому ранее
выданные токены остаются валидными (нет dev-ephemeral поведения, при котором каждый
рестарт инвалидировал бы все токены).
- `App_Data` смонтирован как volume (`api-data:/app/App_Data` в `docker-compose.yml`),
поэтому сертификаты переживают пересоздание контейнера.
### Принести собственные сертификаты
Положить готовые `.pfx` (с приватным ключом) по путям из конфига и, при наличии пароля,
задать `OpenIddict__CertPassword`. Приложение их подхватит вместо генерации self-signed.
```bash
# пример: смонтировать каталог с сертификатами и указать пути
OpenIddict__SigningCertPath=/run/secrets/oidc-signing.pfx
OpenIddict__EncryptionCertPath=/run/secrets/oidc-encryption.pfx
OpenIddict__CertPassword=<пароль или пусто>
```
### Ротация
1. Заменить/удалить `.pfx` файлы (или указать новые пути).
2. Рестарт API: при отсутствии файла сгенерируется новый сертификат.
3. **Важно:** ротация ключа подписи/шифрования инвалидирует все ранее выданные
токены — пользователям потребуется перелогиниться. Планировать на окно обслуживания.
### Проверка (smoke)
```bash
# 5 сегментов = JWE (шифрование включено) — норма для прода
curl -s -X POST $API/connect/token -H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password&username=...&password=...&client_id=food-market-web&scope=api" \
| python3 -c "import sys,json;print(json.load(sys.stdin)['access_token'].count('.')+1,'сегментов')"
```
Проверено локально (2026-05-27): prod-режим генерирует оба сертификата в `App_Data`,
выдаёт 5-сегментный JWE, `/api/me` → 200; после рестарта сертификаты те же
(fingerprint совпадает), токен, выданный до рестарта, остаётся валиден.