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

4.4 KiB
Raw Blame History

Ключи 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.

# пример: смонтировать каталог с сертификатами и указать пути
OpenIddict__SigningCertPath=/run/secrets/oidc-signing.pfx
OpenIddict__EncryptionCertPath=/run/secrets/oidc-encryption.pfx
OpenIddict__CertPassword=<пароль или пусто>

Ротация

  1. Заменить/удалить .pfx файлы (или указать новые пути).
  2. Рестарт API: при отсутствии файла сгенерируется новый сертификат.
  3. Важно: ротация ключа подписи/шифрования инвалидирует все ранее выданные токены — пользователям потребуется перелогиниться. Планировать на окно обслуживания.

Проверка (smoke)

# 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 совпадает), токен, выданный до рестарта, остаётся валиден.