Опирается на правила: AUTH-1, AUTH-2, AUTH-3 из Auth Patterns Rules → раздел 1. Где какая проверка делается.

Важно знать

  • Gateway / API edge — аутентификация: AuthN-middleware валидирует JWT и кладёт *Principal в context.Context.
  • BFF / Application Layer — грубая авторизация по роли (RBAC): RequireRoles("admin") на chi-группе роутов.
  • Domain Service / Handler — авторизация по ресурсу (ABAC): order.CustomerID == principal.Sub в AccessPolicy.
  • ABAC никогда на Gateway — Gateway не знает доменную модель.
  • Каждый слой — одна ответственность: AuthN не делает RBAC, Handler не делает JWT-валидацию.
  • RequireRoles без AuthN перед ним не работает: PrincipalFrom(ctx) вернёт nil.
  • Размытие ответственности — главная причина дыр в авторизации.
  • Ошибки — значения: &AuthError{} → 401, &ForbiddenError{} → 403; путать запрещено (AUTH-6).

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

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

УровеньЧто проверяетИнструмент (Go)
Gateway / API edgeподпись JWT, exp, iss, aud, rate limitAuthN-middleware + golang-jwt/jwt/v5 + JWKS-keyfunc
BFF / Application LayerRBAC: есть ли роль для этого endpointRequireRoles("customer") на chi.Group
Domain Service / HandlerABAC: владеет ли этот user этим ресурсомAccessPolicy.CheckOwnership в core/<aggregate>/

Gateway — аутентификация (AUTH-1)

AuthN-middleware стоит первым в цепочке на всём роутере. Он отвечает на вопрос «кто этот клиент»: извлекает Authorization: Bearer <token>, вызывает JWTValidator.Validate, при успехе кладёт *Principal в контекст.

// adapters/in/http/middleware/authn.go

func AuthN(v *security.JWTValidator) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            raw := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
            if raw == "" {
                httperr.Write(w, r, &apperr.AuthError{Reason: "missing token"})
                return
            }
            principal, err := v.Validate(raw)
            if err != nil {
                httperr.Write(w, r, err)   // AUTH-6: невалидный JWT → 401
                return
            }
            ctx := context.WithValue(r.Context(), principalKey, principal)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

JWTValidator использует golang-jwt/jwt/v5 + keyfunc/v3 (JWKS-кеш с IdP). Подробнее — JWT validation.

Что Gateway не делает: не знает endpoint paths, не знает доменную модель, не проверяет владение ресурсом. При невалидном JWT downstream сервисы не запрашиваются.

BFF — грубая авторизация по роли (AUTH-2)

RequireRoles ставится на chi.Group и отвечает на вопрос «может ли этот клиент вообще обратиться к этому endpoint».

// adapters/in/http/router.go

r := chi.NewRouter()
r.Use(middleware.AuthN(jwtValidator))          // AUTH-1: первым, на всём роутере

r.Group(func(r chi.Router) {
    r.Use(middleware.RequireRoles("customer")) // AUTH-2: только customer
    r.Post("/orders", h.CreateOrder)
    r.Get("/orders/{id}", h.GetOrder)
})

r.Group(func(r chi.Router) {
    r.Use(middleware.RequireRoles("admin"))    // AUTH-2: только admin
    r.Post("/orders/{id}/cancel", h.AdminCancelOrder)
    r.Post("/products", h.CreateProduct)
})

r.Group(func(r chi.Router) {
    r.Use(middleware.RequireRoles("customer", "admin"))
    r.Get("/products/{id}", h.GetProduct)
})
// adapters/in/http/middleware/rbac.go

func RequireRoles(roles ...string) func(http.Handler) http.Handler {
    allowed := make(map[string]struct{}, len(roles))
    for _, r := range roles {
        allowed[r] = struct{}{}
    }
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            p := PrincipalFrom(r.Context())
            if p == nil {
                httperr.Write(w, r, &apperr.AuthError{Reason: "unauthenticated"})
                return
            }
            for _, role := range p.Roles {
                if _, ok := allowed[role]; ok {
                    next.ServeHTTP(w, r)
                    return
                }
            }
            httperr.Write(w, r, &apperr.ForbiddenError{Required: roles})
        })
    }
}

RBAC отвечает на endpoint-level вопросы: POST /orders — только customer, POST /admin/* — только admin. Если роль не подходит — 403 Forbidden до входа в Handler. Что BFF не делает: не проверяет, чей конкретно агрегат. Этим занимается следующий слой.

Domain Handler — авторизация по ресурсу (AUTH-3)

AccessPolicy живёт в core/<aggregate>/ и отвечает на вопрос «может ли этот клиент работать с этим конкретным ресурсом».

// core/order/access.go

type OrderAccessPolicy struct{}

func (p *OrderAccessPolicy) CheckOwnership(order *Order, principal *security.Principal) error {
    for _, role := range principal.Roles {
        if role == "admin" {
            return nil   // AUTH-12: admin проходит, audit пишет Handler
        }
    }
    if order.CustomerID != principal.Sub {
        return &apperr.ForbiddenError{
            Resource:   "order",
            ResourceID: order.ID,
        }
    }
    return nil
}
// core/order/handler/get_order.go

func (h *GetOrderHandler) Handle(ctx context.Context, cmd GetOrderCommand) (OrderView, error) {
    order, err := h.repo.ByID(ctx, cmd.OrderID)
    if err != nil {
        return OrderView{}, fmt.Errorf("load order %s: %w", cmd.OrderID, err)
    }
    principal := middleware.PrincipalFrom(ctx)
    if err := h.policy.CheckOwnership(order, principal); err != nil {   // AUTH-10
        return OrderView{}, err
    }
    return toView(order), nil
}

ABAC отвечает: загрузили order.ID=SB-12345, у него CustomerID="user-42", текущий principal.Sub="user-99" — отказ с ForbiddenError. Это не RBAC: роль customer валидна, endpoint доступен, но этот customer не имеет права читать чужой заказ.

Для GetProduct на Sber-площадке логика та же — ABAC проверяет принадлежность только когда агрегат владелец-специфичен (например, draft-продукт принадлежит seller):

// core/product/handler/get_draft_product.go

func (h *GetDraftProductHandler) Handle(ctx context.Context, cmd GetDraftProductCommand) (ProductView, error) {
    product, err := h.repo.ByID(ctx, cmd.ProductID)
    if err != nil {
        return ProductView{}, fmt.Errorf("load product %s: %w", cmd.ProductID, err)
    }
    principal := middleware.PrincipalFrom(ctx)
    if err := h.policy.CheckSellerOwnership(product, principal); err != nil {
        return ProductView{}, err
    }
    return toView(product), nil
}

Почему ABAC не на Gateway

AUTH-3 (запрет): Gateway не знает доменную модель.

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

  1. Gateway получает GET /orders/SB-12345, JWT валиден, sub="user-99".
  2. Gateway пытается решить ABAC: «чей order SB-12345?» — нужно идти в БД или в order-service.
  3. Чтобы ответить, Gateway фактически дублирует домен и становится его клиентом.
  4. При изменении модели (добавили co-owners у Order) — Gateway надо обновлять параллельно с order-service.

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

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

АнтипаттернПравилоЧто взамен
RBAC на GatewayAUTH-2RequireRoles на chi-группе в сервисе
ABAC на Gateway или в контроллереAUTH-3AccessPolicy.CheckOwnership в Handler
jwt.Parse внутри Handler или UseCaseAUTH-1AuthN-middleware на edge роутера
Endpoint без RequireRoles на его chi-группеAUTH-9RequireRoles обязателен; нет — критическое нарушение
Только RBAC без ABAC для own-resource endpointsAUTH-3RBAC на группе + ABAC в Handler
if p.Roles[0] == "admin" в контроллереAUTH-11AccessPolicy как отдельный тип в core/
Дублирование JWT-валидации на каждом слоеAUTH-1один AuthN-middleware на весь роутер

Куда дальше

  • JWT validation — JWTValidator, JWKS-keyfunc, golang-jwt/jwt/v5, коды ошибок AUTH-4..6.
  • RBAC: маппинг ролей — extractRoles, RequireRoles, разрешённые роли AUTH-7..9.
  • ABAC: владение ресурсом — AccessPolicy, CheckOwnership, admin-bypass AUTH-10..12.
  • Service-to-service — mTLS, Client Credentials Flow, oauth2.TokenSource AUTH-13..14.
  • Audit admin-команд — audit.Logger, *_audit_log, обязателен для каждой admin-команды AUTH-15.
  • PII и секреты — PII не в error.Error() и slog, секреты через envconfig AUTH-16..18.
  • Идемпотентность — RequireIdempotencyKey на money-командах AUTH-19.
  • Хранение токенов на клиенте — BFF HttpOnly cookie, RT rotation AUTH-20..21.