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: