Pular para o conteúdo principal
Versão: main (dev) 🚧

ADR-010 — Microsoft Entra ID como terceiro scheme, validação per-tenant via settings store

Data: 2026-06-29 Status: Aceito Contexto: DevPack 10.9.0 Pareado com: Fase 2 da iniciativa Email/Cache/Entra/Bus — wiring + slice SigninWithEntra + Graph sync + endpoints de config no template ITLab.Template.Next. Cita: ADR-005 (Identity local + dual scheme), ADR-008 (per-tenant settings + secret protection), ADR-007 (resolução de tenant por domínio). Base de referência: multi-scheme Entra + validação per-tenant no OnTokenValidated portado de ITLab.WorkGuardian (CrossCutting/Identity/*, TenantSettingKey.EntraID.*).

Contexto

O DevPack já tinha o substrato de Entra ID — os DTOs (Identity/AuthSettings/EntraId*SettingsDto), MicrosoftEntraIdOptions, EIdentityProvider, IdentitySchemas.MicrosoftEntraID, IdentityProviderName — e o substrato de auth por-tenant (IPerTenantSettingsStore + ISecretProtector, ADR-008). Faltava o conector: registrar o scheme Microsoft.Identity.Web, validar o token Entra por-tenant (cada tenant decide se aceita Entra e contra qual diretório/audience), mapear claims e abstrair o Graph client para o JIT sync.

O canon do template já nasce com auth funcional (ADR-005): dual scheme cookie BFF + Bearer próprio, selecionados por request por um policy scheme (AddDevPackDualSchemeForwardDefaultSelector). A pergunta de design era como encaixar Entra sem quebrar isso e onde mora cada peça (G-01: universal → DevPack; canon → template).

A restrição central do ASP.NET Core (ADR-008 §4) permanece: schemes são estáticos, registrados na startup. Não se registra scheme por request; a config per-tenant vai nos eventos.

Decisão

A abstração de validação per-tenant + o mapeamento de claims + a extensão de registro do scheme vivem no DevPack. Entra é um terceiro scheme, opt-in por tenant; o DefaultProvider continua o default. O wiring, o slice de sign-in, o Graph sync concreto e a persistência das chaves ficam no consumidor.

1. AddDevPackEntraId — registro do terceiro scheme (compõe, não substitui)

DevPackEntraIdServiceCollectionExtensions.AddDevPackEntraId(this AuthenticationBuilder, string scheme = IdentitySchemas.MicrosoftEntraID.Name, Action<MicrosoftIdentityOptions>? configure = null) chama AddMicrosoftIdentityWebApi para o scheme nomeado. O consumidor encaixa esse scheme na cadeia existente (AddDevPackDualScheme + AddCookie + AddJwtBearer) e estende seu seletor smart para encaminhar tokens Microsoft (por iss) ao scheme Entra; tokens próprios continuam no DefaultProvider. Não reescrevemos o seletor nem embrulhamos o AddMicrosoftIdentityWebApi (ADR-001) — só pluga o evento de validação.

2. Issuer aceito: v1 e v2

O issuer validator aceita https://login.microsoftonline.com/{tid}/v2.0 (tokens v2) e https://sts.windows.net/{tid}/ (tokens v1). Hosts não-Microsoft são rejeitados. O helper público ExtractTenantIdFromIssuer(string? iss) extrai o tid de ambos os formatos.

3. Validação per-tenant nos eventos (recipe da ADR-008, agora com abstração)

OnTokenValidated (encadeado após o handler do Microsoft.Identity.Web) resolve IEntraTenantValidator do RequestServices, extrai o tenant Entra do iss e chama ValidateAsync; context.Fail(reason) se inválido. A ADR-008 deliberadamente não promoveu uma abstração de resolver per-tenant ("recipe primeiro, abstração quando o reuso aparecer"). O reuso apareceu: IEntraTenantValidator + EntraTenantValidationResult é essa abstração, agora justificada pelo conector.

4. PerTenantSettingsEntraTenantValidator no DevPack (chaves padronizáveis)

A impl default lê IPerTenantSettingsStore por chaves padronizadas (EntraSettingKeys) e exige: provider habilitado (AuthProvider.EntraID == "true") + o tenant Entra do iss casando o EntraID.TenantId configurado + (opcional) o aud casando o EntraID.WebApi.ClientId. Decisão de localização: a impl mora no DevPack, não no template, porque o esquema de chaves é padronizável — a flag AuthProvider.EntraID é idêntica à chave canon que o template já usa (TenantSettingKey.AuthProvider.EntraID) e a família EntraID.* já estava documentada como recipe na Identity/Settings/README. Nenhuma premissa do canon vaza para a impl.

O tenant da aplicação é resolvido antes da auth (resolução por domínio → ICurrentUser.TenantId, ADR-007), pois o store é indexado por tenant da aplicação e não há lookup reverso a partir do tenant Entra. Sem tenant em escopo → token rejeitado.

5. Audience validation tolerante (mais seguro que o WG)

O WG desligava ValidateAudience. Aqui validamos o aud contra o WebApi.ClientId do tenant — mas de forma tolerante: só exige quando o tenant tem o client id configurado e o token carrega um aud. Aceita {id} e api://{id}. Mais seguro sem travar tenants ainda sem client id configurado.

6. IEntraGraphClientFactory interface-only (Graph fica no consumidor)

O JIT sync (displayName/UPN, desativar conta sumida do Entra) precisa de um GraphServiceClient por tenant. Microsoft.Graph é uma dependência pesada e o credential (ClientSecretCredential) usa segredos per-tenant. Interface no DevPack, impl no consumidor — o DevPack não referencia Microsoft.Graph. O retorno é object? para a abstração não acoplar ao SDK do Graph; o consumidor faz o cast.

7. DefaultEntraClaimsMapper

Mapeia oid/objectidentifier → ObjectId, name/preferred_username/email/upn → UserName/Email, roles + scopes scp (split por espaço) → Roles. Registrado por AddDevPackEntraId via TryAdd (substituível). O lookup do usuário local + emissão do token próprio do app ("Entra autentica, app emite token próprio") ficam no slice do consumidor.

Alternativas consideradas

  • Reescrever o seletor smart para conhecer Entra: descartado — o consumidor estende o ForwardDefaultSelector existente; o DevPack só registra o scheme + o evento. Mantém ADR-001/ADR-005.
  • PerTenantSettingsEntraTenantValidator no template: descartado — as chaves são padronizáveis (a flag enabled já é canon compartilhada); manter no DevPack evita duplicação garantida (G-01). Marcado como ponto a ratificar no plano; ratificado aqui.
  • Impl do Graph client no DevPack: descartado — Microsoft.Graph é peso morto para consumidores sem JIT sync; interface-only mantém o DevPack leve (ADR-001).
  • Desligar ValidateAudience (mirror do WG): descartado — validação per-tenant tolerante é mais segura sem custo de fricção.

Consequências

  • Adição não-quebrante: tipos novos + AddDevPackEntraId opt-in; o dual scheme existente não muda.
  • Pacote novo Microsoft.Identity.Web 3.8.3; Microsoft.IdentityModel.JsonWebTokens bumpado 8.0.1 → 8.8.0 (Identity.Web exige >= 8.8.0; o grafo de tokens unifica em 8.8.0, sem NU1605). Microsoft.Graph não entra.
  • Primeiro consumidor: template ITLab.Template.Next (Fase 2) — wiring do scheme no seletor, slice SigninWithEntra, EntraGraphClientFactory concreto, persistência das EntraSettingKeys e GetPublicAuthConfig expondo os dados públicos do SPA (nunca o secret).
  • O DefaultProvider (cookie + Bearer próprio) continua o default; Entra é opt-in por tenant via a flag AuthProvider.EntraID.
  • Testes: ExtractTenantIdFromIssuerTests, PerTenantSettingsEntraTenantValidatorTests, DefaultEntraClaimsMapperTests.

Referências

  • ADR-001 — No framework wrappers. ADR-005 — Identity local + dual scheme. ADR-007 — Resolução de tenant por domínio. ADR-008 — Per-tenant settings + secret protection (recipe de validação nos eventos).
  • Identity/Authentication/EntraId/ + README.md.