Isaac.

Securing Your Web API

Learn how to protect your web APIs from threats using authentication, authorization, encryption, and best practices.

By EMEPublished: February 20, 2025
securityapiauthenticationauthorizationencryptionoauthjwthttps

A Simple Analogy

Imagine a bank with different security layers:

  • HTTPS lock: The front door has a strong lock (encryption)
  • ID check: Guard verifies who you are (authentication)
  • Permission check: Guard confirms what you're allowed to do (authorization)
  • Rate limiting: Bouncer limits how many people enter per minute (DDoS protection)
  • Audit trail: Cameras record everything for later review (logging)

A secure API has all these layers working together.


What Does "Securing an API" Mean?

Securing an API means protecting it from unauthorized access, data breaches, attacks, and misuse. It involves multiple layers: encryption, authentication, authorization, input validation, and monitoring.


Why Is API Security Critical?

  • Data protection: Prevent theft of sensitive user data
  • Compliance: Meet legal requirements (GDPR, HIPAA, PCI-DSS)
  • Trust: Users trust you with their data
  • Availability: Prevent attacks that take your API offline
  • Brand reputation: A breach damages your credibility
  • Financial: Breaches cost millions in fines and recovery

Core Security Concepts

1. Authentication (Who Are You?)

Authentication verifies the identity of a client making a request.

Common Methods:

Username/Password (Basic Auth)

// Simplest form (never use over HTTP!)
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    [HttpPost("login")]
    public IActionResult Login(string username, string password)
    {
        if (VerifyCredentials(username, password))
        {
            return Ok(new { token = GenerateToken(username) });
        }
        return Unauthorized();
    }
}

JWT (JSON Web Tokens)

// Industry standard for APIs
[HttpPost("login")]
public IActionResult Login(LoginRequest request)
{
    if (ValidateUser(request.Username, request.Password))
    {
        var token = new JwtSecurityTokenHandler().WriteToken(
            new JwtSecurityToken(
                issuer: "myapp",
                audience: "myapp-users",
                claims: new[] { new Claim(ClaimTypes.Name, request.Username) },
                expires: DateTime.UtcNow.AddHours(1),
                signingCredentials: new SigningCredentials(
                    new SymmetricSecurityKey(Encoding.UTF8.GetBytes("my-secret-key")),
                    SecurityAlgorithms.HmacSha256)
            ));
        return Ok(new { token });
    }
    return Unauthorized();
}

OAuth 2.0

// Delegate to identity provider (Google, Microsoft, GitHub)
builder.Services.AddAuthentication()
    .AddGoogle(options =>
    {
        options.ClientId = "your-google-client-id";
        options.ClientSecret = "your-google-client-secret";
    });

2. Authorization (What Can You Do?)

Authorization determines what authenticated users are allowed to access.

[ApiController]
[Route("api/[controller]")]
public class AdminController : ControllerBase
{
    [HttpDelete("{id}")]
    [Authorize(Roles = "Admin")]  // Only admins can delete
    public IActionResult DeleteUser(int id)
    {
        _userService.DeleteUser(id);
        return NoContent();
    }

    [HttpGet("{id}")]
    [Authorize]  // Any authenticated user can view
    public IActionResult GetUser(int id)
    {
        return Ok(_userService.GetUser(id));
    }

    [HttpGet("public")]
    // No [Authorize] - anyone can access
    public IActionResult GetPublicData()
    {
        return Ok(new { message = "Public data" });
    }
}

3. Encryption (Protect Data in Transit)

Always use HTTPS to encrypt data between client and server.

// Configure HTTPS requirement
builder.Services.AddHsts(options =>
{
    options.MaxAge = TimeSpan.FromDays(365);
    options.IncludeSubDomains = true;
    options.Preload = true;
});

var app = builder.Build();
app.UseHttpsRedirection();  // Force HTTPS

API Security Best Practices

1. Use HTTPS (TLS/SSL) Always

// Never send sensitive data over HTTP
// ✓ CORRECT
https://api.example.com/users/login

// ✗ WRONG
http://api.example.com/users/login

2. Input Validation (Prevent Injection)

public class CreateUserRequest
{
    [Required]
    [StringLength(100)]
    public string Name { get; set; }

    [Required]
    [EmailAddress]
    public string Email { get; set; }

    [Required]
    [StringLength(50, MinimumLength = 8)]
    public string Password { get; set; }
}

[HttpPost]
public IActionResult CreateUser([FromBody] CreateUserRequest request)
{
    if (!ModelState.IsValid)
        return BadRequest(ModelState);

    // Validated data only reaches here
    var user = _userService.CreateUser(request);
    return Created(nameof(GetUser), user);
}

3. Rate Limiting (Prevent Brute Force & DDoS)

// Limit requests per IP
builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(
        context => RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: context.Request.Headers["X-Forwarded-For"].ToString(),
            factory: partition => new FixedWindowRateLimiterOptions
            {
                AutoReplenishment = true,
                PermitLimit = 100,
                Window = TimeSpan.FromMinutes(1)
            }));
});

app.UseRateLimiter();

4. CORS (Control Who Can Call Your API)

// Only allow specific origins
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowFrontend", policy =>
    {
        policy
            .WithOrigins("https://app.example.com")
            .AllowAnyMethod()
            .AllowAnyHeader();
    });
});

app.UseCors("AllowFrontend");

5. Secrets Management (Don't Hardcode Keys)

// ✗ WRONG - Hardcoded secret
var apiKey = "sk-1234567890abcdef";

// ✓ CORRECT - From environment or secrets manager
var apiKey = builder.Configuration["ApiKey"];

// ✓ BETTER - Azure Key Vault in production
var keyVaultUrl = new Uri("https://myvault.vault.azure.net/");
builder.Configuration.AddAzureKeyVault(
    keyVaultUrl,
    new DefaultAzureCredential());

6. Logging & Monitoring

public class ApiSecurityMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ApiSecurityMiddleware> _logger;

    public async Task InvokeAsync(HttpContext context)
    {
        var startTime = DateTime.UtcNow;
        
        await _next(context);

        // Log all API calls with security context
        _logger.LogInformation(
            "API Call: {Method} {Path} | User: {User} | Status: {Status} | Duration: {Duration}ms",
            context.Request.Method,
            context.Request.Path,
            context.User.Identity?.Name ?? "Anonymous",
            context.Response.StatusCode,
            (DateTime.UtcNow - startTime).TotalMilliseconds);
    }
}

app.UseMiddleware<ApiSecurityMiddleware>();

7. CSRF Protection (For Form Submission APIs)

// Protect POST/PUT/DELETE endpoints
[ValidateAntiForgeryToken]
[HttpPost]
public IActionResult CreateUser(UserDto dto)
{
    // CSRF token is validated automatically
    var user = _userService.CreateUser(dto);
    return Created(nameof(GetUser), user);
}

8. API Key Management

// Check API key on protected endpoints
[HttpGet("admin/stats")]
public IActionResult GetStats([FromHeader(Name = "X-API-Key")] string apiKey)
{
    if (!ValidateApiKey(apiKey))
        return Unauthorized();

    return Ok(_statsService.GetStats());
}

Practical Security Checklist

□ HTTPS enabled (all endpoints)
□ Authentication implemented (JWT/OAuth)
□ Authorization rules defined (roles, policies)
□ Input validation on all endpoints
□ Rate limiting enabled
□ CORS configured properly
□ Secrets stored securely (not in code)
□ Logging and monitoring active
□ Dependencies updated regularly
□ Security headers set (HSTS, CSP, X-Frame-Options)
□ Error messages don't leak info (generic 500 errors)
□ Database passwords hashed (bcrypt, Argon2)
□ SQL injection prevented (parameterized queries)
□ XSS prevented (output encoding)
□ CSRF protection enabled

Real-World Attack Scenarios & Prevention

Scenario 1: Brute Force Login Attack

Attack: Attacker tries 1000 passwords per second
Prevention: Rate limiting + account lockout

if (failedAttempts > 5)
{
    account.LockUntil = DateTime.UtcNow.AddMinutes(15);
}

Scenario 2: SQL Injection

// ✗ VULNERABLE
var user = context.Users
    .FromSqlInterpolated($"SELECT * FROM Users WHERE Email = '{email}'");

// ✓ SAFE - Parameterized
var user = context.Users
    .FromSqlInterpolated($"SELECT * FROM Users WHERE Email = {email}");

Scenario 3: Data Breach via API

Attack: Attacker gets another user's data
Prevention: Authorization checks

public User GetUser(int userId)
{
    var currentUser = User.FindFirst(ClaimTypes.NameIdentifier);
    
    if (userId != int.Parse(currentUser.Value) && !User.IsInRole("Admin"))
        throw new UnauthorizedException();
    
    return _userService.GetUser(userId);
}

Related Concepts to Explore

  • OAuth 2.0 and OpenID Connect
  • SAML (enterprise single sign-on)
  • Certificate pinning
  • Web Application Firewall (WAF)
  • API gateways and security
  • Encryption at rest (database encryption)
  • Key rotation strategies
  • Vulnerability scanning
  • Penetration testing
  • Security headers (HSTS, CSP, X-Content-Type-Options)
  • API versioning for security updates
  • Zero-trust security model
  • Dependency scanning and updates

Summary

Securing your web API requires multiple layers: HTTPS encryption, strong authentication (JWT/OAuth), precise authorization, input validation, rate limiting, and continuous monitoring. No single technique is sufficient—you need defense in depth. By following these practices and maintaining awareness of evolving threats, you can build APIs that users trust and that withstand attacks. Security isn't a feature; it's a fundamental responsibility.