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>
66 lines
4.4 KiB
Markdown
66 lines
4.4 KiB
Markdown
# Ключи 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 совпадает), токен, выданный до рестарта, остаётся валиден.
|