Lazy Authentication — Como o Sistema Funciona Apesar do Bug de Ordering
Data: 2026-07-08
1. O Mecanismo de Lazy Auth
UseAuthorization() (chamado em ApiConfig.UseApiConfiguration()) registra o AuthorizationMiddleware do ASP.NET Core. Este middleware, ao encontrar um endpoint com metadata [Authorize], dispara autenticação sob demanda — mesmo que UseAuthentication() nunca tenha executado.
Fluxo detalhado
1. Request chega
2. UseRouting() → seleciona endpoint com metadata [Authorize(JwtBearer)]
3. UseAuthorization() → AuthorizationMiddleware é invocado
├── PolicyEvaluator.AuthenticateAsync()
│ └── AuthenticationService.AuthenticateAsync("Bearer")
│ └── JwtBearerHandler.HandleAuthenticateAsync()
│ ├── Lê header Authorization: Bearer {token}
│ ├── Valida assinatura (HMAC-SHA256)
│ ├── Valida expiração (ValidateLifetime = true)
│ └── Cria ClaimsPrincipal com claims:
│ ├── sub → ClaimTypes.NameIdentifier
│ ├── schema → "schema"
│ ├── profile → "profile"
│ └── unique_name
│
│ → HttpContext.User populado ✅
│
└── PolicyEvaluator.AuthorizeAsync()
└── Verifica policies do [Authorize] contra User
└── OK → continua pipeline
4. UseEndpoints() → controller executa
└── GetUserId() → _context.HttpContext.User.Claims.First(x => x.Type == ClaimTypes.NameIdentifier) ✅
└── GetSchemaName() → http.User?.FindFirst("schema") ✅
Código fonte relevante
AuthenticationConfig.cs — JWT handler registrado:
// coezzion-nuget-authentication/src/Coezzion.Authentication/Configuration/AuthenticationConfig.cs
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
SaveSigninToken = true,
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = null // ← carregado lazy no OnMessageReceived
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
// Carrega secret da config (AWS Secrets Manager)
var secret = context.HttpContext.RequestServices
.GetRequiredService<IConfiguration>()
.GetSection("TokenConfigurations")["SecretJWTKey"];
options.TokenValidationParameters.IssuerSigningKey =
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
return Task.CompletedTask;
}
};
});
2. Por Que Funciona com [Authorize]
┌─────────────────────────────────────────────────────── ───┐
│ Pipeline: com [Authorize] │
├──────────────────────────────────────────────────────────┤
│ UseRouting() → seleciona endpoint + metadata │
│ UseCors() │
│ UseAuthorization() → vê [Authorize] → dispara lazy auth │
│ │ │
│ ├── JwtBearerHandler valida token │
│ ├── HttpContext.User = { schema, profile, sub, ... } │
│ └── Authorization check OK │
│ │
│ SerilLogMiddleware │
│ UseEndpoints() → Controller.GetUserId() ✅ │
│ → Controller.GetSchemaName() ✅ │
│ (FindFirst("schema") → "arezzo") │
└──────────────────────────────────────────────────────────┘
3. Por Que FALHA sem [Authorize]
┌──────────────────────────────────────────────────────────┐
│ Pipeline: sem [Authorize] │
├──────────────────────────────────────────────────────────┤
│ UseRouting() → seleciona endpoint SEM metadata │
│ UseCors() │
│ UseAuthorization() → NÃO vê [Authorize] │
│ │ │
│ └── NÃO dispara lazy auth │
│ HttpContext.User = VAZIO (unauthenticated) │
│ │
│ SerilLogMiddleware │
│ UseEndpoints() → Controller executa │
│ → GetUserId() ❌ (InvalidOperationEx) │
│ → GetSchemaName() │
│ FindFirst("schema") → null │
│ Cai no header api-company-target ✅ │
│ → header DO CLIENTE controla tenant │
└──────────────────────────────────────────────────────────┘
4. GetUserId() — Por Que Nunca Falha
UserProvider.GetUserId():
// coezzion-nuget-common/src/Coezzion.Common/Providers/UserProvider.cs:25-32
public int GetUserId()
{
var userId = _context.HttpContext.User.Claims
.First(x => x.Type == ClaimTypes.NameIdentifier)
.Value;
return int.Parse(userId);
}
Este método lança exceção se HttpContext.User não tiver claims. Mas ele NUNCA é chamado em endpoints sem [Authorize]. Todos os call sites estão em endpoints protegidos por [Authorize(JwtBearer)]:
Cart service (exemplo):
AttendanceController.GetCartByStoreId—[Authorize(JwtBearer)]no controllerAttendanceController.FindDetailWithStockById— mesmo controllerAttendanceController.DeleteById— mesmo controller
Convenção, não enforcement: o código depende da disciplina do desenvolvedor de só chamar GetUserId() em endpoints autenticados. Não há validação em runtime.
5. GetSchemaName() — Por Que o Header Vence
UserProvider.GetSchemaName():
public string GetSchemaName()
{
// 1) x-api-key venda_ai → "arezzo" (hardcoded)
if (apiKeyFromHeader == vendaAiKeyFromConfig) return "arezzo";
// 2) JWT claim "schema" → NUNCA ENCONTRADO (User vazio sem [Authorize])
var schemaClaim = http.User?.FindFirst("schema");
if (schemaClaim != null && !string.IsNullOrEmpty(schemaClaim.Value))
return schemaClaim.Value; // ← NÃO EXECUTA
// 3) Header api-company-target → SEMPRE VENCE
if (http.Request.Headers.TryGetValue("api-company-target", out StringValues headerSchema)
&& headerSchema.Any())
return headerSchema.ToString(); // ← EXECUTA
// 4) Default
return _schemaNameScopedWrapper.SchemaName;
}
Cenários:
Endpoint tem [Authorize]? | JWT enviado? | FindFirst("schema") | Schema usado |
|---|---|---|---|
| Sim | Sim | "arezzo" (do JWT) | JWT claim ✅ |
| Não | Sim | null (User vazio) | Header ❌ |
| Não | Não | null (User vazio) | Header ❌ |
6. O Que o Fix de #30 Corrige
Após mover UseAuthentication() para ANTES de UseEndpoints():
┌──────────────────────────────────────────────────────────┐
│ Pipeline: DEPOIS do fix (com ou sem [Authorize]) │
├──────────────────────────────────────────────────────────┤
│ UseSwagger() │
│ UseAuthentication() → JwtBearerHandler SEMPRE executa │
│ │ │
│ └── HttpContext.User SEMPRE populado ✅ │
│ │
│ UseAuthorization() (1ª) → [Authorize] OK │
│ UseRouting() │
│ UseCors() │
│ UseAuthorization() (2ª, duplicado → no-op) │
│ SerilLogMiddleware │
│ UseEndpoints() → Controller executa │
│ → GetUserId() ✅ │
│ → GetSchemaName() │
│ FindFirst("schema") → "arezzo" ✅ │
│ (header IGNORADO) │
└──────────────────────────────────────────────────────────┘
HttpContext.User populado para todos os endpoints, com ou sem [Authorize]. Claim schema sempre disponível → prioridade 2 vence prioridade 3 → header do cliente nunca controla o tenant.