Securing REST APIs
13 minute read
Security is a critical aspect of API design and implementation. All REST APIs should use HTTPS to ensure encrypted communication between clients and servers. Beyond transport security, authentication and authorization mechanisms are essential for controlling access to your API resources.
This guide covers authentication methods for REST APIs from both server-side (implementing authentication in your API) and client-side (consuming authenticated APIs with HttpClient) perspectives.
Authentication Methods Comparison
| Method | Complexity | Security Level | Scalability | Best For | Client Complexity |
|---|---|---|---|---|---|
| API Key | Low | Moderate | High | Public APIs, Simple integrations | Very Low |
| Bearer Token (JWT) | Moderate | High | Very High | Modern web/mobile apps, Microservices | Low |
| OAuth 2.0 / OIDC | High | Very High | Very High | Third-party integrations, Enterprise | Moderate |
| Client Certificates (mTLS) | High | Very High | Moderate | Server-to-server, High-security systems | High |
| Basic Auth | Very Low | Low | High | Internal tools, Development only | Very Low |
Client-Side Authentication with HttpClient
This section demonstrates how to implement various authentication methods when consuming REST APIs with .NET’s HttpClient.
Bearer Token Authentication
Bearer tokens (typically JWTs) are the most common authentication method for modern APIs.
Static Token Configuration
using System.Net.Http.Headers;
HttpClient httpClient = new()
{
BaseAddress = new Uri("https://api.example.com")
};
// Set bearer token for all requests
string accessToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...";
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
// Make authenticated request
HttpResponseMessage response = await httpClient.GetAsync("api/v1/tasks");
Per-Request Token
using System.Net.Http.Headers;
HttpRequestMessage request = new(HttpMethod.Get, "api/v1/tasks");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
HttpResponseMessage response = await httpClient.SendAsync(request);
Token Refresh with DelegatingHandler (Recommended)
For production applications, implement automatic token refresh:
using System.Net;
using System.Net.Http.Headers;
/// <summary>
/// Handles Bearer token authentication with automatic refresh.
/// </summary>
public sealed class BearerTokenHandler : DelegatingHandler
{
private readonly ITokenService _tokenService;
private readonly SemaphoreSlim _semaphore = new(1, 1);
public BearerTokenHandler(ITokenService tokenService)
{
_tokenService = tokenService;
InnerHandler = new HttpClientHandler();
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
// Get current token
string accessToken = await _tokenService.GetAccessTokenAsync(cancellationToken);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
// Handle token expiration
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
await _semaphore.WaitAsync(cancellationToken);
try
{
// Refresh token
accessToken = await _tokenService.RefreshTokenAsync(cancellationToken);
// Retry with new token
HttpRequestMessage retryRequest = await CloneRequestAsync(request);
retryRequest.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
response = await base.SendAsync(retryRequest, cancellationToken);
}
finally
{
_semaphore.Release();
}
}
return response;
}
private static async Task<HttpRequestMessage> CloneRequestAsync(HttpRequestMessage request)
{
HttpRequestMessage clone = new(request.Method, request.RequestUri);
foreach (KeyValuePair<string, IEnumerable<string>> header in request.Headers)
{
clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
if (request.Content is not null)
{
byte[] content = await request.Content.ReadAsByteArrayAsync();
clone.Content = new ByteArrayContent(content);
foreach (KeyValuePair<string, IEnumerable<string>> header in request.Content.Headers)
{
clone.Content.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
}
return clone;
}
}
/// <summary>
/// Token service interface for managing access tokens.
/// </summary>
public interface ITokenService
{
Task<string> GetAccessTokenAsync(CancellationToken cancellationToken = default);
Task<string> RefreshTokenAsync(CancellationToken cancellationToken = default);
}
Registration with IHttpClientFactory
using Microsoft.Extensions.DependencyInjection;
IServiceCollection services = new ServiceCollection();
// Register token service
services.AddSingleton<ITokenService, TokenService>();
// Register HttpClient with authentication handler
services.AddHttpClient("AuthenticatedApi", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
})
.AddHttpMessageHandler<BearerTokenHandler>();
services.AddTransient<BearerTokenHandler>();
API Key Authentication
API keys are simple but effective for server-to-server communication.
Header-based API Key
using System.Net.Http.Headers;
HttpClient httpClient = new()
{
BaseAddress = new Uri("https://api.example.com")
};
// Common header names: X-API-Key, X-Api-Key, Api-Key, Authorization
httpClient.DefaultRequestHeaders.Add("X-API-Key", "your-api-key-here");
// Or using Authorization header with custom scheme
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("ApiKey", "your-api-key-here");
HttpResponseMessage response = await httpClient.GetAsync("api/v1/data");
API Key Handler with Rate Limit Handling
using System.Net;
/// <summary>
/// Handles API key authentication with rate limit awareness.
/// </summary>
public sealed class ApiKeyHandler : DelegatingHandler
{
private readonly string _apiKey;
private readonly string _headerName;
public ApiKeyHandler(string apiKey, string headerName = "X-API-Key")
{
_apiKey = apiKey;
_headerName = headerName;
InnerHandler = new HttpClientHandler();
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
request.Headers.Add(_headerName, _apiKey);
HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
// Log rate limit information if present
if (response.Headers.TryGetValues("X-RateLimit-Remaining", out IEnumerable<string>? remaining))
{
int remainingRequests = int.Parse(remaining.First());
if (remainingRequests < 10)
{
// Log warning about approaching rate limit
Console.WriteLine($"Warning: Only {remainingRequests} API requests remaining");
}
}
// Handle rate limiting
if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
if (response.Headers.RetryAfter?.Delta is TimeSpan retryAfter)
{
await Task.Delay(retryAfter, cancellationToken);
return await SendAsync(request, cancellationToken);
}
}
return response;
}
}
Client Certificate Authentication (mTLS)
For high-security scenarios, mutual TLS (mTLS) provides strong authentication.
Basic Certificate Configuration
using System.Security.Cryptography.X509Certificates;
// Load certificate from file
X509Certificate2 clientCertificate = new(
"path/to/client.pfx",
"certificate-password",
X509KeyStorageFlags.MachineKeySet);
// Configure handler with certificate
HttpClientHandler handler = new();
handler.ClientCertificates.Add(clientCertificate);
// Optional: Custom server certificate validation
handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
{
// In production, implement proper validation
// This example accepts all certificates (NOT for production!)
return true;
};
HttpClient httpClient = new(handler)
{
BaseAddress = new Uri("https://secure-api.example.com")
};
HttpResponseMessage response = await httpClient.GetAsync("api/v1/secure-data");
Certificate from Windows Certificate Store
using System.Security.Cryptography.X509Certificates;
// Load from CurrentUser store
using X509Store store = new(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certificates = store.Certificates.Find(
X509FindType.FindByThumbprint,
"certificate-thumbprint-here",
validOnly: true);
if (certificates.Count == 0)
{
throw new InvalidOperationException("Client certificate not found");
}
X509Certificate2 clientCertificate = certificates[0];
// Configure SocketsHttpHandler for better performance
SocketsHttpHandler handler = new()
{
SslOptions =
{
ClientCertificates = new X509CertificateCollection { clientCertificate }
},
PooledConnectionLifetime = TimeSpan.FromMinutes(15)
};
HttpClient httpClient = new(handler)
{
BaseAddress = new Uri("https://secure-api.example.com")
};
Certificate Handler with Retry Logic
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
/// <summary>
/// Handles client certificate authentication for mTLS.
/// </summary>
public sealed class CertificateAuthHandler : DelegatingHandler
{
private readonly X509Certificate2 _certificate;
private readonly string[] _expectedServerThumbprints;
public CertificateAuthHandler(
X509Certificate2 certificate,
string[]? expectedServerThumbprints = null)
{
_certificate = certificate;
_expectedServerThumbprints = expectedServerThumbprints ?? [];
SocketsHttpHandler innerHandler = new()
{
SslOptions =
{
ClientCertificates = new X509CertificateCollection { _certificate },
RemoteCertificateValidationCallback = ValidateServerCertificate
},
PooledConnectionLifetime = TimeSpan.FromMinutes(15)
};
InnerHandler = innerHandler;
}
private bool ValidateServerCertificate(
object sender,
X509Certificate? certificate,
X509Chain? chain,
SslPolicyErrors sslPolicyErrors)
{
// If no specific thumbprints configured, use default validation
if (_expectedServerThumbprints.Length == 0)
{
return sslPolicyErrors == SslPolicyErrors.None;
}
// Validate against expected thumbprints (certificate pinning)
if (certificate is null)
{
return false;
}
string thumbprint = certificate.GetCertHashString();
return _expectedServerThumbprints.Contains(thumbprint, StringComparer.OrdinalIgnoreCase);
}
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
// Certificate is automatically sent via SslOptions
return base.SendAsync(request, cancellationToken);
}
}
Dependency Injection with Certificates
using Microsoft.Extensions.DependencyInjection;
using System.Security.Cryptography.X509Certificates;
IServiceCollection services = new ServiceCollection();
// Register certificate as singleton
services.AddSingleton<X509Certificate2>(sp =>
{
// Load from configuration or environment
string certPath = Environment.GetEnvironmentVariable("CLIENT_CERT_PATH")!;
string certPassword = Environment.GetEnvironmentVariable("CLIENT_CERT_PASSWORD")!;
return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.MachineKeySet);
});
// Register HttpClient with certificate handler
services.AddHttpClient("SecureApi")
.ConfigurePrimaryHttpMessageHandler(sp =>
{
X509Certificate2 certificate = sp.GetRequiredService<X509Certificate2>();
return new SocketsHttpHandler
{
SslOptions =
{
ClientCertificates = new X509CertificateCollection { certificate }
},
PooledConnectionLifetime = TimeSpan.FromMinutes(15)
};
});
Basic Authentication
using System.Net.Http.Headers;
using System.Text;
string username = "user";
string password = "password";
// Create Base64-encoded credentials
string credentials = Convert.ToBase64String(
Encoding.ASCII.GetBytes($"{username}:{password}"));
HttpClient httpClient = new()
{
BaseAddress = new Uri("https://api.example.com")
};
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic", credentials);
HttpResponseMessage response = await httpClient.GetAsync("api/v1/data");
Combined Authentication Handler
For APIs that support multiple authentication methods:
using System.Net.Http.Headers;
/// <summary>
/// Authentication types supported by the handler.
/// </summary>
public enum AuthenticationType
{
None,
Bearer,
ApiKey,
Basic
}
/// <summary>
/// Flexible authentication handler supporting multiple auth methods.
/// </summary>
public sealed class MultiAuthHandler : DelegatingHandler
{
private readonly AuthenticationType _authType;
private readonly string _credential;
private readonly string _headerName;
public MultiAuthHandler(
AuthenticationType authType,
string credential,
string headerName = "X-API-Key")
{
_authType = authType;
_credential = credential;
_headerName = headerName;
InnerHandler = new HttpClientHandler();
}
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
switch (_authType)
{
case AuthenticationType.Bearer:
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", _credential);
break;
case AuthenticationType.ApiKey:
request.Headers.Add(_headerName, _credential);
break;
case AuthenticationType.Basic:
request.Headers.Authorization =
new AuthenticationHeaderValue("Basic", _credential);
break;
case AuthenticationType.None:
default:
break;
}
return base.SendAsync(request, cancellationToken);
}
}
Server-Side Authentication in ASP.NET Core
This section covers implementing authentication in your own REST APIs using modern .NET 9 patterns.
API Key Authentication (Server-Side)
Modern Implementation
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using System.Security.Claims;
using System.Text.Encodings.Web;
/// <summary>
/// Authentication handler for API key validation.
/// </summary>
public sealed class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
private const string ApiKeyHeaderName = "X-API-Key";
private readonly IApiKeyValidator _validator;
public ApiKeyAuthenticationHandler(
IOptionsMonitor<ApiKeyAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
IApiKeyValidator validator)
: base(options, logger, encoder)
{
_validator = validator;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.TryGetValue(ApiKeyHeaderName, out Microsoft.Extensions.Primitives.StringValues apiKeyValues))
{
return AuthenticateResult.Fail("API Key header is missing");
}
string? apiKey = apiKeyValues.FirstOrDefault();
if (string.IsNullOrWhiteSpace(apiKey))
{
return AuthenticateResult.Fail("API Key is empty");
}
ApiKeyValidationResult result = await _validator.ValidateAsync(apiKey);
if (!result.IsValid)
{
return AuthenticateResult.Fail("Invalid API Key");
}
Claim[] claims =
[
new Claim(ClaimTypes.NameIdentifier, result.ClientId),
new Claim(ClaimTypes.Name, result.ClientName),
new Claim("api_key_scope", string.Join(",", result.Scopes))
];
ClaimsIdentity identity = new(claims, Scheme.Name);
ClaimsPrincipal principal = new(identity);
AuthenticationTicket ticket = new(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
/// <summary>
/// Options for API key authentication.
/// </summary>
public sealed class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions { }
/// <summary>
/// API key validation service interface.
/// </summary>
public interface IApiKeyValidator
{
Task<ApiKeyValidationResult> ValidateAsync(string apiKey);
}
/// <summary>
/// Result of API key validation.
/// </summary>
public sealed record ApiKeyValidationResult(
bool IsValid,
string ClientId,
string ClientName,
IReadOnlyList<string> Scopes);
Registration
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IApiKeyValidator, DatabaseApiKeyValidator>();
builder.Services.AddAuthentication("ApiKey")
.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>("ApiKey", null);
builder.Services.AddAuthorization();
WebApplication app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/api/data", () => Results.Ok(new { message = "Authenticated!" }))
.RequireAuthorization();
app.Run();
- Simple to implement and understand
- Low overhead - no complex token processing
- Stateless - no server-side sessions
- Works with any HTTP client
- No built-in expiration mechanism
- Coarse-grained access control
- Complete compromise if leaked
JWT Bearer Token Authentication (Server-Side)
JWT Structure
A JWT consists of three Base64-encoded parts: Header.Payload.Signature
Implementation in ASP.NET Core
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
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:Key"]!)),
ClockSkew = TimeSpan.FromMinutes(1)
};
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception is SecurityTokenExpiredException)
{
context.Response.Headers.Append("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ReadAccess", policy => policy.RequireClaim("permission", "read"));
options.AddPolicy("AdminOnly", policy => policy.RequireRole("admin"));
});
WebApplication app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/api/data", () => "Protected data").RequireAuthorization();
app.MapGet("/api/admin", () => "Admin only").RequireAuthorization("AdminOnly");
app.Run();
External Identity Providers (OpenID Connect)
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
// Azure AD / Entra ID
options.Authority = "https://login.microsoftonline.com/{tenant-id}/v2.0";
options.Audience = builder.Configuration["AzureAd:ClientId"];
// OR Auth0
// options.Authority = "https://{domain}.auth0.com/";
// options.Audience = builder.Configuration["Auth0:ApiIdentifier"];
});
- Stateless - no server-side sessions
- Scalable across distributed systems
- Fine-grained authorization via claims
- Standardized (RFC 7519)
- Token size increases request payload
- Cannot revoke before expiration
- Requires proper key management
Resources
- Configure JWT bearer authentication
- JWT.io - Token decoder and verifier
Client Certificate Authentication (Server-Side)
ASP.NET Core Configuration
using Microsoft.AspNetCore.Authentication.Certificate;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(https =>
{
https.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
https.CheckCertificateRevocation = true;
});
});
builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.AllowedCertificateTypes = CertificateTypes.Chained;
options.RevocationMode = X509RevocationMode.Online;
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
ICertificateValidationService validationService =
context.HttpContext.RequestServices
.GetRequiredService<ICertificateValidationService>();
if (validationService.ValidateCertificate(context.ClientCertificate))
{
Claim[] claims =
[
new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Thumbprint),
new Claim(ClaimTypes.Name,
context.ClientCertificate.GetNameInfo(X509NameType.SimpleName, false) ?? "Unknown")
];
context.Principal = new ClaimsPrincipal(
new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
}
else
{
context.Fail("Certificate validation failed");
}
return Task.CompletedTask;
}
};
});
builder.Services.AddSingleton<ICertificateValidationService, CertificateValidationService>();
WebApplication app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/api/secure", () => Results.Ok(new { message = "mTLS authenticated!" }))
.RequireAuthorization();
app.Run();
- Highest security level
- Mutual authentication
- No secrets in transit
- Integrates with enterprise PKI
- Complex certificate lifecycle
- Requires CA infrastructure
- Not for public consumer APIs
Resources
Transport Layer Security (HTTPS)
HTTPS is fundamental to API security, providing encryption, data integrity, and server authentication. All modern REST APIs should use HTTPS exclusively—there’s no legitimate reason to offer an unencrypted API endpoint in production environments.
Common HTTPS Misconceptions
A critical security consideration: query parameters in URLs are encrypted during transit with HTTPS, but they have security weaknesses:
- They may be logged by servers, proxies, and browser history
- They appear in Referer headers when linking to external sites
- They’re visible in bookmarks and shared URLs
- They have length limitations that can truncate data
Secure Data Transmission Guidelines
Best practice for authentication credentials:
Always send authentication credentials in headers, not query parameters or URLs.
GET /articles HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Or for API key authentication:
GET /articles HTTP/1.1
X-API-Key: a8asd90a8sd90asd09asd09as0d9...
For operations that require sending sensitive data:
POST /authentication/login HTTP/1.1
Content-Type: application/json
{
"username": "johndoe",
"password": "s3cureP@ssw0rd!"
}
Never transfer sensitive data in query parameters, regardless of HTTPS!
HTTPS Implementation in ASP.NET Core
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// Configure HTTPS requirements
builder.Services.AddHttpsRedirection(options =>
{
options.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect;
options.HttpsPort = 443;
});
// Force HTTPS in production with HSTS
if (!builder.Environment.IsDevelopment())
{
builder.Services.AddHsts(options =>
{
options.Preload = true;
options.IncludeSubDomains = true;
options.MaxAge = TimeSpan.FromDays(365);
});
}
WebApplication app = builder.Build();
// Apply HTTPS redirection and HSTS middleware
if (!app.Environment.IsDevelopment())
{
app.UseHsts();
}
app.UseHttpsRedirection();
Additional Security Considerations
HMAC Authentication
Hash-based Message Authentication Code (HMAC) authentication provides request validation and tampering prevention. The client creates a hash of the request components using a shared secret key. The server independently computes the same hash to verify the request’s authenticity.
using System.Security.Cryptography;
using System.Text;
/// <summary>
/// Generates HMAC signatures for request authentication.
/// </summary>
public static class HmacSignatureGenerator
{
/// <summary>
/// Generates an HMAC-SHA256 signature for the request.
/// </summary>
public static (string Signature, string Timestamp) GenerateHmacSignature(
string requestUri,
string httpMethod,
string body,
string secretKey)
{
string timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
string message = $"{httpMethod}{requestUri}{timestamp}{body}";
using HMACSHA256 hmac = new(Encoding.UTF8.GetBytes(secretKey));
byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
string signature = Convert.ToBase64String(hash);
return (signature, timestamp);
}
}
// Usage with HttpClient
HttpClient client = new();
(string signature, string timestamp) = HmacSignatureGenerator.GenerateHmacSignature(
"/api/resource", "GET", "", "yourSecretKey");
client.DefaultRequestHeaders.Add("X-Signature", signature);
client.DefaultRequestHeaders.Add("X-Timestamp", timestamp);
Rate Limiting and Throttling
Protect your API from abuse and denial-of-service attacks with rate limiting:
using System.Threading.RateLimiting;
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
{
// Identify clients by API key or IP address
string clientId = context.Request.Headers["X-API-Key"].FirstOrDefault() ??
context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
return RateLimitPartition.GetFixedWindowLimiter(clientId, _ =>
new FixedWindowRateLimiterOptions
{
Window = TimeSpan.FromMinutes(1),
PermitLimit = 100,
QueueLimit = 0
});
});
options.OnRejected = async (context, cancellationToken) =>
{
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out TimeSpan retryAfter))
{
context.HttpContext.Response.Headers.RetryAfter =
((int)retryAfter.TotalSeconds).ToString();
}
await context.HttpContext.Response.WriteAsync(
"Too many requests. Please try again later.", cancellationToken);
};
});
WebApplication app = builder.Build();
app.UseRateLimiter();
Content Security Policies
Add security headers to protect against various attacks:
WebApplication app = builder.Build();
app.Use(async (context, next) =>
{
IHeaderDictionary headers = context.Response.Headers;
headers.Append("X-Content-Type-Options", "nosniff");
headers.Append("X-Frame-Options", "DENY");
headers.Append("Content-Security-Policy",
"default-src 'self'; script-src 'self'; object-src 'none'");
headers.Append("X-XSS-Protection", "1; mode=block");
headers.Append("Referrer-Policy", "strict-origin-when-cross-origin");
headers.Append("Permissions-Policy",
"accelerometer=(), camera=(), geolocation=(), microphone=()");
await next();
});
Authentication Method Comparison
| Method | Security Level | Complexity | Best For |
|---|---|---|---|
| API Key | Medium | Low | Internal APIs, Service-to-Service |
| Bearer/JWT | High | Medium | User-facing APIs, Microservices |
| Client Certificate | Highest | High | B2B, Financial Services |
| HMAC | High | Medium | Webhook verification, S3-style APIs |
Recommended Security Best Practices
Defense in Depth: Use multiple security mechanisms rather than relying on a single approach.
Validate All Input: Never trust client input and validate it server-side against strict schemas.
Use Appropriate Authentication: Choose the right authentication mechanism for your API’s context.
Implement Proper Authorization: Validate permissions after authentication using role-based or attribute-based access control.
Apply the Principle of Least Privilege: Grant only the minimum permissions necessary.
Keep Dependencies Updated: Regularly update frameworks and libraries to patch security vulnerabilities.
Use Security Headers: Implement HTTP security headers to prevent common web vulnerabilities.
Implement Rate Limiting: Prevent abuse through request throttling and rate limits.
Log Security Events: Maintain comprehensive audit logs for security-relevant operations.
Regular Security Testing: Conduct penetration tests and security reviews of your API.