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 (AddDevPackDualScheme → ForwardDefaultSelector). 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
ForwardDefaultSelectorexistente; o DevPack só registra o scheme + o evento. Mantém ADR-001/ADR-005. PerTenantSettingsEntraTenantValidatorno 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 +
AddDevPackEntraIdopt-in; o dual scheme existente não muda. - Pacote novo
Microsoft.Identity.Web3.8.3;Microsoft.IdentityModel.JsonWebTokensbumpado 8.0.1 → 8.8.0 (Identity.Web exige >= 8.8.0; o grafo de tokens unifica em 8.8.0, sem NU1605).Microsoft.Graphnão entra. - Primeiro consumidor: template
ITLab.Template.Next(Fase 2) — wiring do scheme no seletor, sliceSigninWithEntra,EntraGraphClientFactoryconcreto, persistência dasEntraSettingKeyseGetPublicAuthConfigexpondo 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 flagAuthProvider.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.