Опирается на правила: 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), RolesGuardForbiddenException (→ 403). Путать запрещено (AUTH-6).
  • Каждый endpoint обязан иметь @Roles(...) или явный @Public() — endpoint без обоих является критическим нарушением (AUTH-9).
  • Глобальные APP_GUARD гарантируют, что ни один новый endpoint не окажется открытым по умолчанию.

Auth — это не один шаг, а три разных проверки на трёх разных уровнях. Каждый уровень имеет своё знание и свою задачу. Смешение приводит либо к дублированию с шансом расхождения, либо к пропускам — endpoint без правильного слоя становится точкой входа без контроля.

Три уровня и три ответственности

УровеньЧто проверяетNestJS-механизм
Gateway / API edgeподпись JWT, exp, iss, aud, rate limitJwtAuthGuard (passport-jwt + jwks-rsa)
BFF / Application LayerRBAC: есть ли роль для этого endpoint@Roles(...) + RolesGuard
Domain HandlerABAC: владеет ли этот 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 кидает UnauthorizedException401; 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/:idcustomer или admin.
  • POST /admin/orders/:id/refund — только admin.

Если роль не подходит — RolesGuard кидает ForbiddenException403 до входа в 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 не знает доменную модель.

Сценарий, где это ломается:

  1. Gateway получает GET /orders/order-12345, JWT валиден, principal.sub='cust-99'.
  2. Gateway пытается решить ABAC: «чей order-12345?» — нужно сходить в DB или в order-service.
  3. Чтобы ответить, Gateway фактически становится вторым сервисом с копией доменной модели.
  4. При добавлении co-owners или делегирования — Gateway нужно обновлять параллельно с order-service.

Это двойной источник правды и размытие ответственности. Корректно: Gateway только аутентификация, ABAC — в Handler, где живёт агрегат Order.

Что запрещено

АнтипаттернПравилоЧто взамен
RBAC на GatewayAUTH-2Gateway только аутентификация
ABAC на Gateway или в контроллереAUTH-3, AUTH-11ABAC в Handler или AccessPolicy
jwt.decode без проверки подписиAUTH-4passport-jwt + jwks-rsa
Endpoint без @Roles(...) и без @Public()AUTH-9декоратор обязателен
Только RBAC, без ABAC для own-resource endpointAUTH-3@Roles на endpoint + ABAC в Handler
Дублирование JWT-проверки в каждом GuardAUTH-1один JwtAuthGuard глобально
UnauthorizedException (401) на рольAUTH-6401 — невалидный 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.