Skip to content
Starone

Security

Designing secure APIs

Most serious API breaches come from authorisation logic, not broken cryptography. This guide covers object-level authorisation, token design, input validation, rate limiting and service-to-service trust, and where each control belongs.

· 5 min read · Starone Engineering

An API is the most direct route to a system's data and operations, and it is designed to be called by software, which makes it straightforward to probe at volume. The failures that matter are rarely exotic. They are endpoints that authenticate the caller and then return someone else's record, accept a field the client should never set, or allow unlimited attempts at an expensive operation.

Our view is that API security is primarily a design discipline. A gateway and a web application firewall are useful, but neither knows whether user A may read invoice B. That decision lives in the application, and it has to be made on every request.

Authorise every object, on every request

Broken object level authorisation, listed first in the OWASP API Security Top 10 and long known as IDOR, is the defining API vulnerability. The endpoint checks that the token is valid, takes an identifier from the path and returns the object, without checking that the object belongs to the caller. Changing the identifier in the URL exposes another customer's data.

Unpredictable identifiers such as UUIDs make enumeration harder but are not a control, because identifiers leak through logs, referrers and other responses. The fix is structural: scope every data access by the caller's identity and tenant in the query itself, so that a record outside that scope is simply not found. In multi-tenant systems, database row-level security adds a second layer beneath the application.

Function-level authorisation needs the same care. Administrative operations must check role and permission on the server, and must not rely on the client hiding a button. We define authorisation rules centrally, as policy evaluated by middleware or a policy engine, and default to deny for any route without an explicit rule.

Design tokens for least privilege

Authentication should rest on established protocols, OAuth 2.0 and OpenID Connect, implemented with a maintained library. The design decisions that remain are about limiting what a stolen token can do.

  • Keep access tokens short-lived and use rotating refresh tokens, with revocation on reuse.
  • Issue narrow scopes per client and check them at the resource server, alongside the object-level check that scopes cannot replace.
  • Validate signature, issuer, audience and expiry on every request, and pin the accepted algorithms so a token cannot choose its own.
  • Use the authorisation code flow with PKCE for browser and mobile clients, and keep tokens out of URLs and logs.
  • Consider sender-constrained tokens, through DPoP or mutual TLS binding, for high-value APIs.

Validate input and control properties at the boundary

Binding a request body directly to a persistence model allows mass assignment: a client adds a role, balance or ownerId field and the framework writes it. Returning the model directly causes the reverse problem, exposing internal fields the interface never shows. Use explicit request and response schemas per endpoint, reject unknown fields, and treat any field that affects privilege, price or ownership as server-controlled.

Every request should be validated against a strict schema before any business logic runs: types, formats, lengths, ranges and allowed values. An OpenAPI definition enforced at the gateway or in middleware makes the contract executable and rejects malformed input early. Validation complements, and does not replace, parameterised queries and output encoding.

Any endpoint that fetches a URL supplied by the client, such as a webhook registration or an image import, is a server-side request forgery risk. Resolve and check the destination against an allow-list, block private and metadata address ranges, and make the outbound call from a network segment with no access to internal services.

Limit consumption, not just request rate

Rate limiting protects availability and slows credential stuffing and enumeration. It should be keyed on the authenticated identity or API key where one exists, because per-IP limits are easily spread across addresses and unfairly penalise users behind a shared gateway.

  • Apply stricter limits to authentication, password reset and one-time-code endpoints.
  • Cap page sizes, payload sizes, upload sizes and execution time.
  • For GraphQL, bound query depth and complexity, since one request can trigger thousands of resolver calls.
  • Limit business actions that cost money, such as sending SMS messages or calling a paid third-party API.
  • Return 429 with a Retry-After header so well-behaved clients can back off.

Do not trust the internal network

A common architecture authenticates at the edge and lets internal services trust any request that reaches them. One compromised service, or one SSRF flaw, then has the run of the system. Services should authenticate each other with mutual TLS using short-lived workload certificates, issued through a service mesh or a SPIFFE-based identity system, and authorise by service identity.

The end user's identity must travel too. A downstream service that sees only the calling service's identity cannot enforce object-level rules, which is the confused deputy problem. Propagate the user context in a signed token, ideally exchanged for one with a narrower audience at each hop.

Know what you expose and watch it

An undocumented endpoint cannot be protected. Keep an inventory generated from the API definitions, retire old versions on a published schedule, and keep debug and internal routes off public listeners. Log authentication and authorisation decisions with caller, object and outcome, and exclude tokens and personal data from the log. A rising rate of 403 or 404 responses from one identity is often the earliest sign of enumeration.

Authorisation also has to be tested. Automated tests that call each endpoint as a second user and a second tenant, and assert that access is refused, catch regressions that scanners miss.

Proportion and trade-offs

Not every control fits every system. Mutual TLS between services brings certificate rotation and debugging overhead, and for a small system on a private network with a few services, network policy and signed service tokens may be a proportionate starting point. A static API key is acceptable for low-risk server-to-server integrations if it is scoped, rotatable and stored in a secrets manager. Object-level authorisation, strict schemas and consumption limits are different: we treat them as non-negotiable at any scale, because they address the failures attackers look for first.

Working on something similar

We are happy to compare notes. Talk to the engineers who wrote this.