Segurança .NET JWT ASP.NET Core APIs

Segurança em APIs .NET: JWT, rate limiting e proteção contra os ataques mais comuns

Guia prático de segurança para APIs ASP.NET Core: autenticação JWT com refresh tokens, autorização baseada em claims, rate limiting nativo do .NET 8.

N
Neryx Digital Architects
1 de fevereiro de 2026
14 min de leitura
270 profissionais leram
Categoria: Arquitetura Público: Times de engenharia e produto Etapa: Decisão

Segurança em APIs é uma das áreas onde os times mais cometem erros silenciosos — o sistema funciona perfeitamente para uso legítimo, mas está aberto para vetores de ataque que só aparecem quando já é tarde. Este guia cobre as principais camadas de proteção que toda API ASP.NET Core em produção precisa ter.

1. Autenticação JWT com refresh tokens

JWT (JSON Web Token) é o padrão de fato para autenticação stateless em APIs. A implementação correta exige atenção a alguns detalhes críticos que a maioria dos tutoriais ignora.

// Program.cs — configuração de autenticação JWT
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"]!)),
            ClockSkew = TimeSpan.Zero // IMPORTANTE: sem tolerância de tempo padrão de 5 min
        };
    });

builder.Services.AddAuthorization();

Geração de tokens com expiração curta

public class TokenService
{
    private readonly IConfiguration _config;
public TokenService(IConfiguration config) => _config = config;

public string GerarAccessToken(Usuario usuario)
{
    var claims = new[]
    {
        new Claim(ClaimTypes.NameIdentifier, usuario.Id.ToString()),
        new Claim(ClaimTypes.Email, usuario.Email),
        new Claim(ClaimTypes.Role, usuario.Role.ToString()),
        new Claim("tenant_id", usuario.TenantId.ToString()),
    };

    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:SecretKey"]!));
    var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

    var token = new JwtSecurityToken(
        issuer: _config["Jwt:Issuer"],
        audience: _config["Jwt:Audience"],
        claims: claims,
        expires: DateTime.UtcNow.AddMinutes(15), // access token curto: 15 minutos
        signingCredentials: creds);

    return new JwtSecurityTokenHandler().WriteToken(token);
}

public string GerarRefreshToken()
{
    // Refresh token é um valor aleatório opaco, não um JWT
    var randomBytes = new byte[64];
    using var rng = RandomNumberGenerator.Create();
    rng.GetBytes(randomBytes);
    return Convert.ToBase64String(randomBytes);
}

}

// Endpoint de refresh [HttpPost(“refresh”)] [AllowAnonymous] public async Task<IActionResult> Refresh([FromBody] RefreshTokenRequest request) { var usuario = await _usuarioService.ValidarRefreshToken(request.RefreshToken); if (usuario == null) return Unauthorized(new { error = “Refresh token inválido ou expirado.” });

// Rotacionar o refresh token (invalidar o antigo, gerar novo)
var novoRefreshToken = _tokenService.GerarRefreshToken();
await _usuarioService.RotacionarRefreshToken(usuario.Id, request.RefreshToken, novoRefreshToken);

return Ok(new
{
    accessToken = _tokenService.GerarAccessToken(usuario),
    refreshToken = novoRefreshToken
});

}

Por que access token curto (15 min) + refresh token? Se um access token for interceptado, o atacante tem no máximo 15 minutos antes de expirar. O refresh token fica armazenado com segurança (HttpOnly cookie ou banco de dados) e é rotacionado a cada uso — se um refresh token for usado duas vezes, indica comprometimento e a sessão é invalidada.

2. Autorização baseada em claims e políticas

// Definir políticas de autorização
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy =>
        policy.RequireRole("Admin"));

    options.AddPolicy("MesmoTenant", policy =>
        policy.RequireClaim("tenant_id"));

    options.AddPolicy("GerenciarPedidos", policy =>
        policy.RequireAssertion(ctx =>
            ctx.User.IsInRole("Admin") ||
            ctx.User.HasClaim("permissao", "pedidos.write")));
});

// Nos controllers
[Authorize(Policy = "GerenciarPedidos")]
[HttpPost("{id}/confirmar")]
public async Task<IActionResult> Confirmar(Guid id)
{
    // Verificação adicional: o pedido pertence ao tenant do usuário logado?
    var tenantId = Guid.Parse(User.FindFirstValue("tenant_id")!);
    var pedido = await _mediator.Send(new GetPedidoQuery(id));

    if (pedido?.TenantId != tenantId)
        return Forbid(); // 403, não 404 — não revelar que o recurso existe

    return Ok(await _mediator.Send(new ConfirmarPedidoCommand(id)));
}

3. Rate limiting nativo do .NET 8

O .NET 8 introduziu rate limiting nativo no ASP.NET Core — sem precisar de pacotes externos.

// Program.cs
builder.Services.AddRateLimiter(options =>
{
    // Política global: 100 requisições por minuto por IP
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(ctx =>
        RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown",
            factory: _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 100,
                Window = TimeSpan.FromMinutes(1),
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                QueueLimit = 0
            }));
// Política específica para login: 5 tentativas por minuto (anti-brute-force)
options.AddFixedWindowLimiter("login", opt =>
{
    opt.PermitLimit = 5;
    opt.Window = TimeSpan.FromMinutes(1);
    opt.QueueLimit = 0;
});

// Resposta quando o limite é excedido
options.OnRejected = async (context, token) =>
{
    context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
    context.HttpContext.Response.Headers["Retry-After"] = "60";
    await context.HttpContext.Response.WriteAsJsonAsync(
        new { error = "Muitas requisições. Tente novamente em 60 segundos." }, token);
};

});

app.UseRateLimiter();

// Aplicar política específica no endpoint de login [HttpPost(“login”)] [EnableRateLimiting(“login”)] public async Task<IActionResult> Login([FromBody] LoginRequest request) { … }

4. Proteção contra OWASP Top 10 no ASP.NET Core

Injeção (SQL Injection)

// NUNCA: concatenação de SQL
var sql = "SELECT * FROM usuarios WHERE email = '" + email + "'";

// SEMPRE: parâmetros (EF Core já faz isso automaticamente) var usuario = await _context.Usuarios .FirstOrDefaultAsync(u => u.Email == email);

// Com Dapper: sempre use parâmetros nomeados var usuario = await connection.QueryFirstOrDefaultAsync<Usuario>( “SELECT * FROM usuarios WHERE email = @Email”, new { Email = email }); // CORRETO — parametrizado

Exposição de dados sensíveis

// NUNCA retorne entidades de domínio diretamente — elas podem ter campos sensíveis
[HttpGet("{id}")]
public async Task<IActionResult> Get(Guid id)
{
    var usuario = await _repository.GetByIdAsync(id);
    return Ok(usuario); // ERRADO: retorna senha hasheada, tokens, dados internos
}

// SEMPRE mapeie para DTOs de resposta public record UsuarioResponseDto(Guid Id, string Nome, string Email, string Role);

[HttpGet(“{id}”)] public async Task<IActionResult> Get(Guid id) { var usuario = await _repository.GetByIdAsync(id); return Ok(new UsuarioResponseDto(usuario.Id, usuario.Nome, usuario.Email, usuario.Role.ToString())); }

Headers de segurança

// Middleware para headers de segurança
app.Use(async (context, next) =>
{
    context.Response.Headers["X-Content-Type-Options"] = "nosniff";
    context.Response.Headers["X-Frame-Options"] = "DENY";
    context.Response.Headers["X-XSS-Protection"] = "1; mode=block";
    context.Response.Headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
    context.Response.Headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()";
    context.Response.Headers["Content-Security-Policy"] =
        "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'";
    await next();
});

CORS bem configurado

// NUNCA em produção:
builder.Services.AddCors(o => o.AddPolicy("Dev", p => p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()));

// SEMPRE: origens explícitas builder.Services.AddCors(options => { options.AddPolicy(“Producao”, policy => policy.WithOrigins( “https://neryx.com.br”, “https://app.neryx.com.br”) .WithMethods(“GET”, “POST”, “PUT”, “DELETE”) .WithHeaders(“Authorization”, “Content-Type”) .AllowCredentials()); });

5. Secrets: nunca no código-fonte

# NUNCA: chaves hardcoded no código ou no appsettings.json commitado
{
  "Jwt": {
    "SecretKey": "minha-chave-super-secreta-123" // ERRADO
  }
}

SEMPRE: use variáveis de ambiente ou AWS Secrets Manager / Azure Key Vault

Em desenvolvimento: dotnet user-secrets

dotnet user-secrets set “Jwt:SecretKey” “chave-local-desenvolvimento”

Em produção (Docker/ECS/Kubernetes): variáveis de ambiente

JWT__SECRETKEY=chave-producao-longa-e-aleatoria

No código .NET, o ConfigurationBuilder já resolve a hierarquia corretamente:

1. appsettings.json (valores padrão, sem secrets)

2. appsettings.{Environment}.json

3. User Secrets (dev)

4. Variáveis de ambiente (produção) — sobrescreve tudo acima

6. Checklist de segurança antes de ir para produção

  • Access tokens com expiração curta (≤ 30 min) + refresh token rotacionado
  • Todas as rotas protegidas têm [Authorize] explícito (ou política global)
  • Rate limiting no login, registro e endpoints sensíveis
  • Nenhuma secret no código-fonte ou git
  • HTTPS obrigatório — app.UseHsts() + app.UseHttpsRedirection()
  • Headers de segurança configurados
  • CORS com origens explícitas
  • Todas as queries parametrizadas (sem concatenação SQL)
  • Respostas de erro sem stack trace em produção
  • Logs de tentativas de login falhas e acessos negados

Conclusão

Segurança em APIs não é uma funcionalidade que se adiciona depois — é uma camada que precisa estar presente desde o início. As medidas cobertas aqui (JWT bem configurado, rate limiting, headers corretos, sem secrets no código) eliminam a maioria dos vetores de ataque mais comuns sem complexidade excessiva.

Se você está preparando uma API .NET para produção e quer uma revisão de segurança, a Neryx faz análise de código e arquitetura com foco em segurança e boas práticas. 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.