Plano de Fix — #30 Header api-company-target Sobrescreve Claim JWT schema
Data: 2026-07-08
Severidade: Alto
Vetor: Middleware pipeline — UseAuthentication() registrado DEPOIS de UseEndpoints() (terminal)
1. Resumo
O middleware UseAuthentication() é registrado DEPOIS de UseEndpoints() em 4 serviços. UseEndpoints(MapControllers) é middleware terminal — não chama o próximo. Resultado: HttpContext.User nunca é populado pelo pipeline de autenticação.
Consequência: em endpoints SEM [Authorize], UserProvider.GetSchemaName() não encontra claim schema no JWT (User vazio) → cai no header api-company-target → header do cliente controla o tenant.
Evidência do pentest:
GET /api/payment/profile-fraud
Authorization: Bearer eyJ...schema:"arezzo"...
api-company-target: schutz
→ 503 "Não foi possível efetuar a conexão com o schema schutz.CustomerProfileFraud"
Servidor tentou schema schutz (do header), ignorou arezzo (do JWT).
2. Causa Raiz
2.1 UseEndpoints() é middleware terminal
Quando uma rota casa com um controller, o EndpointMiddleware executa o controller e envia a resposta. Não invoca _next. Middleware registrado depois nunca executa para requisições que batem em controllers.
Documentação Microsoft: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/
"When a middleware short-circuits, it's called a terminal middleware because it prevents further middleware from processing the request."
Ordem obrigatória documentada:
UseRouting → UseCors → UseAuthentication → UseAuthorization → UseEndpoints(MapControllers)
2.2 Ordem real nos 4 serviços
Startup.Configure() (todos os serviços):
// Checkout Startup.cs:70-74
app.UseApiConfiguration(env); // contém UseAuthZ + UseEndpoints
app.UseSwaggerConfiguration(env, Configuration);
app.UseAuthenticationConfiguration(); // contém UseAuthN + UseAuthZ ← NUNCA EXECUTA
ApiConfig.UseApiConfiguration() (checkout L61-91):
app.UseRouting(); // L75
app.UseCors("Total"); // L77
app.UseAuthorization(); // L79 ← ANTES de UseAuthN
app.UseMiddleware<SerilLogMiddleware>(); // L81
app.UseEndpoints(endpoints => // L83 ← TERMINAL
{
endpoints.MapControllers();
});
AuthenticationConfig.UseAuthenticationConfiguration() (nuget L81-88):
app.UseAuthentication(); // L83 ← DEPOIS do terminal → NUNCA executa
app.UseAuthorization(); // L85 ← DEPOIS do terminal → NUNCA executa (duplicado)
2.3 Pipeline real em runtime
UseRouting() ✅
UseCors() ✅
UseAuthorization() ✅ (mas User ainda não populado)
SerilLogMiddleware ✅
UseEndpoints() ✅ → executa controller → TERMINAL
--- NADA abaixo executa para requests em controllers ---
UseAuthentication() ❌ NUNCA
UseAuthorization() ❌ NUNCA (duplicado)
2.4 Por que o sistema "funciona" apesar do bug
UseAuthorization() (L79, que RODA) implementa lazy authentication: quando encontra um endpoint com [Authorize], chama automaticamente JwtBearerHandler.HandleAuthenticateAsync(), que valida o token e popula HttpContext.User.
Isso mascara o bug para endpoints COM [Authorize]. Mas endpoints SEM [Authorize] nunca disparam lazy auth → HttpContext.User permanece vazio → claim schema sempre null → UserProvider.GetSchemaName() cai no header (prioridade 3).
Fluxo com [Authorize]:
Request → UseRouting() seleciona endpoint com [Authorize]
→ UseAuthorization() vê metadata → dispara lazy auth
→ JwtBearerHandler valida token → User populado (schema, profile, sub)
→ Authorization OK
→ UseEndpoints() → controller executa
Fluxo sem [Authorize] (vulnerável):
Request → UseRouting() seleciona endpoint SEM [Authorize]
→ UseAuthorization() não vê metadata → NÃO dispara lazy auth
→ User VAZIO
→ UseEndpoints() → controller executa
→ GetSchemaName(): FindFirst("schema") = null → header vence ❌
3. Decisões de Design
| # | Decisão | Escolha | Motivo |
|---|---|---|---|
| 1 | Abordagem | Mover UseAuthNConf() antes de UseApiConf() (Abordagem A) | 1 linha por serviço, sem alterar lógica interna dos métodos |
| 2 | Escopo | 4 serviços do pentest (checkout, cart, product, store) | Demais 13+ serviços seguem mesmo padrão — card futuro |
| 3 | Duplicata UseAuthZ | Manter (remover depois) | Segunda chamada é no-op com User já autenticado. Remoção é refactor menor, card separado |
| 4 | Ordem #08 vs #30 | #08 primeiro (já funciona via lazy auth), #30 depois | #30 não é bloqueante para #08. Fix de #08 + #30 = defesa completa |
| 5 | Swagger antes de auth | Manter | Swagger serve assets estáticos, não precisa de auth |
4. Fix: Reordenar Startup.Configure()
4.1 Checkout
Arquivo: coezzion-service-checkout/src/Checkout.API/Startup.cs:68-76
// ANTES:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IInitializeService initializeService)
{
app.UseApiConfiguration(env);
app.UseSwaggerConfiguration(env, Configuration);
app.UseAuthenticationConfiguration(); // ← DEPOIS de UseEndpoints
// DEPOIS:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IInitializeService initializeService)
{
app.UseSwaggerConfiguration(env, Configuration);
app.UseAuthenticationConfiguration(); // ← UseAuthN + UseAuthZ PRIMEIRO
app.UseApiConfiguration(env); // ← UseAuthZ + UseEndpoints DEPOIS
4.2 Cart
Arquivo: coezzion-service-cart/src/Cart.API/Startup.cs:57-64
// ANTES:
app.UseApiConfiguration(env, Configuration);
app.UseSwaggerConfiguration(env);
app.UseAuthenticationConfiguration();
// DEPOIS:
app.UseSwaggerConfiguration(env);
app.UseAuthenticationConfiguration();
app.UseApiConfiguration(env, Configuration);
4.3 Product
Arquivo: coezzion-service-product/src/Product.API/Startup.cs:63-72
// ANTES:
app.UseApiConfiguration(env, Configuration);
app.UseSwaggerConfiguration(env, Configuration);
app.UseAuthenticationConfiguration();
// DEPOIS:
app.UseSwaggerConfiguration(env, Configuration);
app.UseAuthenticationConfiguration();
app.UseApiConfiguration(env, Configuration);
4.4 Store
Arquivo: coezzion-service-store/src/Store.API/Startup.cs:57-62
// ANTES:
app.UseApiConfiguration(env, Configuration);
app.UseAuthenticationConfiguration();
// DEPOIS:
app.UseAuthenticationConfiguration();
app.UseApiConfiguration(env, Configuration);
5. Pipeline Resultante
DEPOIS:
UseSwagger() ← static files
UseAuthN() ← ✅ JwtBearerHandler popula HttpContext.User
UseAuthZ() ← ✅ [Authorize] verificado
UseRouting()
UseCors()
UseAuthZ() ← duplicado (no-op, User já autenticado)
SerilLogMiddleware
UseEndpoints() ← TERMINAL
HttpContext.User populado para TODOS os endpoints, com ou sem [Authorize]. Claim schema sempre disponível → GetSchemaName() prioridade 2 vence prioridade 3.
6. Verificação de Segurança
| # | Teste | Esperado |
|---|---|---|
| 1 | GET /api/payment/profile-fraud com JWT schema: arezzo + header api-company-target: schutz | Schema usado: arezzo (JWT claim vence) |
| 2 | GET /api/payment/profile-fraud com JWT schema: arezzo sem header api-company-target | Schema usado: arezzo (JWT claim) |
| 3 | GET /api/payment/profile-fraud sem JWT + header api-company-target: arezzo | 401 (endpoint exige [Authorize] via #08) |
| 4 | Qualquer endpoint com [Authorize] + JWT válido | GetUserId() funciona, GetSchemaName() retorna claim |
| 5 | Qualquer endpoint sem [Authorize] + JWT válido | HttpContext.User populado — claim schema disponível |
| 6 | [Authorize] + JWT inválido/expirado | 401 Unauthorized |
7. Fora do Escopo
| Item | Motivo | Quando |
|---|---|---|
Remover UseAuthZ duplicado do ApiConfig.cs | No-op inofensivo, refactor cosmético | Card separado |
| Corrigir ordering nos 13+ serviços restantes | Escopo do pentest cobre apenas 4 | Card futuro batch |
SignalR hub auth (/hubs/*) | UseAuthN deve rodar no connection negotiate — afetado pelo mesmo bug | Card #29 |
| Health check auth | MapHealthChecks("/check") sem auth — não afetado | — |
| Background jobs / event handlers | Não passam pelo pipeline HTTP — schema via SchemaNameScopedWrapper | OK |
8. Relação com Outros Cards
| Card | Relação |
|---|---|
| #08 | Adiciona [Authorize] nos endpoints de payment. Lazy auth já funciona. Fix de #30 garante auth para TODOS os endpoints, não só os com [Authorize]. |
| #07 | Schema whitelist ("arezzo") no UserProvider. Com #30, claim JWT sempre tem precedência. Whitelist é defense-in-depth. |
| #29 | SignalR de PROD aceita JWT de QA. Mesma causa raiz: UseAuthN() não executa no negotiate. |