Skip to content
Insights
Software & Platform8 min read ·

API Security Beyond OAuth: Implementing Advanced Authentication for Enterprise REST and GraphQL APIs

API Security Beyond OAuth: Implementing Advanced Authentication for Enterprise REST and GraphQL APIs

The short answer: advanced API authentication means treating an OAuth access token as the start of your security model, not the whole of it. For enterprise REST and GraphQL APIs, that means four things. Bind tokens to the client that requested them, so a stolen token is useless on its own. Validate every token strictly at the resource server. Use token exchange instead of forwarding user tokens between services. Push authorization decisions down to the object and field level. OAuth 2.0 and OpenID Connect tell you how to obtain tokens. They say comparatively little about how to stop those tokens from being replayed, over-scoped, or trusted by the wrong service.

If your current setup is "the gateway checks a bearer JWT and everything behind it trusts the request," you are carrying risk that the standards community has spent the last several years documenting and addressing. This post walks through the patterns we recommend, with references to the specifications behind them.

Why Bearer Tokens Alone Fall Short

A bearer token works like cash: whoever holds it can spend it. That model breaks down in several ways teams run into regularly:

  • Token leakage through logs, browser storage, misconfigured proxies, or compromised CI systems. The token is valid for whoever finds it.
  • Audience confusion, where a token minted for one API is accepted by another because both trust the same issuer and nobody checks the aud claim.
  • Token forwarding in microservice chains, where a user's broad token is passed downstream to services that need only a fraction of its privileges.
  • Authorization gaps that authentication cannot fix. The OWASP API Security Top 10 (2023) lists Broken Object Level Authorization as API1 and Broken Authentication as API2. Most of the list concerns what an authenticated caller can reach.

The IETF's OAuth 2.0 Security Best Current Practice (RFC 9700) and the in-progress OAuth 2.1 draft consolidate much of this guidance. They form a good baseline before adding anything more advanced.

OAuth 2.1 Best Practices as Your Baseline

OAuth 2.1 is not a new protocol. It is OAuth 2.0 with the risky parts removed and the hardening made mandatory. If you do nothing else, align with it:

  1. Authorization Code with PKCE for all clients, including confidential ones. PKCE (RFC 7636) protects against code interception and injection.
  2. No implicit grant and no resource owner password grant. Both are omitted from 2.1.
  3. Exact redirect URI matching. No wildcards, no prefix matching.
  4. Refresh token protection. Public clients should use sender-constrained refresh tokens or refresh token rotation.
  5. No tokens in query strings. Bearer tokens belong in the Authorization header.

For identity, use OpenID Connect for APIs carefully. The ID token tells the client who the user is. It is not an access token and should never be sent to your API as proof of authorization. APIs should receive access tokens issued for their audience.

Sender-Constrained Tokens: The Core of Advanced API Authentication

The single biggest upgrade over plain OAuth is sender constraining: binding a token to a key the legitimate client holds. There are two standardized options.

Mutual TLS (RFC 8705)

The client authenticates with an X.509 certificate during the TLS handshake. The authorization server embeds the certificate's SHA-256 thumbprint in the token's cnf claim:

{
  "iss": "https://auth.example.com",
  "aud": "https://api.example.com/orders",
  "sub": "svc-billing",
  "exp": 1735689600,
  "cnf": {
    "x5t#S256": "bwcK0esc3ACC3DB2Y5_lESsXE8o9ltc05O89jdN-dg2"
  }
}

The resource server compares that thumbprint to the certificate presented on the connection. mTLS fits service-to-service traffic inside infrastructure you control, especially where a service mesh already issues workload certificates. It is harder to use from browsers and mobile apps, and TLS termination at a load balancer means you must forward certificate details securely.

DPoP (RFC 9449)

Demonstrating Proof of Possession works at the application layer. The client generates a key pair and signs a short-lived proof JWT for every request:

// DPoP proof header
{ "typ": "dpop+jwt", "alg": "ES256", "jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." } }

// DPoP proof payload
{
  "jti": "e1j3V_bKic8-LAEB",
  "htm": "POST",
  "htu": "https://api.example.com/orders",
  "iat": 1735689000,
  "ath": "fUHyO2r2Z3DZ53EsNrWBb0xWXoaNy59IiKCAqksmQEo"
}

The request then carries both:

POST /orders HTTP/1.1
Host: api.example.com
Authorization: DPoP eyJhbGciOiJFUzI1NiIs...
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIs...

The access token contains cnf.jkt, the thumbprint of the client's public key. The API verifies the proof signature, checks that htm and htu match the actual request, confirms ath is the hash of the presented access token, and rejects reused jti values within its acceptance window. DPoP works well for single-page apps and mobile clients, where mTLS is impractical.

Our rule of thumb: use mTLS for workloads and DPoP for user-facing clients, and require one or the other for any token that grants write access to sensitive data.

Strict Token Validation at the Resource Server

Sender constraining is only as good as the validation behind it. RFC 8725 (JWT Best Current Practices) and RFC 9068 (JWT Profile for OAuth 2.0 Access Tokens) define what "strict" means. In practice:

  • Pin allowed algorithms. Never let the token's alg header decide. Reject none.
  • Check typ. RFC 9068 access tokens use at+jwt, which prevents an ID token from being accepted as an access token.
  • Validate iss, aud, exp, and nbf on every request, with small clock-skew tolerance.
  • Fetch keys from JWKS with caching and rotation handling, not a hardcoded key.

Here is a minimal example using the jose library in Node.js:

import { createRemoteJWKSet, jwtVerify } from 'jose';

const JWKS = createRemoteJWKSet(new URL('https://auth.example.com/.well-known/jwks.json'));

export async function verifyAccessToken(token) {
  const { payload } = await jwtVerify(token, JWKS, {
    issuer: 'https://auth.example.com',
    audience: 'https://api.example.com/orders',
    algorithms: ['ES256'],
    typ: 'at+jwt',
    clockTolerance: 30,
  });

  if (!payload.cnf) {
    throw new Error('Token is not sender-constrained');
  }
  return payload;
}

For high-risk operations, consider opaque tokens with introspection (RFC 7662) instead of self-contained JWTs. Introspection lets you revoke immediately at the cost of a network call per request or per cache window.

Token Exchange for Service-to-Service Calls

When the orders API needs to call the inventory API on a user's behalf, forwarding the original token is tempting and wrong. It gives inventory a token with the wrong audience and, often, more scope than it needs.

OAuth 2.0 Token Exchange (RFC 8693) solves this. The calling service trades the incoming token for a new, narrowly scoped one:

POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=eyJhbGciOiJFUzI1NiIs...
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&audience=https://api.example.com/inventory
&scope=inventory:read

The new token carries the user's identity (and optionally an act claim identifying the calling service), targets exactly one audience, and has the minimum scope. Each hop gets an auditable, least-privilege credential. For fine-grained requirements, such as "approve payment up to a specific amount," look at Rich Authorization Requests (RFC 9396), which replaces flat scope strings with structured authorization_details.

Securing GraphQL Endpoints

GraphQL changes the threat model because a single endpoint exposes an entire graph. Authentication at the HTTP layer is necessary but nowhere near sufficient.

Authorize at the field and object level

Every resolver that returns sensitive data should enforce authorization against the authenticated principal, not just the top-level query. Schema directives make the policy visible:

directive @requiresScope(scope: String!) on FIELD_DEFINITION
directive @ownerOnly on FIELD_DEFINITION

type Customer {
  id: ID!
  name: String!
  email: String! @ownerOnly
  paymentMethods: [PaymentMethod!]! @requiresScope(scope: "payments:read")
}

The directive implementation should run inside the resolver chain, so nested queries such as order { customer { paymentMethods } } cannot bypass it.

Control what can be asked

  • Persisted or allow-listed queries for first-party clients, so production accepts only known operations.
  • Depth and complexity limits to prevent resource exhaustion (OWASP API4: Unrestricted Resource Consumption).
  • Alias and batching limits. Attackers use aliases to run hundreds of login or OTP-verification mutations in one HTTP request, defeating per-request rate limits.
  • Disable introspection in production for public-facing APIs, recognizing this is defense in depth and not a substitute for authorization.

REST API Authentication Strategies That Hold Up

For REST, the same principles apply with a different shape:

  • Enforce object-level checks in the handler, not just route-level scope checks. GET /orders/{id} must verify the caller owns or may access that order.
  • Separate read and write scopes, and require sender-constrained tokens for writes.
  • Use step-up authentication for sensitive operations. RFC 9470 defines how an API can signal that a higher authentication level or more recent login is required.
  • Centralize policy with an engine such as Open Policy Agent or a similar policy-as-code tool, so authorization logic is testable and reviewable rather than scattered across services.

Putting It Together

A practical rollout sequence that avoids a big-bang migration:

  1. Align authorization servers and clients with OAuth 2.1 and RFC 9700.
  2. Harden resource-server validation (algorithms, typ, aud, JWKS rotation).
  3. Introduce token exchange for internal service calls.
  4. Add sender constraining, starting with mTLS for workloads, then DPoP for public clients.
  5. Layer in field-level GraphQL authorization and object-level REST checks, backed by centralized policy.

Each step reduces risk independently, which matters when you are changing authentication on systems that cannot go down. If you are planning this kind of work, our software and platform engineering capabilities cover API architecture and security hardening, and we've written about how requirements differ across regulated industries such as finance and healthcare.

FAQ

Is OAuth 2.1 a finished standard?

As of this writing, OAuth 2.1 is an IETF draft. Most of its requirements already exist in published RFCs, particularly RFC 9700 (OAuth 2.0 Security Best Current Practice), so you can adopt its guidance now without waiting for final publication.

Should I choose DPoP or mTLS?

Use mTLS for service-to-service traffic where you control certificate issuance, such as within a service mesh. Use DPoP for browser and mobile clients, where managing client certificates is impractical. Many enterprises run both.

Can I send OpenID Connect ID tokens to my API?

No. ID tokens are intended for the client application to learn about the user. APIs should accept only access tokens issued for their audience, ideally typed as at+jwt per RFC 9068 so the two cannot be confused.

Does disabling GraphQL introspection secure my API?

It reduces discoverability but does not prevent attacks. Determined attackers can infer schemas through error messages and client code. Field-level authorization, query allow-listing, and complexity limits provide the actual protection.

Are JWTs or opaque tokens better for enterprise APIs?

Both are valid. JWTs allow local validation with low latency but are harder to revoke before expiry. Opaque tokens with introspection (RFC 7662) support immediate revocation at the cost of a lookup. Short-lived JWTs combined with sender constraining are a common middle ground.