.NET OAuth2 OIDC IdentityServer Segurança

OAuth2 e OpenID Connect no .NET: autenticação centralizada com Duende IdentityServer

Como implementar autenticação OAuth2/OIDC em .NET com Duende IdentityServer: Authorization Code Flow com PKCE, Client Credentials, refresh tokens.

N
Neryx Digital Architects
14 de setembro de 2025
14 min de leitura
250 profissionais leram
Categoria: .NET Público: Times de engenharia e produto Etapa: Decisão

OAuth2 é o protocolo de autorização e OpenID Connect (OIDC) é a camada de identidade construída sobre ele. Juntos resolvem o problema central de autenticação distribuída: como um usuário prova sua identidade uma vez e acessa múltiplos serviços sem re-autenticar? Duende IdentityServer é a implementação de referência para .NET — usado por Microsoft, Okta e grandes SaaS ao redor do mundo.

Os três fluxos que você precisa conhecer

Authorization Code + PKCE: para aplicações web e SPAs onde um usuário humano faz login. O mais seguro — o access token nunca fica exposto na URL. PKCE (Proof Key for Code Exchange) protege contra interceptação do código de autorização.

Client Credentials: para comunicação máquina-a-máquina (M2M). Um microsserviço se autentica com client_id + client_secret e recebe um token para chamar outros serviços. Sem usuário no fluxo.

Refresh Token: access tokens têm vida curta (15–60 min). Refresh tokens permitem obter novos access tokens sem pedir ao usuário para fazer login novamente.

Configurando o Duende IdentityServer

dotnet add package Duende.IdentityServer

// Program.cs do projeto IdentityServer:
builder.Services.AddIdentityServer(options =>
{
    options.Events.RaiseErrorEvents = true;
    options.Events.RaiseInformationEvents = true;
    options.Events.RaiseFailureEvents = true;
    options.Events.RaiseSuccessEvents = true;
})
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryApiResources(Config.ApiResources)
.AddInMemoryClients(Config.Clients)
.AddAspNetIdentity<ApplicationUser>()  // integração com ASP.NET Core Identity
.AddDeveloperSigningCredential();       // em produção: .AddSigningCredential(cert)

app.UseIdentityServer();
// Config.cs — definindo recursos, escopos e clientes:
public static class Config
{
    public static IEnumerable<IdentityResource> IdentityResources =>
    [
        new IdentityResources.OpenId(),   // sub (subject/user id)
        new IdentityResources.Profile(),  // name, email, picture
        new IdentityResources.Email(),
        new IdentityResource("tenant", "Tenant ID", ["tenant_id", "tenant_slug"]),
    ];

    public static IEnumerable<ApiScope> ApiScopes =>
    [
        new ApiScope("api.read",  "Leitura da API"),
        new ApiScope("api.write", "Escrita na API"),
        new ApiScope("api.admin", "Acesso administrativo"),
    ];

    public static IEnumerable<ApiResource> ApiResources =>
    [
        new ApiResource("neryx-api", "Neryx API")
        {
            Scopes = { "api.read", "api.write", "api.admin" },
            UserClaims = { "tenant_id", "role" },
        }
    ];

    public static IEnumerable<Client> Clients =>
    [
        // SPA React — Authorization Code + PKCE (sem client_secret)
        new Client
        {
            ClientId = "neryx-spa",
            ClientName = "Neryx Web App",
            AllowedGrantTypes = GrantTypes.Code,
            RequirePkce = true,
            RequireClientSecret = false,  // SPAs não podem guardar secrets

            RedirectUris           = { "https://app.neryx.com.br/auth/callback" },
            PostLogoutRedirectUris = { "https://app.neryx.com.br/" },
            AllowedCorsOrigins     = { "https://app.neryx.com.br" },

            AllowedScopes = { "openid", "profile", "email", "tenant", "api.read", "api.write" },
            AllowOfflineAccess = true,   // habilita refresh tokens
            AccessTokenLifetime = 900,   // 15 minutos
            RefreshTokenExpiration = TokenExpiration.Sliding,
            SlidingRefreshTokenLifetime = 86400 * 30, // 30 dias
        },

        // Microsserviço — Client Credentials (M2M, sem usuário)
        new Client
        {
            ClientId = "pedidos-service",
            ClientSecrets = { new Secret("secret-pedidos".Sha256()) },
            AllowedGrantTypes = GrantTypes.ClientCredentials,
            AllowedScopes = { "api.read", "api.write" },
            AccessTokenLifetime = 3600, // 1 hora
        },

        // App mobile — Authorization Code + PKCE com custom scheme
        new Client
        {
            ClientId = "neryx-mobile",
            AllowedGrantTypes = GrantTypes.Code,
            RequirePkce = true,
            RequireClientSecret = false,
            RedirectUris = { "com.neryx.app://auth/callback" },
            AllowedScopes = { "openid", "profile", "email", "api.read", "api.write" },
            AllowOfflineAccess = true,
            AccessTokenLifetime = 900,
        },
    ];
}

Protegendo a API com JWT Bearer

// Na API .NET que será protegida:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://auth.neryx.com.br"; // URL do IdentityServer
        options.Audience = "neryx-api";
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ClockSkew = TimeSpan.FromSeconds(30), // tolerância de clock entre servidores
    };

    // Para Reference Tokens (mais seguro que JWT — revogação imediata):
    // options.ForwardDefaultSelector = Selector.ForwardReferenceToken("introspection");
});

builder.Services.AddAuthorization(options => { options.AddPolicy(“api-leitura”, policy => policy.RequireAuthenticatedUser() .RequireClaim(“scope”, “api.read”));

options.AddPolicy("api-escrita", policy =>
    policy.RequireAuthenticatedUser()
          .RequireClaim("scope", "api.write"));

options.AddPolicy("admin", policy =>
    policy.RequireAuthenticatedUser()
          .RequireClaim("scope", "api.admin")
          .RequireClaim("role", "admin"));

options.AddPolicy("mesmo-tenant", policy =>
    policy.RequireAuthenticatedUser()
          .RequireAssertion(ctx =>
              ctx.User.HasClaim("tenant_id", ctx.Resource?.ToString() ?? "")));

});

// Endpoints protegidos: app.MapGet(“/api/produtos”, GetProdutos).RequireAuthorization(“api-leitura”); app.MapPost(“/api/produtos”, CriarProduto).RequireAuthorization(“api-escrita”); app.MapDelete(“/api/admin/usuarios”, DeletarUsuario).RequireAuthorization(“admin”);

Client Credentials: microsserviço chamando outro

dotnet add package IdentityModel

// Registrar cliente M2M com renovação automática de token: builder.Services.AddClientCredentialsTokenManagement() .AddClient(“pedidos-service”, client => { client.TokenEndpoint = “https://auth.neryx.com.br/connect/token”; client.ClientId = “pedidos-service”; client.ClientSecret = builder.Configuration[“Auth:ClientSecret”]; client.Scope = “api.read api.write”; });

// HttpClient que injeta o token automaticamente: builder.Services.AddClientCredentialsHttpClient(“produtos-api”, “pedidos-service”, client => client.BaseAddress = new Uri(“https://api.neryx.com.br”));

// Uso no serviço: public class PedidoService { private readonly HttpClient _produtosApi;

public PedidoService(IHttpClientFactory factory)
    => _produtosApi = factory.CreateClient("produtos-api");
    // O token Bearer é injetado automaticamente — zero boilerplate

public async Task&lt;ProdutoDto?&gt; GetProdutoAsync(Guid id, CancellationToken ct)
    => await _produtosApi.GetFromJsonAsync&lt;ProdutoDto&gt;($"/api/produtos/{id}", ct);

}

Claims customizadas: enriquecendo o token com dados do tenant

// IProfileService — adiciona claims ao token no momento da geração:
public class ProfileService : IProfileService
{
    private readonly UserManager<ApplicationUser> _users;
    private readonly ITenantRepository _tenants;
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
    var user = await _users.GetUserAsync(context.Subject);
    if (user == null) return;

    var tenant = await _tenants.GetByUserIdAsync(user.Id);

    var claims = new List&lt;Claim&gt;
    {
        new("email", user.Email!),
        new("name", user.FullName),
        new("role", user.Role),
    };

    // Adiciona claims do tenant se o cliente solicitou o scope "tenant"
    if (context.RequestedClaimTypes.Contains("tenant_id") && tenant != null)
    {
        claims.Add(new("tenant_id", tenant.Id.ToString()));
        claims.Add(new("tenant_slug", tenant.Slug));
        claims.Add(new("tenant_plan", tenant.Plan));
    }

    context.IssuedClaims.AddRange(claims);
}

public async Task IsActiveAsync(IsActiveContext context)
{
    var user = await _users.GetUserAsync(context.Subject);
    context.IsActive = user != null && !user.IsBlocked;
}

}

// Registrar: builder.Services.AddTransient<IProfileService, ProfileService>();

Revogação de tokens e logout global (SSO)

// Logout que invalida sessão em todos os clientes (back-channel logout):
[HttpPost("logout")]
public async Task<IActionResult> Logout(string? returnUrl = null)
{
    // Limpa cookie de autenticação local
    await HttpContext.SignOutAsync();
// Redireciona para IdentityServer que notifica todos os clientes via back-channel
return SignOut(new AuthenticationProperties { RedirectUri = returnUrl ?? "/" },
    CookieAuthenticationDefaults.AuthenticationScheme,
    OpenIdConnectDefaults.AuthenticationScheme);

}

// Introspection endpoint — para Reference Tokens (revogação imediata): // Ao usar Reference Tokens, a API chama o IdentityServer a cada request para validar // Mais seguro que JWT (pode revogar imediatamente), mas adiciona latência // Trade-off: JWT = sem latência extra, mas token válido até expirar mesmo após logout // Reference Token = latência extra por request, mas revogação instantânea

builder.Services.AddAuthentication() .AddOAuth2Introspection(“introspection”, options => { options.Authority = “https://auth.neryx.com.br”; options.ClientId = “neryx-api”; options.ClientSecret = “api-introspection-secret”; options.EnableCaching = true; options.CacheDuration = TimeSpan.FromMinutes(5); // cache para reduzir chamadas });

Ambiente de produção: signing certificates e persistência

// Em produção, substitua AddDeveloperSigningCredential por certificado real:
var cert = new X509Certificate2(
    builder.Configuration["IdentityServer:CertPath"],
    builder.Configuration["IdentityServer:CertPassword"]);

builder.Services.AddIdentityServer() .AddSigningCredential(cert) // assina os tokens JWTs .AddValidationKey(certAnterior) // mantém chave anterior para tokens ainda válidos .AddOperationalStore(options => // persiste grants, tokens e device codes { options.ConfigureDbContext = b => b.UseNpgsql(connString); options.EnableTokenCleanup = true; // limpeza automática de tokens expirados options.TokenCleanupInterval = 3600; // a cada hora });

Quando usar IdentityServer vs Auth0 vs Cognito

Duende IdentityServer é ideal quando você precisa de controle total sobre o fluxo de autenticação (claims customizadas, lógica de multi-tenancy, integração com LDAP/AD), quer rodar on-premise ou em VPC privada, ou tem volume suficiente para justificar o custo de licença. Auth0 e AWS Cognito são serviços gerenciados — menos controle, mais conveniência. Para SaaS early-stage sem requisitos especiais, Auth0 ou Cognito evitam a complexidade de operar o servidor de identidade.

Conclusão

OAuth2 + OIDC com Duende IdentityServer centraliza toda a autenticação da plataforma — uma única fonte de verdade para identidade, tokens e sessões. O Client Credentials Flow elimina tokens hardcoded entre microsserviços, e o SSO melhora a experiência do usuário em sistemas com múltiplas aplicações. O investimento em configurar corretamente paga dividendos quando o sistema escala.

Se você precisa implementar autenticação centralizada para uma plataforma SaaS ou migrar de tokens hardcoded para OAuth2, a Neryx pode desenhar e implementar a solução. Consultoria inicial gratuita.

Leitura complementar:

Quer sair do modo reativo e priorizar o que mais importa?

O diagnóstico de maturidade ajuda a transformar sintomas operacionais em um plano mais claro de evolução.

Avaliar maturidade

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.