Securing REST APIs

Best practices for securing REST APIs in .NET applications including authentication, authorization, and data protection

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 the primary authentication methods for REST APIs, security best practices, and implementation approaches in .NET.

Authentication Methods Comparison

MethodComplexitySecurity LevelScalabilityUse CasesKey StrengthsKey Weaknesses
API KeyLowModerateModeratePublic APIs, Simple servicesEasy implementation, Low overheadNo built-in expiration, Limited authorization granularity
JWT/TokenModerateHighHighMost web/mobile apps, MicroservicesFine-grained permissions, Decoupled authenticationMore complex implementation, Key management challenges
OAuth 2.0HighVery HighHighThird-party integrations, Enterprise appsDelegated authorization, No password sharingImplementation complexity, More moving parts
CertificateModerateVery HighModerateServer-to-server, Backend systemsStrong security, Mutual authenticationCertificate management overhead, Client setup complexity
HMACModerateHighModerateAPIs with data integrity concernsRequest tampering preventionImplementation complexity, Clock synchronization issues

API Key-based Authentication

In API key authentication, an individual key is issued to the client, usually a string of alphanumeric characters. The length varies, but typically 32-64 characters provides a good balance between security and usability. The server receives the API key, which is usually located in the HTTP Authorization or a custom HTTP header. The key is then validated against a key store (typically a database). If valid, the server processes the request.

Implementation in ASP.NET Core

// Program.cs or Startup.cs
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = "ApiKey";
    options.DefaultChallengeScheme = "ApiKey";
})
.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>("ApiKey", options => { });

// ApiKeyAuthenticationHandler.cs
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
    private const string ApiKeyHeaderName = "X-API-Key";
    private readonly IApiKeyService _apiKeyService;

    public ApiKeyAuthenticationHandler(
        IOptionsMonitor<ApiKeyAuthenticationOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        ISystemClock clock,
        IApiKeyService apiKeyService) : base(options, logger, encoder, clock)
    {
        _apiKeyService = apiKeyService;
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        if (!Request.Headers.TryGetValue(ApiKeyHeaderName, out var apiKeyHeaderValues))
        {
            return AuthenticateResult.Fail("API Key is missing");
        }

        string apiKey = apiKeyHeaderValues.FirstOrDefault();
        if (string.IsNullOrWhiteSpace(apiKey))
        {
            return AuthenticateResult.Fail("API Key is missing");
        }

        // Validate API key with your service
        ApiKeyValidationResult validationResult = await _apiKeyService.ValidateApiKeyAsync(apiKey);
        if (!validationResult.IsValid)
        {
            return AuthenticateResult.Fail("Invalid API key");
        }

        var claims = new[] {
            new Claim(ClaimTypes.Name, validationResult.ClientId),
            new Claim("ApiKeyScope", string.Join(",", validationResult.Scopes))
        };

        var identity = new ClaimsIdentity(claims, Scheme.Name);
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, Scheme.Name);

        return AuthenticateResult.Success(ticket);
    }
}

Pros and Cons

Advantages

  • Simplicity: Easy to implement and understand
  • Low overhead: No complex token processing or validation
  • Stateless: No need to maintain session state
  • Broad client support: Works with any HTTP client

Disadvantages

  • Security limitations: No built-in expiration mechanism
  • Coarse-grained access control: Limited permission granularity
  • Key rotation challenges: Difficult to rotate keys without service interruption
  • Key exposure risk: Complete key compromise if leaked
  • Lack of standards: No widely accepted standards for implementation

Token-based Authentication

Token-based authentication, particularly using JSON Web Tokens (JWT), has become the industry standard for modern web APIs. With this approach, the client first obtains a token for authentication (the id_token) from an authorization server. The client then uses this token to request an access_token for the specific API they wish to access.

The API trusts the authorization server that issued the token and can validate it without additional database lookups. This creates a decoupled architecture where authentication and resource access are separate concerns.

JWT Token Structure

A JWT token consists of three parts, separated by dots:

  1. Header: Contains metadata about the token type and signing algorithm
  2. Payload: Contains claims (statements about the user and permissions)
  3. Signature: Used to verify the token hasn’t been tampered with
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Implementation in ASP.NET Core

// Program.cs or Startup.cs
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"]))
        };
    });

// Controller usage
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class ResourceController : ControllerBase
{
    [HttpGet]
    public IActionResult Get()
    {
        var currentUser = HttpContext.User;
        string userId = currentUser.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        
        // Access control based on claims
        if (currentUser.HasClaim(c => c.Type == "permission" && c.Value == "read:resource"))
        {
            // Authorized access
            return Ok(new { message = "Protected resource accessed successfully" });
        }
        
        return Forbid();
    }
}

OAuth 2.0 and OpenID Connect

For a complete authentication and authorization solution, OAuth 2.0 and OpenID Connect (OIDC) provide standardized protocols:

  • OAuth 2.0: Authorization framework that enables third-party applications to access user resources
  • OpenID Connect: Authentication layer built on top of OAuth 2.0 that provides identity verification

Pros and Cons

Advantages

  • Statelessness: No server-side session storage required
  • Scalability: Works well in distributed systems and microservices
  • Fine-grained authorization: Detailed permission control through claims
  • Limited lifespan: Tokens expire, reducing risk from compromised tokens
  • Centralized authentication: Identity management separated from resource servers

Disadvantages

  • Implementation complexity: Requires proper understanding of token security
  • Token size: JWTs can become large, increasing request payload size
  • Revocation challenges: Tokens remain valid until expiration unless using additional systems
  • Configuration overhead: Proper setup of issuing authority, signing keys, and validation logic

Resources

Certificate-based Authentication

Certificate-based authentication uses X.509 certificates for client identification. Both the server and client possess certificates, creating a mutual authentication system. When the client connects to the server, a TLS handshake occurs, and the server verifies that the client’s certificate was issued by a trusted Certificate Authority (CA) and matches expected criteria.

This method provides a high level of security and is particularly well-suited for server-to-server communication and internal APIs where certificate management is feasible.

Implementation in ASP.NET Core

// Program.cs or Startup.cs
builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
    .AddCertificate(options =>
    {
        // Basic certificate validation
        options.AllowedCertificateTypes = CertificateTypes.All;
        options.RevocationMode = X509RevocationMode.NoCheck;
        
        // Custom validation logic
        options.Events = new CertificateAuthenticationEvents
        {
            OnCertificateValidated = context =>
            {
                var validationService = context.HttpContext.RequestServices
                    .GetRequiredService<ICertificateValidationService>();
                
                if (validationService.ValidateCertificate(context.ClientCertificate))
                {
                    var claims = new[] 
                    {
                        new Claim(ClaimTypes.NameIdentifier, 
                            context.ClientCertificate.Subject,
                            ClaimValueTypes.String, 
                            context.Options.ClaimsIssuer),
                        new Claim(ClaimTypes.Name, 
                            context.ClientCertificate.GetNameInfo(X509NameType.SimpleName, false),
                            ClaimValueTypes.String, 
                            context.Options.ClaimsIssuer)
                    };

                    context.Principal = new ClaimsPrincipal(
                        new ClaimsIdentity(claims, context.Scheme.Name));
                    context.Success();
                }
                else
                {
                    context.Fail("Invalid certificate");
                }
                
                return Task.CompletedTask;
            }
        };
    });

// Add HTTPS configuration
builder.Services.AddHttpsRedirection(options =>
{
    options.HttpsPort = 443;
    options.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect;
});

// Configure Kestrel to require client certificates
builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(https =>
    {
        https.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
        https.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13;
    });
});

Pros and Cons

Advantages

  • Strong security: High-grade cryptographic authentication
  • Mutual authentication: Both client and server verify each other’s identity
  • No secrets in transit: Authentication doesn’t require sending passwords or keys
  • Difficult to forge: Certificates are cryptographically secured
  • Integrates with enterprise PKI: Works with existing certificate infrastructure

Disadvantages

  • Certificate management: Requires robust processes for issuance, renewal, and revocation
  • Implementation complexity: More complex setup than other authentication methods
  • Client configuration: Requires installation and management of certificates on clients
  • Operational overhead: Maintaining CA infrastructure and certificate lifecycle
  • Limited suitability: Not practical for public-facing services with diverse clients

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:

  1. They may be logged by servers, proxies, and browser history
  2. They appear in Referer headers when linking to external sites
  3. They’re visible in bookmarks and shared URLs
  4. 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

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Configure HTTPS requirements
builder.Services.AddHttpsRedirection(options =>
{
    options.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect;
    options.HttpsPort = 443;
});

// Force HTTPS in production
if (!builder.Environment.IsDevelopment())
{
    builder.Services.AddHsts(options =>
    {
        options.Preload = true;
        options.IncludeSubDomains = true;
        options.MaxAge = TimeSpan.FromDays(365);
    });
}

var 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.

// Client-side HMAC generation
string GenerateHmacSignature(string requestUri, string httpMethod, string body, string apiKey)
{
    string timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
    string message = $"{httpMethod}{requestUri}{timestamp}{body}";
    
    using (HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes(apiKey)))
    {
        byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
        return Convert.ToBase64String(hash);
    }
}

// Usage
string signature = GenerateHmacSignature("/api/resource", "GET", "", "yourSecretKey");
httpClient.DefaultRequestHeaders.Add("X-Signature", signature);
httpClient.DefaultRequestHeaders.Add("X-Timestamp", timestamp);

Rate Limiting and Throttling

Protect your API from abuse and denial-of-service attacks with rate limiting:

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, _) =>
    {
        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        await context.HttpContext.Response.WriteAsync("Too many requests. Please try again later.");
    };
});

Content Security Policies

Add security headers to protect against various attacks:

app.Use(async (context, next) =>
{
    context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
    context.Response.Headers.Add("X-Frame-Options", "DENY");
    context.Response.Headers.Add("Content-Security-Policy", 
        "default-src 'self'; script-src 'self'; object-src 'none'");
    context.Response.Headers.Add("X-XSS-Protection", "1; mode=block");
    context.Response.Headers.Add("Referrer-Policy", "strict-origin-when-cross-origin");
    context.Response.Headers.Add("Permissions-Policy", "accelerometer=(), camera=(), geolocation=(), microphone=()");
    
    await next();
});
  1. Defense in Depth: Use multiple security mechanisms rather than relying on a single approach.

  2. Validate All Input: Never trust client input and validate it server-side against strict schemas.

  3. Use Appropriate Authentication: Choose the right authentication mechanism for your API’s context.

  4. Implement Proper Authorization: Validate permissions after authentication using role-based or attribute-based access control.

  5. Apply the Principle of Least Privilege: Grant only the minimum permissions necessary.

  6. Keep Dependencies Updated: Regularly update frameworks and libraries to patch security vulnerabilities.

  7. Use Security Headers: Implement HTTP security headers to prevent common web vulnerabilities.

  8. Implement Rate Limiting: Prevent abuse through request throttling and rate limits.

  9. Log Security Events: Maintain comprehensive audit logs for security-relevant operations.

  10. Regular Security Testing: Conduct penetration tests and security reviews of your API.