Pular para o conteúdo principal
Versão: 10.4.1

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:

  1. Levantar um DbContext de Identity que respeitasse os interceptors de auditoria/soft-delete do DevPack.
  2. Emitir um JWT próprio (até aqui o canon só validava token de IdP externo).
  3. 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 kid ativo do ISigningKeyProvider.
  • 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. ITokenService não embrulha framework algum (é mecânica Microsoft.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). ICurrentUser continua sem Permissions. As roles bakeadas no token/cookie alimentam UserRoles; RoleBasedPermissionEvaluator + IRolePermissionResolver seguem resolvendo permissões. O pipeline de autorização não muda.
  • ADR-003 (AuditTimestamp). BaseIdentityDbContext mapeia AuditTimestamp [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çãoPor que descartada
Assinatura simétrica (HMAC) no ITokenServiceService-to-service exige validação sem o segredo. Assimétrica + JWKS é requisito (Template ADR-006).
Integração OpenIddict no DevPackOpinativa, pesada, app-specific. Fora do escopo ratificado. Violaria ADR-001 e o minimalismo.
AppDbContext/AppUser concretos no DevPackTipos concretos de user/role são específicos do consumidor. DevPack só fornece as bases.
AddDevPackIdentityServer() god-methodEsconderia a config do ASP.NET. Direto contra ADR-001.
RefreshTokenStore concreto no DevPackStorage 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.Next consome estes blocos ao bumpar para 10.4.0 (ver Template ADR-006): deriva AppUser/AppRole/AppDbContext, implementa IRefreshTokenStore/ServiceClient EF 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) e CHANGELOG.md sob [Unreleased].
  • Testes novos: DefaultJwtTokenServiceTests (claims→JWT, kid, expiração), JwksDocumentBuilderTests (round-trip de validação com a chave pública), EphemeralRsaSigningKeyProviderTests.

Referências