ADR-005 — Identity store base types, self-issued token service e publicação JWKS
Data: 2026-06-11 Status: Aceito Contexto: DevPack 10.4.0 (planejada) Pareado com: Template ADR-006
Contexto
O DevPack já expõe, desde bundles anteriores, os blocos de um Identity store local: BaseIdentityUser : IdentityUser<int> e BaseIdentityRole : IdentityRole<int> (ambos DDD-ficados com AuditTimestamp, soft-delete e domain events), além das referências a Microsoft.AspNetCore.Identity.EntityFrameworkCore e BCrypt.Net-Next. O que não existia era a capacidade de, genericamente:
- Levantar um
DbContextde Identity que respeitasse os interceptors de auditoria/soft-delete do DevPack. - Emitir um JWT próprio (até aqui o canon só validava token de IdP externo).
- Publicar as chaves públicas para validação por terceiros (service-to-service).
O framework do template está movendo o default para Identity local (ver Template ADR-006), com dois schemes coexistindo — BFF cookie para browser e Bearer JWT (assimétrico, validado por JWKS) para mobile/service-to-service. Por G-01 (CLAUDE.md do workspace), tudo que é universal a qualquer consumidor mora no DevPack; o que é específico de como o template fiou mora no template.
A tensão é com ADR-001: o DevPack não embrulha frameworks. A emissão/validação de JWT não é um framework a embrulhar — é mecânica de Microsoft.IdentityModel.Tokens + BCL; expô-la como serviço genérico é legítimo. Já a configuração opinativa de um servidor OIDC (OpenIddict) não entra — é app-specific e foi descartada no escopo (ver Template ADR-006).
Decisão
Adicionar ao DevPack os blocos consumer-agnostic abaixo. Nenhum esconde a API do ASP.NET — AddCookie/AddJwtBearer/AddIdentityCore permanecem visíveis e chamados pelo consumidor.
BaseIdentityDbContext<TUser, TRole> — área Infrastructure.EfCore
public abstract class BaseIdentityDbContext<TUser, TRole>(DbContextOptions options)
: IdentityDbContext<TUser, TRole, int>(options)
where TUser : BaseIdentityUser
where TRole : BaseIdentityRole
{
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// Move as tabelas de Identity para o schema dedicado (default "identity"),
// dropa o prefixo AspNet (AspNetUsers -> identity.Users), mapeia AuditTimestamp [Owned]
// e aplica o filtro de soft-delete coerente com os interceptors.
builder.ApplyDevPackIdentityConventions<TUser, TRole>();
}
}
Combina IdentityDbContext com as convenções de auditoria/soft-delete do DevPack. O consumidor deriva com seus TUser/TRole concretos e segue plugando os interceptors via AddDevPackEfCoreInterceptors().
ITokenService + DefaultJwtTokenService — área Identity/Authentication/Bearer
public interface ITokenService
{
AccessToken IssueAccessToken(ClaimsIdentity identity, TimeSpan lifetime);
}
public sealed class DefaultJwtTokenService(ISigningKeyProvider keys, TimeProvider clock) : ITokenService;
- Assina assimetricamente (RSA ou ECDSA) com o
kidativo doISigningKeyProvider. - Issuer/audience/lifetime vêm de
IdentityTokenOptions(IAppOptions). - Não persiste nada e não conhece refresh — só transforma claims em JWT assinado.
ISigningKeyProvider — fornecimento e rotação de chaves
public interface ISigningKeyProvider
{
SigningCredentials ActiveSigningCredentials { get; } // chave privada ativa (com kid)
IReadOnlyCollection<SecurityKey> PublicKeys { get; } // ativa + em rotação, para JWKS
}
Implementações shipadas: EphemeralRsaSigningKeyProvider (dev — chave em memória), FileSigningKeyProvider (PEM), e contrato pronto para Key Vault no consumidor. Falha-cedo: em produção, ausência de chave configurada lança na startup (espelha o padrão do Template ADR-003 para Authority/Audience).
JwksDocumentBuilder — publicação RFC 7517
public static class JwksDocumentBuilder
{
public static JsonWebKeySet Build(IReadOnlyCollection<SecurityKey> publicKeys);
}
Converte as chaves públicas em um JWKS. O mapeamento do endpoint /.well-known/jwks.json é responsabilidade do consumidor (é fiação de app).
RefreshToken + IRefreshTokenStore — área Identity
public abstract class RefreshToken : Entity<Guid> // token hash, UserId, ExpiresAt, RevokedAt, ReplacedByTokenId
public interface IRefreshTokenStore
{
Task<RefreshToken?> FindActiveAsync(string tokenHash, CancellationToken ct);
Task RotateAsync(RefreshToken current, RefreshToken replacement, CancellationToken ct);
Task RevokeAsync(Guid userId, CancellationToken ct);
}
Contrato de rotação/revogação; o storage concreto (EF) fica no consumidor.
ServiceClient — base para client-credentials (service-to-service)
public abstract class ServiceClient : Entity<int> // ClientId, SecretHash (BCrypt), GrantedRoles
Suporta o endpoint client-credentials mínimo do consumidor (clientId/secret → JWT de serviço), sem trazer um servidor OAuth completo.
Helpers finos de DI
IdentitySchemas.Bff = "Bff"; // constante de scheme do cookie BFF
services.AddDevPackDualScheme(selector => ...); // só pluga o ForwardDefaultSelector
services.AddDevPackTokenService<TSigningKeyProvider>();
AddDevPackDualScheme registra apenas o PolicyScheme com o ForwardDefaultSelector (header Bearer → JWT, senão → cookie). As chamadas AddCookie(...) e AddJwtBearer(...) continuam explícitas no Program.cs do consumidor — transparência exigida pelo ADR-001.
Filosofia preservada
- ADR-001 (no wrappers). Nada acima esconde o ASP.NET.
ITokenServicenão embrulha framework algum (é mecânicaMicrosoft.IdentityModel); os helpers de DI compõem, não ocultam. Não entra config de servidor OIDC/OpenIddict — é opinativa e app-specific. - ADR-004 (permissões derivadas).
ICurrentUsercontinua semPermissions. As roles bakeadas no token/cookie alimentamUserRoles;RoleBasedPermissionEvaluator+IRolePermissionResolverseguem resolvendo permissões. O pipeline de autorização não muda. - ADR-003 (AuditTimestamp).
BaseIdentityDbContextmapeiaAuditTimestamp [Owned]nas tabelas de Identity sem regressão.
Convenção de tabelas — schema dedicado, sem prefixo AspNet
ApplyDevPackIdentityConventions move as sete tabelas de Identity para um schema dedicado (default identity, configurável via BaseIdentityDbContext.IdentitySchema) e dropa o prefixo AspNet: AspNetUsers → identity.Users, AspNetRoles → identity.Roles, AspNetUserRoles → identity.UserRoles, AspNetUserClaims → identity.UserClaims, AspNetUserLogins → identity.UserLogins, AspNetUserTokens → identity.UserTokens, AspNetRoleClaims → identity.RoleClaims. Isola o Identity das tabelas de domínio do consumidor e remove o ruído do prefixo do framework.
Alternativas consideradas
| Opção | Por que descartada |
|---|---|
Assinatura simétrica (HMAC) no ITokenService | Service-to-service exige validação sem o segredo. Assimétrica + JWKS é requisito (Template ADR-006). |
| Integração OpenIddict no DevPack | Opinativa, pesada, app-specific. Fora do escopo ratificado. Violaria ADR-001 e o minimalismo. |
AppDbContext/AppUser concretos no DevPack | Tipos concretos de user/role são específicos do consumidor. DevPack só fornece as bases. |
AddDevPackIdentityServer() god-method | Esconderia a config do ASP.NET. Direto contra ADR-001. |
RefreshTokenStore concreto no DevPack | Storage depende do provider/contexto do consumidor. DevPack só define o contrato. |
Consequências
- Adições não-quebrantes. Nenhum tipo existente muda assinatura; consumidores atuais não são afetados.
- O template
ITLab.Template.Nextconsome estes blocos ao bumpar para 10.4.0 (ver Template ADR-006): derivaAppUser/AppRole/AppDbContext, implementaIRefreshTokenStore/ServiceClientEF e mapeia os endpoints/v1/auth/*+ JWKS. - Responsabilidade de chaves vira contrato explícito: dev usa RSA ephemeral; produção configura chave persistente (PEM/Key Vault) ou a startup falha — sem 401 silencioso.
- Atualizar
devpack-docs/areas/Identity/README.md(seção Self-issued tokens & dual scheme) eCHANGELOG.mdsob[Unreleased]. - Testes novos:
DefaultJwtTokenServiceTests(claims→JWT, kid, expiração),JwksDocumentBuilderTests(round-trip de validação com a chave pública),EphemeralRsaSigningKeyProviderTests.
Referências
- ADR-001 — No framework wrappers
- ADR-004 — Role-based permission resolution
- Template ADR-006 — Local Identity + dual scheme default
src/ITLab.Template.DevPack/Identity/BaseIdentityUser.cs,BaseIdentityRole.cs(bases existentes)- CLAUDE.md do workspace § Princípio G-01.