Comprehensive API Security 2026 — OWASP Top 10, JWT Hardening, and Defense in Depth

Posted on: 4/17/2026 7:13:31 PM

1. The API security landscape in 2026

APIs are the backbone of every modern application. From mobile apps to SPAs, from microservices to IoT, everything talks over APIs. But that ubiquity makes APIs the top target for attackers. Security reports consistently show that more than 90% of web applications contain at least one API-related vulnerability, and the damage from API-driven breaches keeps growing.

91% Of web applications have an API vulnerability
681% API traffic growth (2021-2025)
$4.45M Average cost per data breach
74% Of organizations had 3+ API breaches

Why is API security different from traditional web security?

APIs have no UI to "hide" backend logic — attackers interact directly with business logic. An authorization bug in a web form might be limited by the UI flow; the same bug in an API endpoint lets an attacker exploit it at scale via automated scripts. That's why OWASP separated the API Security Top 10 from the Web Application Top 10.

2. OWASP API Security Top 10 — A detailed walkthrough

OWASP API Security Top 10 (2023 edition) is the list of 10 most critical API security risks, updated from the 2019 version with many important changes reflecting new attack realities.

graph TD
    A["OWASP API
Security Top 10
(2023)"] --> B["API1
Broken Object Level
Authorization"] A --> C["API2
Broken
Authentication"] A --> D["API3
Broken Object Property
Level Authorization"] A --> E["API4
Unrestricted Resource
Consumption"] A --> F["API5
Broken Function Level
Authorization"] A --> G["API6
Unrestricted Access to
Sensitive Business Flows"] A --> H["API7
Server Side
Request Forgery"] A --> I["API8
Security
Misconfiguration"] A --> J["API9
Improper Inventory
Management"] A --> K["API10
Unsafe Consumption
of APIs"] style A fill:#e94560,stroke:#fff,color:#fff style B fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style C fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style D fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style E fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style F fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style G fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style H fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style I fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style J fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style K fill:#f8f9fa,stroke:#e94560,color:#2c3e50

The 10 API security risks per OWASP 2023

API1:2023 — Broken Object Level Authorization (BOLA)

Broken Object Level Authorization

CRITICAL

This is the most common and most dangerous vulnerability. It happens when an API doesn't properly check object ownership for the user making the request. An attacker only has to change the object ID in the request to access someone else's data.

Example attack:

// Valid request — user looks up their own order
GET /api/orders/1001
Authorization: Bearer eyJhbGciOi...

// Attacker swaps the ID → reads someone else's order
GET /api/orders/1002
Authorization: Bearer eyJhbGciOi... (same token!)

Prevention in .NET 10:

// Authorization handler that checks ownership
public class OrderOwnerHandler : AuthorizationHandler<OrderOwnerRequirement, Order>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        OrderOwnerRequirement requirement,
        Order order)
    {
        var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;

        if (order.UserId == userId)
            context.Succeed(requirement);

        return Task.CompletedTask;
    }
}

// Use in the controller
[HttpGet("{id}")]
public async Task<IActionResult> GetOrder(int id)
{
    var order = await _repo.GetByIdAsync(id);
    var authResult = await _authService
        .AuthorizeAsync(User, order, "OrderOwner");

    if (!authResult.Succeeded) return Forbid();
    return Ok(order);
}

API2:2023 — Broken Authentication

Broken Authentication

CRITICAL

Weak or missing API authentication lets attackers impersonate others. Common flaws: not validating JWT signatures, accepting expired tokens, unblocked brute force, credential stuffing.

Common mistakes when implementing authentication:

Mistake Consequence Fix
Not validating the JWT alg header Attacker switches to "alg":"none" to bypass the signature Whitelist algorithms; reject none
Weak or hardcoded secret key Brute-force the secret → forge tokens Use RSA/ECDSA; rotate keys regularly
Not checking the exp claim Old tokens remain valid forever Validate expiration; use short-lived tokens
No rate limit on the login endpoint Credential stuffing, password brute-force Rate limiting + account lockout + CAPTCHA
Token in URL query string Token gets logged in server logs, browser history, Referer header Only pass tokens via the Authorization header

API3:2023 — Broken Object Property Level Authorization

Broken Object Property Level Authorization

HIGH

Merged from two older issues: Excessive Data Exposure (API returns too many fields) and Mass Assignment (API accepts unintended fields). This is the result of binding request/response directly to database entities.

// WRONG — returns the whole entity including sensitive fields
[HttpGet("{id}")]
public async Task<User> GetUser(int id)
    => await _db.Users.FindAsync(id);
// Response leaks: PasswordHash, SSN, InternalNotes...

// RIGHT — use a DTO containing only the needed fields
[HttpGet("{id}")]
public async Task<UserDto> GetUser(int id)
{
    var user = await _db.Users.FindAsync(id);
    return new UserDto(user.Id, user.Name, user.Email);
}

// WRONG — Mass Assignment: binding straight to the request body
[HttpPut("{id}")]
public async Task UpdateUser(int id, [FromBody] User user) { ... }
// Attacker sends: {"role": "admin", "isVerified": true}

// RIGHT — use a DTO with allowed fields
[HttpPut("{id}")]
public async Task UpdateUser(int id, [FromBody] UpdateUserDto dto) { ... }
// UpdateUserDto only has: Name, Email, Avatar

API4:2023 — Unrestricted Resource Consumption

APIs without resource limits are fair game for DoS, bill shock (on the cloud), or simply bringing down the system. You need to control: request count, body size, record count returned, processing time, upload size.

// .NET 10 — built-in Rate Limiting middleware
builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("api", opt =>
    {
        opt.PermitLimit = 100;
        opt.Window = TimeSpan.FromMinutes(1);
        opt.QueueLimit = 0;
    });

    options.AddTokenBucketLimiter("upload", opt =>
    {
        opt.TokenLimit = 10;
        opt.ReplenishmentPeriod = TimeSpan.FromSeconds(10);
        opt.TokensPerPeriod = 2;
    });

    options.RejectionStatusCode = 429;
});

// Cap request body size
builder.WebHost.ConfigureKestrel(options =>
{
    options.Limits.MaxRequestBodySize = 10 * 1024 * 1024; // 10MB
});

API5-API10 — Quick summary

Risk Description Main defense
API5: Broken Function Level Authorization Regular users can reach admin endpoints RBAC/ABAC; role checks on every endpoint
API6: Unrestricted Access to Sensitive Business Flows Buyer bots, signup spam, scraping CAPTCHA, device fingerprinting, business-logic rate limits
API7: Server Side Request Forgery (SSRF) API fetches a URL from user input → reaches internal services Domain whitelists, block internal IPs, use allowlists
API8: Security Misconfiguration CORS *, debug mode in prod, default credentials Hardening checklist, automated scanning, IaC
API9: Improper Inventory Management Old APIs, shadow APIs, undocumented endpoints API gateway, OpenAPI spec, deprecation policy
API10: Unsafe Consumption of APIs Trusting data from third-party APIs blindly Validate external API responses; timeouts; circuit breakers

3. JWT Hardening per RFC 8725

JSON Web Token is the most widely-used API authentication method. But JWTs have many ways to be exploited if implemented wrong. RFC 8725 (JSON Web Token Best Current Practices) lays down the key guidelines.

sequenceDiagram
    participant C as Client (Vue.js)
    participant A as Auth Server
    participant API as API Server (.NET)
    participant DB as Database

    C->>A: POST /token (credentials)
    A->>A: Validate credentials
    A->>A: Generate JWT (RS256, short exp)
    A-->>C: Access Token + Refresh Token

    C->>API: GET /api/data
Authorization: Bearer {JWT} API->>API: Validate signature (public key) API->>API: Check exp, iss, aud, nbf API->>API: Extract claims, check permissions API->>DB: Query with user context DB-->>API: Filtered data API-->>C: 200 OK + data Note over C,API: Token expires after 15 minutes C->>A: POST /token/refresh A->>A: Validate refresh token A->>A: Issue new access token A-->>C: New Access Token

A standard JWT auth flow with short-lived access tokens and refresh tokens

JWT hardening checklist

JWT security checklist

  • Algorithm: use RS256 or ES256 (asymmetric). Never use HS256 with a weak secret. Reject "alg":"none".
  • Expiration: access tokens: 15-30 minutes. Refresh tokens: 7-30 days. Always validate the exp claim.
  • Audience (aud): every API must validate that the aud claim matches itself — preventing a service-A token from being used against service B.
  • Issuer (iss): validate the issuer so the token must come from a trusted auth server.
  • Not Before (nbf): set nbf to the issue time to keep tokens from being used before creation.
  • JTI (JWT ID): unique ID per token; add to a blacklist on revocation.
  • Claims: include only necessary information in the payload. NEVER put passwords, sensitive PII, or internal IDs in it.
  • Key rotation: rotate signing keys regularly. Publish public keys via a JWKS endpoint with the kid header.

JWT authentication configuration in .NET 10:

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://auth.example.com";
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = "https://auth.example.com",

            ValidateAudience = true,
            ValidAudience = "https://api.example.com",

            ValidateLifetime = true,
            ClockSkew = TimeSpan.FromSeconds(30),

            ValidateIssuerSigningKey = true,
            ValidAlgorithms = new[] { "RS256", "ES256" },

            RequireExpirationTime = true,
            RequireSignedTokens = true,
        };

        options.Events = new JwtBearerEvents
        {
            OnAuthenticationFailed = context =>
            {
                if (context.Exception is SecurityTokenExpiredException)
                    context.Response.Headers["X-Token-Expired"] = "true";
                return Task.CompletedTask;
            }
        };
    });

// Fallback policy — every endpoint requires authentication
builder.Services.AddAuthorizationBuilder()
    .SetFallbackPolicy(new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build());

Warning: JWT is not a session

JWT is stateless — once issued, the server can't revoke it until it expires. If you need to revoke immediately (user banned, password changed), you must also use a token blacklist or short-lived tokens + refresh rotation. Don't set exp too long (24h+) — it widens the "zombie token" window.

4. CORS, CSP, and Security Headers

CORS — Cross-Origin Resource Sharing

CORS is the browser's mechanism to control cross-origin requests. Misconfigured CORS is one of the most common Security Misconfiguration (API8) bugs.

sequenceDiagram
    participant B as Browser
    participant API as API Server

    Note over B,API: Preflight Request (PUT/DELETE/custom headers)
    B->>API: OPTIONS /api/data
Origin: https://app.example.com
Access-Control-Request-Method: PUT API->>API: Is the origin in the allowlist? API-->>B: 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Max-Age: 3600 Note over B,API: Actual Request B->>API: PUT /api/data
Origin: https://app.example.com API-->>B: 200 OK
Access-Control-Allow-Origin: https://app.example.com

The CORS preflight flow and the actual request

Config Risk Recommendation
Access-Control-Allow-Origin: * Any domain can call the API Use only for public, unauthenticated APIs
Reflecting the Origin header as-is Completely bypasses CORS — equivalent to * Whitelist specific origins
Allow-Credentials: true + Origin: * Browsers block it (invalid), but devs sometimes bypass by reflecting Credentials only with specific origins
No Max-Age set Browser sends a preflight on every request → higher latency Set Max-Age: 3600 (1 hour)

Correct CORS configuration in .NET 10:

builder.Services.AddCors(options =>
{
    options.AddPolicy("Production", policy =>
    {
        policy
            .WithOrigins(
                "https://app.example.com",
                "https://admin.example.com"
            )
            .WithMethods("GET", "POST", "PUT", "DELETE")
            .WithHeaders("Authorization", "Content-Type", "X-Request-Id")
            .SetPreflightMaxAge(TimeSpan.FromHours(1))
            .AllowCredentials();
    });
});

Essential security headers

Beyond CORS, a whole set of HTTP headers help protect the API and its clients:

// Security headers middleware for .NET 10
app.Use(async (context, next) =>
{
    var headers = context.Response.Headers;

    // Clickjacking protection
    headers["X-Frame-Options"] = "DENY";

    // MIME-type sniffing protection
    headers["X-Content-Type-Options"] = "nosniff";

    // Content Security Policy
    headers["Content-Security-Policy"] =
        "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'";

    // Strict Transport Security (HTTPS only)
    headers["Strict-Transport-Security"] =
        "max-age=31536000; includeSubDomains; preload";

    // Prevent sensitive info in Referer headers
    headers["Referrer-Policy"] = "strict-origin-when-cross-origin";

    // Turn off unnecessary browser APIs
    headers["Permissions-Policy"] =
        "camera=(), microphone=(), geolocation=()";

    await next();
});

5. Input validation and output encoding

Input validation is the first line of defense against injection attacks (SQL Injection, NoSQL Injection, Command Injection, XSS). The core rule: never trust user input — validate everything entering the system from outside.

graph LR
    A["Client Input"] --> B["Schema
Validation"] B --> C["Type
Checking"] C --> D["Business
Rules"] D --> E["Sanitization"] E --> F["Safe Data"] B -->|Invalid| G["400
Bad Request"] C -->|Invalid| G D -->|Invalid| G style A fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style B fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style C fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style D fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style E fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style F fill:#4CAF50,stroke:#fff,color:#fff style G fill:#e94560,stroke:#fff,color:#fff

Input-validation pipeline

Validation on .NET 10 — FluentValidation + Minimal API

// DTO with validation attributes
public record CreateOrderRequest(
    [Required, StringLength(200)] string ProductName,
    [Range(1, 1000)] int Quantity,
    [Required, EmailAddress] string CustomerEmail,
    [RegularExpression(@"^[a-zA-Z0-9\-]+$")] string? CouponCode
);

// FluentValidation for complex business rules
public class CreateOrderValidator : AbstractValidator<CreateOrderRequest>
{
    public CreateOrderValidator()
    {
        RuleFor(x => x.ProductName)
            .NotEmpty()
            .MaximumLength(200)
            .Must(name => !name.Contains("<script>"))
            .WithMessage("Product name contains invalid characters");

        RuleFor(x => x.Quantity)
            .InclusiveBetween(1, 1000);

        RuleFor(x => x.CustomerEmail)
            .EmailAddress(EmailValidationMode.AspNetCoreCompatible);

        RuleFor(x => x.CouponCode)
            .Matches(@"^[A-Z0-9\-]{4,20}$")
            .When(x => x.CouponCode != null);
    }
}

// Minimal API endpoint with validation
app.MapPost("/api/orders", async (
    CreateOrderRequest request,
    IValidator<CreateOrderRequest> validator,
    OrderService service) =>
{
    var result = await validator.ValidateAsync(request);
    if (!result.IsValid)
        return Results.ValidationProblem(result.ToDictionary());

    var order = await service.CreateAsync(request);
    return Results.Created($"/api/orders/{order.Id}", order);
});

Stopping SQL injection — parameterized queries

SQL injection is still threat #1

Even in 2026, SQL injection remains in the top common vulnerabilities. The main cause: developers still concatenate SQL strings instead of using parameterized queries. ORMs like Entity Framework Core prevent most of these, but raw SQL still demands care.

// WRONG — SQL injection vulnerability
var sql = $"SELECT * FROM Users WHERE Name = '{name}'";
// Input: ' OR '1'='1' -- → returns every user

// RIGHT — parameterized query with EF Core
var users = await _db.Users
    .Where(u => u.Name == name)
    .ToListAsync();

// RIGHT — raw SQL with a parameter
var users = await _db.Users
    .FromSqlInterpolated($"SELECT * FROM Users WHERE Name = {name}")
    .ToListAsync();

Stopping XSS in Vue.js

<!-- WRONG — v-html renders HTML without sanitizing -->
<div v-html="userComment"></div>
<!-- Input: <img src=x onerror=alert(document.cookie)> → XSS! -->

<!-- RIGHT — Vue templates escape by default -->
<div>{{ userComment }}</div>
<!-- Output: &lt;img src=x onerror=alert(...)&gt; → safe -->

<!-- If you MUST render HTML (rich text editor): use DOMPurify -->
<script setup>
import DOMPurify from 'dompurify';

const safeHtml = computed(() =>
    DOMPurify.sanitize(userComment.value, {
        ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
        ALLOWED_ATTR: ['href', 'target', 'rel']
    })
);
</script>

<div v-html="safeHtml"></div>

6. A Defense-in-Depth architecture for APIs

No single security measure is enough. Defense in Depth layers multiple overlapping protections — when one layer is bypassed, the next still stops the attacker.

graph TB
    subgraph L1["Layer 1: Network"]
        WAF["WAF
(Cloudflare/AWS WAF)"] DDoS["DDoS
Protection"] TLS["TLS 1.3
Termination"] end subgraph L2["Layer 2: API Gateway"] RL["Rate
Limiting"] AUTH["Authentication
(JWT/OAuth)"] CORS2["CORS
Enforcement"] end subgraph L3["Layer 3: Application"] AUTHZ["Authorization
(RBAC/ABAC)"] VAL["Input
Validation"] BIZ["Business Logic
Guards"] end subgraph L4["Layer 4: Data"] ENC["Encryption
at Rest"] MASK["Data
Masking"] AUDIT["Audit
Logging"] end L1 --> L2 --> L3 --> L4 style L1 fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style L2 fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style L3 fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style L4 fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style WAF fill:#e94560,stroke:#fff,color:#fff style DDoS fill:#e94560,stroke:#fff,color:#fff style TLS fill:#e94560,stroke:#fff,color:#fff style RL fill:#2c3e50,stroke:#fff,color:#fff style AUTH fill:#2c3e50,stroke:#fff,color:#fff style CORS2 fill:#2c3e50,stroke:#fff,color:#fff style AUTHZ fill:#4CAF50,stroke:#fff,color:#fff style VAL fill:#4CAF50,stroke:#fff,color:#fff style BIZ fill:#4CAF50,stroke:#fff,color:#fff style ENC fill:#16213e,stroke:#fff,color:#fff style MASK fill:#16213e,stroke:#fff,color:#fff style AUDIT fill:#16213e,stroke:#fff,color:#fff

A 4-layer Defense-in-Depth architecture for production APIs

Layer-by-layer details

Layer 1 — Network: a WAF (Web Application Firewall) like Cloudflare WAF or AWS WAF in front of the API blocks known attack patterns (SQL injection, XSS, path traversal) before they reach the application. DDoS protection (Cloudflare includes it free on every plan) absorbs abnormal traffic. TLS 1.3 encrypts all traffic.

Layer 2 — API Gateway: centralizes authentication, rate limiting, and CORS enforcement. The API Gateway is the single entry point, helping apply consistent policies across every endpoint. On .NET 10 you can use YARP (Yet Another Reverse Proxy) or Ocelot.

Layer 3 — Application: fine-grained authorization (who can do what on which resource), input validation, and business-logic guards (e.g. a user can't increase their own balance; order quantity can't be negative).

Layer 4 — Data: encrypt sensitive data at rest (SQL Server Always Encrypted, Azure Key Vault), mask data for staging/dev environments, and audit-log every important operation for forensics when needed.

7. Implementation on .NET 10 and Vue.js

Security pipeline on .NET 10

The middleware order in ASP.NET Core matters — security middleware must sit in the right spot:

var app = builder.Build();

// 1. Exception handler (first — catches all errors)
app.UseExceptionHandler("/error");

// 2. HSTS (redirect HTTP → HTTPS)
app.UseHsts();
app.UseHttpsRedirection();

// 3. Security headers (custom middleware)
app.UseSecurityHeaders();

// 4. CORS (before authentication)
app.UseCors("Production");

// 5. Rate limiting
app.UseRateLimiter();

// 6. Authentication (identity verification)
app.UseAuthentication();

// 7. Authorization (permission check)
app.UseAuthorization();

// 8. Endpoints
app.MapControllers();

Tip: use Problem Details for security errors

.NET 10 supports RFC 9457 (Problem Details) for error responses. When returning a security error, NEVER leak internal information: no stack trace, table names, or query details. Return only what the client needs to handle.

// Global exception handler — no internal detail leaks
app.MapGet("/error", (HttpContext context) =>
{
    var error = context.Features.Get<IExceptionHandlerFeature>()?.Error;

    return Results.Problem(
        title: "An error occurred",
        statusCode: 500,
        detail: "Please contact support with the trace ID.",
        extensions: new Dictionary<string, object?>
        {
            ["traceId"] = Activity.Current?.Id ?? context.TraceIdentifier
        }
    );
});

Security patterns in Vue.js

// composables/useAuth.ts — Secure token management
import { ref, computed } from 'vue';

const accessToken = ref<string | null>(null);
const refreshToken = ref<string | null>(null);

export function useAuth() {
    // Do NOT store tokens in localStorage (XSS-accessible)
    // Use in-memory + httpOnly cookie for the refresh token

    const isAuthenticated = computed(() => !!accessToken.value);

    async function login(email: string, password: string) {
        const response = await fetch('/api/auth/login', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            credentials: 'include', // sends the httpOnly cookie
            body: JSON.stringify({ email, password })
        });

        if (!response.ok) throw new Error('Login failed');

        const data = await response.json();
        accessToken.value = data.accessToken;
        // refreshToken lives in an httpOnly cookie — JS can't read it
    }

    async function fetchWithAuth(url: string, options: RequestInit = {}) {
        const headers = new Headers(options.headers);

        if (accessToken.value) {
            headers.set('Authorization', `Bearer ${accessToken.value}`);
        }

        let response = await fetch(url, {
            ...options,
            headers,
            credentials: 'include'
        });

        // Auto-refresh on 401
        if (response.status === 401) {
            await refreshAccessToken();
            headers.set('Authorization', `Bearer ${accessToken.value}`);
            response = await fetch(url, {
                ...options,
                headers,
                credentials: 'include'
            });
        }

        return response;
    }

    return { isAuthenticated, login, fetchWithAuth };
}

Pre-production API security checklist

Item Check Tools
Authentication JWT validated properly (alg, exp, iss, aud); unsigned tokens rejected jwt.io debugger, Burp Suite
Authorization Every endpoint checks permissions; test BOLA via ID enumeration OWASP ZAP, Postman scripts
Input validation Reject inputs outside the schema; test SQL injection/XSS payloads SQLMap, XSStrike, Burp Scanner
CORS No * wildcard for authed APIs; test with unknown origins curl manual tests, CORS tester
Rate limiting Block >100 requests/min/IP; test burst traffic ab (Apache Bench), k6
Security headers HSTS, X-Content-Type-Options, CSP, X-Frame-Options securityheaders.com, Mozilla Observatory
Error handling No stack trace, DB schema, or internal paths leaked Manual testing, Sentry config
Logging Log auth failures and authz denials; NEVER log tokens/passwords Structured logging, SIEM
Dependencies No known CVEs in NuGet/npm packages dotnet audit, npm audit, Snyk
TLS TLS 1.2+ only, strong cipher suites, valid certificates SSL Labs, testssl.sh

8. Conclusion

API security isn't something you "add later" — it has to be designed in from day one, embedded into every layer of the system. The OWASP API Security Top 10 is a great framework for systematically assessing and improving API security. Combined with JWT hardening per RFC 8725, correct CORS/CSP, strict input validation, and a Defense-in-Depth architecture, you can build production-safe APIs without sacrificing developer experience.

Remember: security is a continuous process, not a state to be reached. Regular audits, updated dependencies, and tracking OWASP plus security advisories are work that never really ends.

Quick recap

  • OWASP API Security Top 10 (2023) is the compass — BOLA (#1) and Broken Authentication (#2) are the most common
  • JWT: use RS256/ES256, short-lived tokens, validate enough claims (exp, iss, aud, nbf)
  • CORS: whitelist specific origins, no wildcards for authed APIs
  • Input validation: validate at the boundary, use parameterized queries for SQL, DOMPurify for HTML
  • Defense in Depth: WAF → API Gateway → Application → Data — each layer blocks a different class of attack

References