Securing Your API Is More Than Just Adding a Token

by Eric Hanson, Backend Developer at Clean Systems Consulting

The checklist that stops at authentication

Most API security discussions cover authentication well: use OAuth, use JWTs, rotate your API keys. What gets less attention is everything that happens after a valid token arrives. Authentication tells you who is making the request. It says nothing about whether what they are doing is safe.

An authenticated attacker is still an attacker. They have a valid token — possibly stolen, possibly from a compromised account, possibly their own account used maliciously. Authentication being present does not mean authorization is correct, inputs are safe, data leakage is prevented, or denial-of-service is impossible.

Authorization: the layer authentication is not

Authentication and authorization are distinct. Authentication says "this is user 4291." Authorization says "user 4291 is allowed to access this resource in this way."

The two most common authorization failures in APIs are:

Broken Object Level Authorization (BOLA, also called IDOR): an authenticated user requests a resource they do not own by manipulating an ID parameter.

GET /api/v1/invoices/10042
Authorization: Bearer <valid_token_for_user_A>

If invoice 10042 belongs to user B, and the server returns it because the token is valid without checking ownership, that is BOLA. It is the most common API vulnerability class (OWASP API Security Top 10, #1).

The fix is at the data access layer, not the authentication layer:

def get_invoice(invoice_id: str, current_user: User) -> Invoice:
    invoice = db.get(Invoice, invoice_id)
    if invoice is None:
        raise NotFound()
    if invoice.owner_id != current_user.id:
        raise Forbidden()  # not NotFound — don't leak existence
    return invoice

Never trust client-supplied IDs at face value. Always verify ownership against the authenticated identity.

Broken Function Level Authorization: endpoints that perform privileged operations are accessible to non-privileged users because the authorization check was missed.

This often happens when admin endpoints share a router with public endpoints and authorization middleware is only applied selectively. Apply authorization checks at the handler level as a default, not an opt-in.

Input validation is a security control

SQL injection, NoSQL injection, command injection, and XML injection are all input validation failures dressed up as security issues. The defense is treating all input as untrusted, regardless of whether the caller is authenticated.

Use parameterized queries — never string interpolation for database queries:

# Vulnerable
query = f"SELECT * FROM users WHERE email = '{email}'"

# Safe
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))

Validate input shape and semantics before any processing. Reject requests that do not conform — do not sanitize and proceed. Sanitization is error-prone; rejection is robust.

Sensitive data in responses

Returning more data than necessary is a security failure. A user endpoint that returns hashed_password, internal_account_flags, or created_by_admin_id alongside the public profile is leaking data that clients have no business receiving.

Define response schemas explicitly and serialize only to that schema. In frameworks that support it (Spring, FastAPI, NestJS), use response serializer annotations or DTOs that explicitly whitelist returned fields rather than returning your entire domain object.

This also applies to error messages: never return stack traces, SQL errors, or internal system names in production API responses.

Transport security is not optional

All API traffic should be over TLS 1.2 minimum, TLS 1.3 preferred. Redirect HTTP to HTTPS at the load balancer layer. Set HSTS headers with a long max-age:

Strict-Transport-Security: max-age=31536000; includeSubDomains

For internal service-to-service traffic in a zero-trust environment, mTLS provides mutual authentication: both the client and server present certificates. This prevents a compromised internal service from calling other services using only a stolen token.

Rate limiting is security, not just operations

An unauthenticated endpoint with no rate limiting is a credential stuffing surface. A valid-token endpoint with no rate limiting is a data harvesting surface. Rate limiting at the API gateway level protects both.

Implement limits at multiple granularities: per-IP for unauthenticated endpoints, per-API-key or per-user for authenticated endpoints, and per-endpoint for sensitive operations like password reset or 2FA verification.

For authentication endpoints specifically, apply exponential backoff on failed attempts and alert on high failure rates from a single source.

Logging and monitoring as a security control

You cannot detect an attack you are not logging for. At minimum, log:

  • Authentication failures with client IP and user agent
  • Authorization failures with the authenticated identity, endpoint, and resource ID
  • Abnormal request patterns (high volume from a single token, sequential ID enumeration)
  • All requests to sensitive endpoints (admin functions, bulk exports, permission changes)

Do not log request bodies containing credentials or PII. Log the structure and outcome, not the sensitive content.

Set up alerts on authentication failure rate spikes, sudden increases in 403 responses from a previously normal client, and access to resources by IDs outside the expected range.

Security is not a state you achieve — it is a set of ongoing controls. Authentication is the first one, not the last.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

Flash Drives, Multi-Layer RDP, and Manager Approvals: A Day in a Bureaucratic Dev Team

You sit down to fix a small bug. It should take 10 minutes. Six hours later, you’re still waiting—for access, for approval, for something to happen.

Read more

Hong Kong's Backend Developer Market Is Contracting — Here Is How Smart Startups Are Responding

Hong Kong's tech talent pool has been shrinking for reasons that have nothing to do with the startup scene's ambitions.

Read more

Java Streams Are Lazy — What That Means for Performance and Correctness

Stream intermediate operations do not execute until a terminal operation is called. This laziness enables short-circuiting, infinite streams, and fusion optimizations — and causes correctness bugs when side effects are assumed to have already fired.

Read more

New Zealand's Tech Talent Pool Is Small. Async Remote Contractors Are How Startups Close the Gap

You've been looking for a senior backend engineer for three months. You've seen every relevant CV in Auckland twice. The pool isn't refreshing — it's the same twelve people.

Read more