n4nAI

When to use OAuth2 instead of static API keys

A practical guide on when to use OAuth2 instead of API keys for LLM integrations, covering delegation, rotation, and multi-tenant tradeoffs.

n4n Team6 min read1,241 words

Audio narration

Coming soon — every post will get a voice note here.

Deciding when to use OAuth2 instead of API keys determines how much operational security you inherit versus how much you build yourself. For teams shipping LLM features, the trade-off centers on delegated access, tenant isolation, and token lifecycle rather than raw convenience. This guide gives an ordered path to choose and implement the right scheme.

1. Map your trust boundaries

Start by listing every caller of your service. If the only client is your own backend job polling an embedding endpoint inside a sealed network, a static API key issued per environment is sufficient. The moment a partner service or a customer-deployed app calls your LLM route on behalf of their users, you have crossed into delegated access territory.

Static keys assume a single trusted owner. OAuth2 assumes the caller is separable from the resource owner. Draw a box around each trust domain before writing code. Internal microservices that talk only to your own proxy can share a key with tight network policies; an external SaaS marketplace where vendors call your completion API cannot.

2. Evaluate delegation requirements

The core reason to pick OAuth2 is delegated authorization. A user grants your app limited scope to call a model provider; you present a token that proves that grant. API keys cannot express “act as user X with read-only chat scope.”

When you need consent screens, per-user revocation, or scoped access to a subset of models, you have your answer on when to use OAuth2 instead of API keys. The authorization code flow makes this explicit:

  1. Client redirects user to auth server.
  2. User authenticates and approves scopes.
  3. Auth server returns a short-lived code.
  4. Client exchanges code for access + refresh tokens.

A static key bolted to a proxy that injects user context is a homegrown, fragile approximation of this. If you are building that proxy, stop and use OAuth2.

{
  "scope": "llm:chat llm:embed",
  "aud": "https://api.example.com",
  "sub": "user_123",
  "exp": 1710000000
}

That claim set is impossible with a raw sk- string.

3. Assess token rotation and revocation needs

API keys live for months or years. Leak one via a log dump and you scramble to rotate across all environments, possibly breaking downstream clients. OAuth2 access tokens expire in minutes; a refresh token can be revoked centrally without redeploying clients.

If your compliance regime demands short-lived credentials and audit trails, OAuth2 wins. For a nightly batch script inside a sealed VPC, a static key with tight network controls is simpler and equally safe. Rotation overhead for OAuth2 is shifted to your auth provider: you manage refresh token lifetimes, not secret distribution.

A practical midpoint: issue static keys with a mandatory 30-day expiry and automated rotation in Vault. But that still lacks per-user scoping. The question of when to use OAuth2 instead of API keys resurfaces as soon as you need to revoke one user without revoking the whole pipeline.

4. Match the client profile

Choose the grant type before the library:

Client type Grant Static key fit
Backend job, own infra Client credentials Good
Web app with user login Auth code + PKCE Poor
Mobile app Auth code + PKCE Dangerous
CLI without browser Device flow None
Third-party server Client credentials Risky

Static API keys map only to the first row and fail the others without bolting on fragile proxies. OAuth2 covers all rows with standardized flows.

5. Implement client credentials for LLM service calls

Below is a minimal Python client fetching a token and calling an OpenAI-compatible chat endpoint. This pattern fits backend services that previously hard-coded Authorization: Bearer sk-....

import requests, time

class TokenCache:
    def __init__(self, client_id, client_secret, token_url, scope):
        self._creds = (client_id, client_secret)
        self._url = token_url
        self._scope = scope
        self._token = None
        self._expiry = 0

    def get(self):
        if time.time() < self._expiry - 30:
            return self._token
        r = requests.post(
            self._url,
            data={"grant_type": "client_credentials", "scope": self._scope},
            auth=self._creds, timeout=5,
        )
        r.raise_for_status()
        body = r.json()
        self._token = body["access_token"]
        self._expiry = time.time() + body["expires_in"]
        return self._token

def chat(token, prompt, base_url):
    r = requests.post(
        f"{base_url}/v1/chat/completions",
        headers={"Authorization": f"Bearer {token}"},
        json={"model": "gpt-4o-mini",
              "messages": [{"role": "user", "content": prompt}]},
        timeout=30,
    )
    r.raise_for_status()
    return r.json()

cache = TokenCache("svc_1", "secret", "https://auth.example.com/token", "llm:chat")
print(chat(cache.get(), "Summarize: OAuth2 vs keys", "https://api.example.com"))

The access token expires; the cache refreshes it transparently. That single change removes long-lived secrets from your codebase and gives you a clear audit point.

6. Integrate with an LLM gateway

When you sit behind a gateway that aggregates providers, the auth layer must map external tokens to internal routing. n4n.ai exposes one OpenAI-compatible endpoint covering 240+ models, automatically falls back when a provider is rate-limited or degraded, and applies per-token usage metering. OAuth2 lets the gateway issue a tenant-scoped bearer, then internally swap it for the correct provider key without exposing those keys to the caller. The metering attaches to the token subject, giving per-tenant cost traces that a static shared key cannot.

A static key could achieve the same isolation only by issuing a different key per tenant and hoping they don’t reuse it cross-tenant. OAuth2 scopes make the boundary explicit and machine-enforceable.

7. Common pitfalls and tradeoffs

Clock skew: JWT validation breaks if your auth server and resource server drift. Use NTP and allow 30s leeway in leeway params.

Token storage: OAuth2 shifts risk to the token cache. Never log the token; treat refresh tokens like passwords. A leaked refresh token is worse than a leaked static key because it may mint new access tokens until revoked.

Scope creep: Developers request llm:* because it is easy. Review scopes in PRs; default to least privilege.

Confusing client secrets with API keys: A client secret in the authorization code flow identifies the app, not the user. Do not ship it in mobile binaries; use PKCE to remove the secret requirement.

Introspection overload: If you validate tokens by calling the auth server on every request, you add latency and a hard dependency. Cache JWKS or introspection responses with a short TTL.

Missing PKCE: Authorization code flow without PKCE is vulnerable to interception. Always use code_challenge for public clients.

Tradeoff: OAuth2 adds two network hops (token, introspect) and a dependency on an auth server. For a solo script, that overhead is pure tax. When to use OAuth2 instead of API keys is ultimately a question of how many independent principals touch the token.

8. Decision checklist

Walk this list top to bottom:

  1. Does a third party or end user delegate access? If yes, OAuth2. A static key cannot carry user consent.
  2. Need central revocation or short TTL? OAuth2 refresh tokens and expiring access tokens solve this; keys require manual rotation.
  3. Multiple tenants on one endpoint with per-tenant model limits? OAuth2 scopes map cleanly to tenant policies.
  4. Single owner, sealed network, no user context? Static API key is simpler and adequate.
  5. Frontend or mobile app directly calling LLM route? OAuth2 with PKCE; never embed a key.

If you answered OAuth2 on any but item 4, adopt the client credentials or PKCE flow and retire the shared key. The migration is mostly token acquisition code plus a middleware that validates the bearer.

9. Minimal validation middleware

For a FastAPI service, verifying a JWT bearer takes a few lines with python-jose. This guards your LLM route without trusting static strings.

from fastapi import Depends, HTTPException, Request
from jose import jwt, JWTError

def require_token(request: Request):
    auth = request.headers.get("Authorization", "")
    if not auth.startswith("Bearer "):
        raise HTTPException(401, "Missing bearer")
    token = auth.split()[1]
    try:
        claims = jwt.decode(token, "PUBLIC_KEY", algorithms=["RS256"],
                            audience="https://api.example.com")
        if "llm:chat" not in claims.get("scope", "").split():
            raise HTTPException(403, "Scope missing")
        return claims
    except JWTError:
        raise HTTPException(401, "Bad token")

Drop that dependency in front of /v1/chat/completions and you have replaced a static key check with scoped, expiring auth.

10. Migration path from keys to OAuth2

Do not flip the switch overnight. Support both for a transition window:

  1. Issue client credentials to existing key holders.
  2. Deploy bearer validation alongside x-api-key checks.
  3. Log which requests use which method.
  4. Deprecate keys after 30 days of zero key traffic.
  5. Remove the legacy middleware.

This lets you answer when to use OAuth2 instead of API keys in policy while keeping production stable.

11. Operational note

Rotate client secrets on the same schedule as you would API keys, but expect fewer fire drills. OAuth2 moves credential risk from long-lived strings to time-boxed tokens and explicit scopes. That is the practical line for when to use OAuth2 instead of API keys in any system touching user data or multiple tenants.

Tagsoauth2api-keysauthenticationguide

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All oauth2 & bearer token auth for llm platforms posts →