Опирается на правила:
AUTH-1,AUTH-2,AUTH-3из Auth Patterns → раздел 1. Где какая проверка делается.
Важно знать
- Gateway / API edge — аутентификация:
JwtAuthGuard(passport-jwt + jwks-rsa), rate limiting. Identity кладётся вrequest.userи прокидывается в downstream.- BFF / Application Layer — грубая авторизация по роли (RBAC):
@Roles('admin')+RolesGuardна каждом endpoint.- Domain Service / Handler — авторизация по ресурсу (ABAC):
order.customerId === principal.sub, бизнес-правила.- ABAC никогда на Gateway — Gateway не знает доменную модель; чтобы решить «чей order», ему пришлось бы идти в DB.
JwtAuthGuardкидаетUnauthorizedException(→ 401),RolesGuard—ForbiddenException(→ 403). Путать запрещено (AUTH-6).- Каждый endpoint обязан иметь
@Roles(...)или явный@Public()— endpoint без обоих является критическим нарушением (AUTH-9).- Глобальные
APP_GUARDгарантируют, что ни один новый endpoint не окажется открытым по умолчанию.
Auth — это не один шаг, а три разных проверки на трёх разных уровнях. Каждый уровень имеет своё знание и свою задачу. Смешение приводит либо к дублированию с шансом расхождения, либо к пропускам — endpoint без правильного слоя становится точкой входа без контроля.
Три уровня и три ответственности
| Уровень | Что проверяет | NestJS-механизм |
|---|---|---|
| Gateway / API edge | подпись JWT, exp, iss, aud, rate limit | JwtAuthGuard (passport-jwt + jwks-rsa) |
| BFF / Application Layer | RBAC: есть ли роль для этого endpoint | @Roles(...) + RolesGuard |
| Domain Handler | ABAC: владеет ли этот user этим ресурсом | сравнение aggregate.ownerId === principal.sub |
Gateway — аутентификация
AUTH-1: Gateway отвечает на вопрос «кто этот клиент».
JwtStrategy настраивается один раз на всё приложение через passport-jwt и jwks-rsa. Она получает публичный ключ из JWK Set IdP, проверяет подпись, exp, iss, aud, и возвращает Principal — типизированный объект, который NestJS кладёт в request.user.
// adapters/in/http/security/jwt.strategy.ts
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(config: AppConfig) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
algorithms: ['RS256'],
audience: config.auth.audience,
issuer: config.auth.issuer,
secretOrKeyProvider: passportJwtSecret({
jwksUri: config.auth.jwksUri,
cache: true,
cacheMaxAge: 300_000, // AUTH-5: кеш ~5 мин
rateLimit: true,
}),
});
}
validate(claims: JwtClaims): Principal {
return { sub: claims.sub, roles: extractRoles(claims) };
}
}
validate вызывается только после того, как библиотека убедилась в подписи и exp. Самописный jwt.decode без проверки подписи — AUTH-4-нарушение.
Что Gateway не делает: не знает endpoint paths и роли, не знает бизнес-модель, не проверяет владение ресурсом.
При невалидном JWT — JwtAuthGuard кидает UnauthorizedException → 401; downstream-обработчики не вызываются.
client → POST /orders + Bearer <jwt>
↓
JwtAuthGuard (validates JWT, populates request.user)
↓ (JWT valid)
RolesGuard → Handler: знает, что пришёл customer с sub=42
BFF — грубая авторизация по роли
AUTH-2: BFF/Application отвечает на вопрос «может ли этот клиент вообще обратиться к этому endpoint».
Глобальные APP_GUARD регистрируются в AppModule в порядке: сначала JwtAuthGuard (401), затем RolesGuard (403). Публичные endpoint-ы получают явный @Public() декоратор — отсутствие guard-а не эквивалентно публичности.
// app.module.ts
providers: [
{ provide: APP_GUARD, useClass: JwtAuthGuard }, // AUTH-1
{ provide: APP_GUARD, useClass: RolesGuard }, // AUTH-2
],
RolesGuard читает метаданные через Reflector и сверяет principal.roles с требуемыми:
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(ctx: ExecutionContext): boolean {
const required = this.reflector.get(Roles, ctx.getHandler());
if (!required) throw new ForbiddenException(); // AUTH-9: нет @Roles — запрещено
const { user } = ctx.switchToHttp().getRequest<{ user: Principal }>();
if (!required.some((r) => user.roles.includes(r))) throw new ForbiddenException();
return true;
}
}
Разметка endpoint-ов по ролям:
// adapters/in/http/order.controller.ts
@Controller('orders')
export class OrderController {
@Post()
@Roles('customer')
async create(@Body() dto: CreateOrderDto, @Req() req: Request): Promise<OrderResponse> {
return this.handler.execute(dto, req.user as Principal);
}
@Get(':id')
@Roles('customer', 'admin')
async getById(@Param('id') id: string, @Req() req: Request): Promise<OrderResponse> {
return this.handler.execute({ orderId: id }, req.user as Principal);
}
}
// adapters/in/http/admin/admin-order.controller.ts
@Controller('admin/orders')
export class AdminOrderController {
@Post(':id/refund')
@Roles('admin')
async refund(@Param('id') id: string, @Req() req: Request): Promise<OrderResponse> {
return this.handler.execute({ orderId: id }, req.user as Principal);
}
}
RBAC на endpoint-level:
POST /orders— толькоcustomer.GET /orders/:id—customerилиadmin.POST /admin/orders/:id/refund— толькоadmin.
Если роль не подходит — RolesGuard кидает ForbiddenException → 403 до входа в Handler.
Что BFF не делает: не проверяет, чей конкретно order. Это следующий слой.
Domain Handler — авторизация по ресурсу
AUTH-3: Handler отвечает на вопрос «может ли этот клиент работать с этим конкретным ресурсом».
// core/order/handlers/get-order-by-id.handler.ts
@Injectable()
export class GetOrderByIdHandler {
constructor(private readonly orders: OrderRepository) {}
async execute(query: GetOrderByIdQuery, principal: Principal): Promise<Order> {
const order = await this.orders.byId(query.orderId);
if (!order) throw new OrderNotFoundError(query.orderId);
if (!principal.roles.includes('admin') && order.customerId !== principal.sub) {
throw new ForbiddenError(query.orderId); // AUTH-10
}
return order;
}
}
ABAC отвечает: загрузили order с customerId='cust-42', текущий principal.sub='cust-99' — отказ. Это не RBAC: роль customer валидна, RolesGuard пропустил, но этот customer не имеет права читать чужой заказ.
Для Product и Customer та же модель:
// core/product/handlers/update-product.handler.ts
async execute(cmd: UpdateProductCommand, principal: Principal): Promise<void> {
const product = await this.products.byId(cmd.productId);
if (!product) throw new ProductNotFoundError(cmd.productId);
if (!principal.roles.includes('admin') && product.sellerId !== principal.sub) {
throw new ForbiddenError(cmd.productId); // AUTH-10
}
// ...
}
ABAC-логика размещается в выделенном @Injectable() AccessPolicy или непосредственно в Handler — никогда не в контроллере (AUTH-11). Роль admin обходит ABAC, но каждое такое действие обязательно попадает в audit log (AUTH-12, AUTH-15).
Подробнее — ABAC: владение ресурсом.
Почему ABAC не на Gateway
AUTH-3 (запрет): Gateway не знает доменную модель.
Сценарий, где это ломается:
- Gateway получает
GET /orders/order-12345, JWT валиден,principal.sub='cust-99'. - Gateway пытается решить ABAC: «чей order-12345?» — нужно сходить в DB или в order-service.
- Чтобы ответить, Gateway фактически становится вторым сервисом с копией доменной модели.
- При добавлении co-owners или делегирования — Gateway нужно обновлять параллельно с order-service.
Это двойной источник правды и размытие ответственности. Корректно: Gateway только аутентификация, ABAC — в Handler, где живёт агрегат Order.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
| RBAC на Gateway | AUTH-2 | Gateway только аутентификация |
| ABAC на Gateway или в контроллере | AUTH-3, AUTH-11 | ABAC в Handler или AccessPolicy |
jwt.decode без проверки подписи | AUTH-4 | passport-jwt + jwks-rsa |
Endpoint без @Roles(...) и без @Public() | AUTH-9 | декоратор обязателен |
| Только RBAC, без ABAC для own-resource endpoint | AUTH-3 | @Roles на endpoint + ABAC в Handler |
| Дублирование JWT-проверки в каждом Guard | AUTH-1 | один JwtAuthGuard глобально |
UnauthorizedException (401) на роль | AUTH-6 | 401 — невалидный JWT; 403 — прав нет |
Куда дальше
- JWT validation —
JwtStrategy,passportJwtSecret,jwks-rsa. - RBAC: маппинг ролей —
extractRoles, разрешённые роли,RolesGuard. - ABAC: владение ресурсом —
AccessPolicy, обход дляadmin. - Service-to-service — Client Credentials Flow, mTLS, outbound-клиенты.
- Audit admin-команд —
NestInterceptorдля*_audit_log. - PII и секреты — pino redact, env/Vault, Exception Filter.