Auth Patterns Style Guide

Правила авторизации для UCP-сервисов с кодами AUTH-1..AUTH-21: где какая проверка (Gateway/BFF/Domain), JWT validation, RBAC через @PreAuthorize, ABAC по владению, S2S через mTLS, audit admin, PII в логах и detail, Idempotency-Key для money.

Статья внедрена в скилл AI-агента ucp-auth-review / ucp-auth-design

Правила авторизации и аутентификации для сервисов на Use Case Pattern. Каждое правило идентифицируется кодом AUTH-N — скиллы ucp-auth-review и ucp-auth-design цитируют его в findings.

Скилл намеренно узкий: покрывает то, что встречается в типовых UCP-сервисах (REST за JWT, BFF + Domain Service, маркетплейс из кейса). OWASP Top 10, криптография ключей, фроды — вне его.

Если ищешь обучающее введение в OAuth 2.0, OIDC, JWT, RBAC/ABAC с диаграммами и примерами под SPA / мобильные / микросервисы — открой Паттерны авторизации.


1. Где какая проверка делается

AUTH-1 Gateway / API edge делает аутентификацию: валидация JWT (подпись, exp, iss, aud) и rate limiting. Прокидывает identity в downstream-сервисы.

AUTH-2 BFF / Application Layer делает грубую авторизацию по роли (RBAC): @PreAuthorize("hasRole('ADMIN')"), фильтрация endpoint-ов.

AUTH-3 Domain Service делает авторизацию по ресурсу (ABAC): order.customerId == jwt.sub, бизнес-правила. Никогда не выносится на Gateway — Gateway не знает доменную модель.


2. JWT validation

AUTH-4 JWT проверяется через oauth2ResourceServer().jwt() Spring Security. Кастомный фильтр запрещён — это анти-паттерн, маскирующий ошибки.

http.oauth2ResourceServer(oauth -> oauth
    .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter())));

AUTH-5 JWK Set тянется из IdP по URL spring.security.oauth2.resourceserver.jwt.jwk-set-uri. Кеш по умолчанию 5 минут. Вручную распаковывать ключи запрещено.

AUTH-6 При невалидной подписи / просроченном exp сервис возвращает 401, не 403. 401 = «не аутентифицирован»; 403 = «прав не хватает». Путать запрещено.


3. RBAC: маппинг ролей

AUTH-7 Роли в JWT кладутся IdP в realm_access.roles (Keycloak) или scope (стандартный OAuth2). На вход handler-а они приходят как SimpleGrantedAuthority с префиксом ROLE_ — через JwtAuthenticationConverter:

@Bean
JwtAuthenticationConverter jwtAuthConverter() {
    var authorities = new JwtGrantedAuthoritiesConverter();
    authorities.setAuthorityPrefix("ROLE_");
    authorities.setAuthoritiesClaimName("realm_access.roles");
    var converter = new JwtAuthenticationConverter();
    converter.setJwtGrantedAuthoritiesConverter(authorities);
    return converter;
}

AUTH-8 Разрешённые роли в нашей методологии: customer, seller, admin, system. Любая другая роль появляется только через пересмотр Bounded Context.

AUTH-9 На каждом REST-endpoint обязательна аннотация @PreAuthorize("hasRole(...)") или @PreAuthorize("hasAnyRole(...)"). Endpoint без проверки роли — критическое нарушение.


4. ABAC: владение ресурсом

AUTH-10 Когда команда / запрос работают с агрегатом по id — обязателен ABAC по владению. Реализуется одним из двух способов:

  • @PreAuthorize("@access.canEditOrder(#id, authentication.principal)") — для простых случаев.
  • Внутри Handler-а: загрузить агрегат, сравнить aggregate.<ownerId> с jwt.sub, бросить FORBIDDEN если не совпало.

AUTH-11 ABAC-логика выносится в @Component("access") бин или в Handler — но не размазывается по контроллерам.

AUTH-12 Для роли admin ABAC по умолчанию обходится (полный доступ), но каждое действие admin обязательно пишется в audit log (AUTH-15).


5. Service-to-service

AUTH-13 Сервис-к-сервису общение использует один из двух способов:

  • mTLS (рекомендуется): двусторонний TLS на Kubernetes Service Mesh / Istio.
  • Client Credentials Flow (grant_type=client_credentials): сервис получает свой access_token от IdP с scope=service:operation.

AUTH-14 Внутренние клиенты в adapter-out-* никогда не делают RestTemplate.exchange(...) без mTLS / Bearer-заголовка. Анонимный inter-service трафик — критическое нарушение.


6. Аудит admin-команд

AUTH-15 Каждая команда от роли admin, изменяющая состояние агрегата, обязана писать строку в *_audit_log таблицу с полями: кто (actor_id), когда (occurred_at), что (action), к чему (order_id), детали (metadata JSONB). Реализация — @Around-аспект или явный вызов в Handler.


7. PII и секреты

AUTH-16 PII-поля (email, phone, ФИО, адрес) не попадают:

  • в логи (даже на уровне DEBUG);
  • в Exception.getMessage() и далее в ProblemDetails.detail;
  • в Kafka-события (передавать только id, payload подгружается потребителем по запросу).

AUTH-17 Секреты (spring.security.oauth2.client.registration.*.client-secret, JDBC-пароли, ключи шлюзов) никогда не коммитятся в git. Только через application-${profile}.yml в Vault / SealedSecrets.

AUTH-18 RestControllerAdvice для OrderDomainException (и аналогов) не выводит cause.getMessage() в detail — только заранее заданное сообщение по коду.


8. Идемпотентность как часть auth-контракта

AUTH-19 Любая команда, меняющая деньги или резерв (CreateOrder, ConfirmPayment, Refund...), обязана требовать заголовок Idempotency-Key (см. R-HDR-3). Повторный вызов с тем же ключом возвращает прежний результат, а не дубль.


9. Хранение токенов на клиенте (информативно для BFF/SPA)

AUTH-20 Для SPA — HttpOnly + Secure + SameSite=Lax cookie (либо session-cookie у BFF, либо JWT-в-cookie). localStorage запрещён.

AUTH-21 Refresh-токены — с rotation: при каждом обновлении старый инвалидируется. При повторном использовании старого RT — компрометация, инвалидируется вся цепочка.


10. Чек-лист обзора

ГруппаПравила
Где какая проверкаAUTH-1AUTH-3
JWT validationAUTH-4AUTH-6
RBACAUTH-7AUTH-9
ABACAUTH-10AUTH-12
Service-to-serviceAUTH-13AUTH-14
Аудит adminAUTH-15
PII / секреты / логиAUTH-16AUTH-18
ИдемпотентностьAUTH-19
Клиентская сторона (BFF/SPA)AUTH-20AUTH-21