You are currently viewing Secure API Design: Authorization, Validation, and Testing

Secure API Design: Authorization, Validation, and Testing

A request for GET /orders/4821 can be valid and authenticated yet still expose another customer’s order. The server needs to check who made the request and whether that person may read order 4821. Accepting a request is not the same as authorizing what it does.

APIs serve browsers, mobile apps, other services, and automation. Clients can call an endpoint without going through your interface, so hiding a button in the UI offers no protection. Document the resources, callers, data classifications, and permitted operations, then enforce those rules on the server for every request.

Make the API contract a security boundary

Before implementing a route, define what it accepts and returns: HTTP method, path, required identity, writable fields, response fields, and expected errors. A customer-facing order update, for example, might accept a delivery note but must not let the client choose ownerId, price, or status.

An order ID locates a record; it does not grant access to it. An undocumented endpoint is still reachable if it is deployed. Inventory administrative routes, older API versions, and background-service endpoints alongside customer routes. Remove operations that are no longer used rather than leaving them exposed without an owner.

Put size limits in the contract too: maximum body length, string length, collection count, and supported content types. Predictable limits help stop a small request from consuming excessive memory or database work.

API routes mapped to callers and permissions

Authenticate callers, then authorize each action

Authentication establishes an identity; authorization determines what that identity may do. Use a mature authentication component instead of inventing token formats or password handling. Verify a token’s signature, expected issuer and audience where applicable, and lifetime before trusting its claims. Plan for token rotation or revocation as well.

Authorization must account for both the action and the resource. A general customer role is not enough for GET /orders/{id}: the server must establish that the order belongs to that customer or has been explicitly shared. For list endpoints, scope the database query to permitted records instead of fetching everything and filtering afterward.

Keep permissions close to the operation

Write down rules such as “support staff may view order contact details only when assigned to the case,” then test them at the service boundary. A route-level check can miss another route that calls the same operation. Deny access when identity, assignment, or policy information is missing. Give service-to-service credentials only the operations each service needs, rather than a broad administrator role.

For browser sessions authenticated with cookies, account for cross-site request forgery on state-changing requests. Restrict cookie scope, choose appropriate SameSite settings, and use a CSRF defense suited to the application. Keep bearer tokens out of URLs, where they can end up in logs or browser history.

Validate input and constrain output

Validate both structure and meaning at the server boundary. A schema can reject unknown fields and incorrect types; business rules must also catch impossible date ranges or quantities outside the allowed range. Normalize values only when the contract specifies how, since silent changes can cause surprising behavior or authorization mistakes. Use parameterized database queries, and use APIs that pass untrusted values to other interpreters as data rather than commands.

Frameworks that map an entire JSON object onto a database model can introduce mass-assignment risks. List the writable fields for each operation explicitly. Apply the same discipline to responses: return an approved response model instead of serializing a database object wholesale. An internal field added next month should not appear in the public API by accident.

A syntactically valid URL is not necessarily a safe destination for a server-side fetch; an unrestricted fetch may reach internal services. Prefer a fixed set of destinations where possible. Otherwise, restrict destinations, control network egress, and handle redirects carefully. For uploads, validate file type, size, and downstream processing behavior separately from the filename.

Protect data in transit and at rest

Use HTTPS for external traffic and for internal hops where traffic could be observed or altered. Configure certificates and trusted endpoints deliberately. Disabling certificate verification to get a development connection working removes the identity check TLS provides. Store secrets in a managed secret store or protected environment configuration, not in source control, sample requests, or client-side code.

Collect and retain only the data an operation needs. If it needs a shipping region, do not put a full address in its logs. Classify sensitive fields so developers know which responses, backups, and diagnostics need tighter access. Encryption at rest protects stored data, but it cannot fix an endpoint that returns it to the wrong caller.

Control how much work a request can trigger

Rate limits can reduce abuse and accidental overload, but a per-IP limit alone may penalize people sharing a network while failing to constrain one authenticated account. Consider the caller, endpoint, and cost of the operation, and decide how anonymous traffic is handled. Set request timeouts and cap pagination size; do not let clients request a page containing everything.

Expensive searches and exports need tighter limits than lightweight lookups. Bound database query complexity and concurrent jobs. For an operation that may be retried after a network interruption, consider an idempotency key. The server records the result for that caller and key so a retry does not create a second charge or order. Specify how long the key remains valid and what happens if it is reused with different input.

Request trends monitored for unusual API activity

Make errors and observability safe

Use consistent status codes and error bodies that help clients recover without exposing internals. A validation error can name an invalid field; a production response should not contain a stack trace, database query, or credential. Decide whether an unauthorized lookup should reveal that a resource exists. Where existence is sensitive, a consistent “not found” response may be appropriate.

Log what you need to investigate failures: timestamp, route template, request ID, authenticated principal or service identity, decision outcome, and latency. Leave out raw authorization headers, passwords, full request bodies, and sensitive response data. A template such as /orders/{id} is generally more useful for aggregate monitoring than a long list of individual URLs.

Watch for repeated authorization denials, spikes in failed authentication, unusual export volume, and changes in response size or latency. Alerts should point to a reviewable event, not collect secrets in a dashboard. Restrict access to logs and set retention periods that reflect the data they contain.

Test the boundary, not just the happy path

Build tests around roles, resources, and actions. For each sensitive operation, try an authorized caller, an unauthenticated caller, and an authenticated caller who cannot access that particular record. Test omitted fields, wrong types, oversized values, and attempts to set server-controlled properties. A passing 200 test tells you little about the denial path.

A compact order-endpoint test set

  • A customer can retrieve their own order but receives no other customer’s order data.
  • A customer cannot change the owner, calculated price, or fulfillment status through an update request.
  • A list request returns only permitted records and respects the maximum page size.
  • An invalid or expired credential is rejected before any order data is returned.
  • A repeated create request with the same valid idempotency key does not create a duplicate order.

Run these tests in an isolated environment using accounts you control. Add integration tests for database scoping and middleware configuration: mocked unit tests can miss a route without an authorization check. Review dependencies, configuration changes, and generated API documentation during development. Even a secure handler can be exposed by an overly broad gateway rule.

Plan for changes after deployment

API security includes versioning. When an endpoint changes, check that old clients cannot keep using a deprecated, less restrictive route indefinitely. Track deployed versions, announce retirement dates to legitimate consumers, and remove old routes after the migration window. Review new response fields as deliberate disclosures.

Assign an owner to each endpoint and provide a way to report unexpected behavior. At deployment, confirm that production secrets differ from test credentials, debugging responses are disabled, and gateway limits match the application’s assumptions. For a new GET /orders/{id} route, finish with a concrete check: request the order as its owner and as a different test customer. Only the owner’s response should contain the order data.