Secure By Design: Building Secure APIs from Day One

Learn how to design, build, and secure your APIs with best practices in authentication, rate limiting, validation, and more. Part of TechWayFit's Secure By Design series.

Secure By Design: Building Secure APIs from Day One

APIs are the backbone of modern applications. As more systems expose their functionality through APIs, attackers increasingly exploit poorly secured endpoints. In this guide, we explore how to build secure APIs using real-world practices and ASP.NET Core examples.

🔐 Why API Security Matters

Incident Vulnerability Description Impact on Organization
Facebook (2018) Access Token Exposure A flaw in the “View As” feature allowed attackers to steal session tokens via the video uploader component. These tokens could be used to impersonate users without logging in. Exposed ~50 million accounts; triggered global scrutiny; regulatory investigations in the EU and US; significantly damaged user trust and led to tightened platform security policies.
Venmo Unprotected Public API The API allowed access to public transaction feeds without rate limiting or authentication, enabling mass scraping of users’ payment activity, including sender, receiver, and notes. Raised serious privacy concerns; journalists demonstrated large-scale scraping; Venmo updated its settings interface but delayed implementing stronger technical controls.
Panera Bread Unauthenticated Data Access Customer information—including names, emails, phone numbers, and birthdays—was accessible via an unprotected API endpoint without any form of authentication or authorization. Estimated exposure of over 37 million records; delayed incident response worsened reputational damage; served as a key industry example of failure in basic API hygiene and disclosure practices.

🛡️ Common API Vulnerabilities

APIs are often targeted due to their accessibility and the sensitive data they handle. Here are some common vulnerabilities:

1. Excessive Data Exposure

APIs sometimes return more data than necessary. Attackers can exploit this to access sensitive information. This is a common pitfall in API development where an endpoint returns entire objects—like full user profiles—instead of just the data actually needed by the client.

// 🛑 Insecure: Returns entire user object
        return Ok(userService.GetUser(id));

In the insecure code snippet, the API returns the full User object directly from the service layer. This might seem convenient, but it risks exposing sensitive fields like passwords, internal IDs, or metadata unintentionally.

// ✅ Secure: Return only required fields via DTO
        return Ok(new UserDto { Name = user.Name, Email = user.Email });

This pattern explicitly controls what gets serialized and returned, typically through a lightweight class like UserDto that holds only the fields the UI requires. Not only does this minimize risk, but it keeps your response payloads lean and purpose-driven. In essence, it’s about being intentional with data exposure—show only what the client truly needs, and nothing more. This principle not only enhances security but also helps reduce payload size and improves maintainability.

2. Broken Object-Level Authorization (BOLA)

APIs often expose object IDs (like user or order IDs) without validating ownership, allowing attackers to access others' data. This is one of the most common and dangerous API flaws: Broken Object-Level Authorization (BOLA). It happens when APIs expose object identifiers—like orderId, userId, or invoiceId , but fail to confirm whether the requester is authorized to access that object.

// 🛑 Insecure: No ownership check
            [HttpGet("/orders/{id}")]
            public IActionResult GetOrder(int id) => Ok(orderService.GetById(id));

The API simply retrieves and returns the order object without verifying if the current user owns it. That means an attacker could manipulate the URL (e.g., /orders/12345) and potentially access someone else's data—a textbook case of insecure direct object reference (IDOR).

// ✅ Secure: Check if user owns the order
            [HttpGet("/orders/{id}")]
            public IActionResult GetOrder(int id)
            {
                var order = orderService.GetById(id);
                if (order.UserId != GetCurrentUserId()) return Forbid();
                return Ok(order);
            }

Now, unless the UserId on the order matches the ID of the logged-in user, access is denied. This is exactly the kind of defensive pattern the Secure By Design guide emphasizes.

You’ll often find BOLA risks in APIs that expose predictable ID patterns without authentication gates—like /users/3 or /invoices/5. By combining predictable routing with missing access controls, attackers don’t even need complex exploits; they just enumerate IDs.

3. Lack of Input Validation

Without input validation, APIs are prone to injection attacks, such as SQL injection or XSS. This is a foundational API security principle: never trust user input. When an API accepts unvalidated or unsanitized input—like a search query or form field—it opens the door to injection attacks. These might include:

  • SQL injection, where attackers inject SQL commands to manipulate the database (e.g., '; DROP TABLE Users;--)
  • Cross-Site Scripting (XSS), where scripts are injected to run in browsers, potentially stealing user data or tokens

// 🛑 Insecure: Blindly accepts input
public IActionResult Search(string query) => db.Search(query);

In this insecure example, the API directly uses the query parameter in a database search without validating or sanitizing it. An attacker could pass a malicious string that executes harmful SQL commands or scripts.

// ✅ Secure: Validate and sanitize input
public IActionResult Search([FromQuery] string query)
{
    if (string.IsNullOrWhiteSpace(query)) return BadRequest();
    return Ok(db.Search(Sanitize(query)));
}

In the secure version, the API checks

  • string.IsNullOrWhiteSpace(query) ensures that only meaningful input proceeds
  • Sanitize(query) is a method that cleans the input, removing or escaping potentially dangerous characters

Always validate input against a schema or set of rules, and use libraries that automatically handle sanitization for you. This is a core principle of Secure By Design—never trust data from outside your system.

4. No Rate Limiting or Abuse Protection

Without rate limiting, attackers can brute-force or flood your APIs with requests, leading to denial-of-service (DoS). This is a classic availability threat: APIs without rate limiting or abuse protection are like open doors with no bouncer—anyone can walk in, and in large enough numbers, they can knock the entire system offline.

When you don’t set limits, attackers can:

  • Brute-force login or auth endpoints (e.g. guessing passwords or tokens at scale)
  • Flood your API with requests, overwhelming backend services—an easy route to a denial-of-service (DoS)
To prevent that, we can follow two practical defenses:

✅ 1. Rate Limiting Middleware

// Use ASP.NET Core Rate Limiting middleware
app.UseRateLimiter(new RateLimiterOptions {
    PermitLimit = 100,
    QueueLimit = 10
});
    

✅ 2.API Gateway Enforcement:

It also recommends external enforcement through gateways like Azure API Management or AWS API Gateway, where you can:
  • Set usage quotas per user or IP
  • Define burst limits to absorb spikes safely
  • Block suspicious IP ranges

Together, these strategies help insulate your APIs from both targeted attacks and accidental overloads—critical for uptime, scalability, and basic fairness among clients.

🧪 Design-Time Security Principles

These guidelines align with the page’s broader philosophy: secure your APIs by design, not by reacting to breaches later.
  • 1. Minimize surface area: Expose only the endpoints that are absolutely necessary. Every extra endpoint increases your attack surface. For example, avoid generic "get all" endpoints unless truly needed.
  • 2. Validate all input at the edge: Validate and sanitize all incoming data as soon as it enters your system—ideally at the API gateway or controller level. This blocks common attacks like SQL injection and XSS.
  • 3. Don’t expose internal identifiers: Use GUIDs or slugs instead of sequential IDs. This prevents attackers from easily guessing valid object references, reducing risks like BOLA (Broken Object-Level Authorization).
  • 4. Principle of least privilege: Grant each API client or user only the permissions they need—nothing more. Use role-based or claims-based authorization to enforce this.
  • 5. Fail securely: When errors occur, return generic error messages and avoid leaking stack traces or sensitive details in API responses.
  • 6. Secure defaults: APIs should be secure by default—require authentication, use HTTPS, and deny access unless explicitly allowed.

🚦 Authentication & Authorization

Why It Matters

The surrounding content presents real-world failures (like Facebook’s token leak or Panera’s exposed customer data) as a warning. Without strong auth controls, even well-coded APIs can become gaping vulnerabilities.

What It’s Saying

Use OAuth2 and Token-Based Access

The guide recommends modern, token-based mechanisms (like OAuth2 or JWT) rather than session-based or cookie authentication. These tokens include defined scopes (what the token can access) and lifetimes (when it expires), limiting potential misuse.

Compare the Two Code Samples:

🔓 Insecure:

[HttpGet("/orders")]
public IActionResult GetOrders() => Ok(orderService.GetAll());
  

This endpoint allows anyone to hit /orders—no login, no access control. An attacker or script could grab every order with no friction.

🔐 Secure:

[Authorize(Roles = "Admin")]
[HttpGet("/orders")]
public IActionResult GetOrders() => Ok(orderService.GetAll());
  

This version requires the caller to be authenticated and have an "Admin" role. That’s role-based access control (RBAC) in action—only users with the right authority can access sensitive functions.

This example fits neatly into the page’s broader approach: lock down APIs by default, exposing only what’s needed, to who truly needs it.

🧰 Input Validation & Output Encoding

Why This Matters

Without input validation, APIs are vulnerable to classic attacks like SQL injection, XSS, or even simple data corruption. The guide references real-world examples to emphasize that blind trust in user data is a major risk.

What’s Happening in the Code

🛑 Insecure Version


public IActionResult CreateUser(User user) => userService.Save(user);
  

This implementation accepts user input as-is and immediately passes it to the business logic layer. There’s no validation, allowing malformed or malicious data to slip through unchecked.

✅ Secure Version


public class UserDto
{
    [Required]
    [StringLength(50)]
    public string Name { get; set; }

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

    [Range(18, 100)]
    public int Age { get; set; }
}

public IActionResult CreateUser([FromBody] UserDto userDto)
{
    if (!ModelState.IsValid) return BadRequest("Invalid data");
    return Ok(userService.Save(userDto));
}
  

This version:

  • Defines explicit rules for Name, Email, and Age using attributes like [Required] and [EmailAddress].
  • Validates incoming data via ModelState.IsValid before processing it.
  • Rejects bad input early with a 400 Bad Request response.

This aligns with the guide’s emphasis on treating all input as untrusted and validating it at the boundary. It also ensures downstream components receive well-formed data.

Rate Limiting & Abuse Protection

Why It Matters

Without rate limiting, even legitimate endpoints can become dangerous. The page highlights risks such as brute-force login attempts, bot-driven scraping, or flooding APIs to trigger denial-of-service (DoS) conditions. To prevent this, limiting access by volume and frequency is critical.

What’s the Code Doing?


app.UseRateLimiter(new RateLimiterOptions {
    PermitLimit = 100,  // max 100 requests per minute
    QueueLimit = 10     // buffer 10 before rejecting
});
  

This middleware enforces throttling by:

  • Capping request volume (100 per minute in this case)
  • Queuing minor bursts (up to 10 extra requests)
  • Denying overflow once the buffer is full

Recommended Enhancements

The guide suggests pairing middleware with API gateway rules for a more robust shield. Example configuration with Azure API Management:



<rate-limit-by-key calls="100" renewal-period="60">
  <key value="@(context.Request.IpAddress)" />
</rate-limit-by-key>
  

This enforces a per-IP limit of 100 calls per minute. You can also scope by client ID, subscription key, or token claims. Benefits:

  • Prevents overuse before reaching backend services
  • Supports custom rate strategies per API or operation
  • Enables real-time abuse mitigation and analytics

Together, these form a layered defense—middleware handles local enforcement, while the API gateway manages edge-level control. The page calls this a "Secure by Design" approach: defending APIs before threats manifest.

Logging & Monitoring

Why It Matters

Logging is a critical part of API observability and security. The guide highlights how the absence of meaningful logs leads to blind spots during incidents, while poor logging hygiene can expose sensitive data. Striking the right balance is key.

Best Practices from the Guide

  • Use structured logging tools like Serilog or Seq to produce searchable, consistent log output.
  • Attach correlation IDs to every request to trace activity across services.
  • Never log PII, passwords, or secrets—logs should help you investigate issues, not create new security holes.

Code Sample: Secure Logging in ASP.NET Core (Serilog)


// In Program.cs
Log.Logger = new LoggerConfiguration()
    .Enrich.FromLogContext()
    .Enrich.WithProperty("Application", "SecureApi")
    .WriteTo.Console()
    .WriteTo.Seq("http://localhost:5341")
    .CreateLogger();

builder.Host.UseSerilog();

// Inside a controller
[HttpPost("/login")]
public IActionResult Login(LoginRequest req)
{
    logger.LogInformation("Login attempt for user {Username} with RequestId {RequestId}",
        req.Username,
        HttpContext.TraceIdentifier); // Do NOT log passwords or tokens!

    var result = authService.AttemptLogin(req);
    if (!result.Success)
    {
        logger.LogWarning("Failed login for user {Username}", req.Username);
        return Unauthorized();
    }

    return Ok(result.Token);
}
  

This example shows how to:

  • Use structured logs with enriched metadata
  • Log meaningful user actions and trace IDs
  • Completely avoid logging sensitive payloads like passwords

The page concludes that smart logging is part of securing APIs “by design”—collect what you need to detect and respond, never more.

📊 Secure API Versioning

Why This Matters

As APIs grow, it's natural to roll out v2, v3, and beyond. But older versions often remain in use by internal tools or clients. The guide warns that unmonitored legacy APIs become prime targets for attackers—especially if security patches or controls aren’t maintained.

Key Recommendations

  • Protect older versions until deprecated safely: Apply auth, rate limiting, and logging to all supported versions.
  • Avoid breaking changes: Design updates to be backward-compatible and gentle to adopt.
  • Patch across versions: Backport critical fixes to every active version to prevent patch gaps.

Sample Compatibility & Deprecation Matrix

Version Status Auth Rate Limit Logging Security Patching Deprecation Timeline
v1 Legacy ✅ ✅ Limited ✅ Backported Phase-out: Q4 2025
v2 Active ✅ ✅ ✅ ✅ Sunset planned Q2 2026
v3 Current ✅ ✅ ✅ + Telemetry ✅ N/A

📝 Tools for API Security Testing

Why This Matters

Building secure APIs is just the start—testing them regularly ensures your defenses actually work. The guide encourages adopting attacker-mindset tools to proactively uncover vulnerabilities before they’re exploited.

Recommended Tools

  • OWASP ZAP: A free and powerful scanner that detects API vulnerabilities like injection flaws, broken authentication, and misconfigured headers. Ideal for integration in CI/CD pipelines or staging environments.
  • Postman Security Scans: Built into Postman, these scans test API collections for misconfigured security, overly permissive responses, or missing headers—all within a familiar dev workflow.
  • Swagger/OpenAPI Security Annotations: Embed auth schemes directly into your API specification to guide tools and gateways. For example:

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

security:
  - bearerAuth: []
  

This not only documents your API’s requirements clearly but also allows automated tools to simulate authenticated requests, strengthening your test coverage and onboarding experience.

📌 Conclusion

🔒 Securing APIs is not optional—it’s foundational. From validating inputs to enforcing strong authentication and rate limiting, API security must be considered from design through deployment. Secure your APIs by default, not by patching after a breach.

  • ✅ Validate all inputs — Never trust client data without validation and sanitization.
  • ✅ Enforce authentication & authorization — Use OAuth2, scoped tokens, and role checks.
  • ✅ Apply rate limiting — Throttle traffic via middleware and API gateways to prevent abuse.
  • ✅ Log securely — Use structured logging with request IDs, and never log PII or secrets.
  • ✅ Protect all versions — Maintain security across API versions until they’re properly deprecated.
  • 🧪 Test proactively — Use tools like OWASP ZAP, Postman, and OpenAPI annotations to catch flaws early.

Built securely from design to deployment. Don't wait for a breach—lock it down by default.