If I had to cut API security down to a few steps, I’d say this: use short-lived tokens, check every JWT on every request, pick the right OAuth2 flow, and protect both browser and service-to-service traffic.
Here’s the plain-English version:
- OAuth2 decides how access is granted
- JWT carries who the caller is and what they can do
- APIs should check signature,
iss,aud,exp, and scopes every time - For web and mobile apps, use Authorisation Code Flow with PKCE
- For service-to-service calls, use Client Credentials
- Keep access tokens short-lived, usually 5 to 15 minutes
- For browsers, store tokens in
HttpOnlycookies, notlocalStorage - For internal traffic, use TLS, and for higher-risk routes use mTLS or DPoP
- Gateways, app code, and internal services should all enforce the same checks
- Watch for common mistakes like
decode()without verification, missingaudchecks, and long-lived tokens
A few numbers stand out. The article points to 5–15 minute token lifetimes, 10–30 seconds of clock-skew tolerance, and alerts when failed validation jumps above 100 attempts per minute from one IP.
My takeaway: if you want a clean setup, don’t rely on one control. You need token checks, short lifetimes, safe storage, careful key handling, and clear monitoring working together.
This article then walks through those choices in a simple order: validation, flow selection, storage, gateways, app libraries, and internal service security.
::: @figure
{OAuth2 & JWT API Security: End-to-End Flow}
:::
Securing Your APIs with OAuth 2.0 - API Days
Need help optimizing your cloud costs?
Get expert advice on how to reduce your cloud expenses without sacrificing performance.
Core OAuth2 and JWT patterns for securing APIs

Start with the checks every API needs to enforce. First validate tokens. Then pick the right OAuth2 flow. Then lock down storage and transport.
Validating tokens on every request
Every API endpoint must validate the incoming JWT on each request. In plain terms, that means checking the cryptographic signature, the issuer (iss), the audience (aud), the expiry (exp), and the granted scopes.
Signature verification should use the authorisation server’s public key from a JWKS endpoint. Cache those keys for a few minutes so you’re not making needless network calls on every request. But the cache also needs to deal with key rotation properly by using the kid (Key ID) header [7][9].
Be strict about signing algorithms. Allowlist RS256, PS256, or ES256. Never accept the none algorithm, and reject HS256 in distributed systems where a shared symmetric secret across services adds risk. Put simply: pin the accepted signing algorithm and do not allow downgrades [12].
The audience check is easy to overlook, but it matters. Without it, a valid token meant for one service could be replayed against another. It also helps to allow a small clock skew tolerance of 10–30 seconds on time-based claims, which avoids false rejections when servers are slightly out of sync [9][6].
| Check | What it prevents |
|---|---|
| Signature | Tampered or forged tokens |
Issuer (iss) |
Tokens from untrusted identity providers |
Audience (aud) |
Token replay across different services |
Expiry (exp) |
Stolen tokens remaining valid indefinitely |
| Scopes | Over-privileged access at the route level |
Use verified token middleware, not raw decode(). decode() only parses the token; it does not verify the signature. Always use verify() or the matching validated middleware instead [5].
These checks are the baseline for enforcement at both gateway and service level.
Choosing the right OAuth2 flow for web, mobile and back-end services
Different clients need different flows. There isn’t a one-size-fits-all setup here.
Use Authorisation Code Flow with PKCE for browser and mobile clients. PKCE stops authorisation code interception by tying the code to a one-time verifier that only the proper client holds. Under OAuth 2.1, PKCE is mandatory for all user-facing applications.
For machine-to-machine communication, use the Client Credentials Flow. This fits cases like a background job calling an internal API, or one microservice calling another. There’s no user in the middle, so there’s no authorisation code step. The service authenticates straight to the authorisation server with its client ID and secret, then gets a scoped access token.
Drop Implicit Flow and ROPC. Both are deprecated in OAuth 2.1.
Refresh tokens let users renew access without signing in again. But don’t stop there. Use refresh token rotation, where each use invalidates the old token and issues a new one. If a stolen refresh token is replayed, the server can spot the reuse and invalidate the whole session [3][9][6].
Once you’ve picked the flow, storage and transport become the next place where things often go wrong.
Storing and transporting tokens safely
Access tokens should be short-lived, usually 5 to 15 minutes [5][10]. That keeps the blast radius smaller if a token is intercepted.
For browser clients, don’t store tokens in localStorage. Use HttpOnly cookies with Secure and SameSite=Strict instead [6][8][9]. If a single-page application handles sensitive scopes, the Backend-for-Frontend (BFF) pattern goes a step further. A server-side layer handles the token exchange, which keeps tokens out of the browser altogether [9][6].
For services, avoid static environment variables for client secrets. They can show up in process listings and crash dumps. Use a secrets manager such as HashiCorp Vault to issue dynamic, short-lived credentials instead [3].
Use TLS for all token exchange and API calls, and send tokens in the Authorization: Bearer header. Never put them in query strings. Query parameters can leak into server logs, browser history, and Referer headers [11][8][9]. It’s also smart to strip Authorization headers from application logs and APM traces before anything is written to storage.
With token handling fixed, the next step is enforcing it in tools and frameworks.
Tools and frameworks for OAuth2 and JWT
Most setups use three layers: an identity provider that issues tokens, a gateway that checks policy at the edge, and app-level libraries that handle finer checks inside each service. From there, the main choice is simple: which tools should enforce those checks at the edge, and which should do it inside each service?
Identity and token services
Keycloak is an open-source identity provider for OAuth2, OIDC and SAML. It issues JWTs and publishes JWKS for verification [17][19].
Auth0 and Okta are cloud-based identity services. Auth0 leans into passwordless authentication, while Okta offers a central directory with adaptive multi-factor authentication [19].
Kong Identity can issue tokens with contextual claims for machine-to-machine access [15].
API gateways for edge enforcement
At the edge, gateways apply the same token rules before traffic gets anywhere near application code. They check the JWT signature, expiry, issuer and audience before requests reach services [13][20].
That edge layer should also clean up inbound identity data. Strip the incoming Authorization header and any untrusted identity headers. Then inject only headers you trust, such as X-User-ID or X-Roles [13].
Common gateways here include Kong, Tyk and Apigee. They overlap in many areas, but the details matter:
| Feature | Kong Gateway | Tyk Gateway |
|---|---|---|
| OAuth2 support | Via OIDC and JWT plugins; supports introspection [13][15] | Built-in authorisation server; supports Auth Code, Client Credentials, and Password grants [14] |
| JWT validation | Local validation via JWKS; caches public keys [13] | Validates signatures and claims; maps claims to security policies [16] |
| mTLS support | Supported via plugins and service mesh integration [15][18] | Supported; can enforce mTLS for all traffic [16] |
| Integration model | Plugin-based (OIDC, Upstream OAuth) [15][18] | Native API definition settings or Tyk Vendor Extensions [14][16] |
| Operational fit | Hybrid, cloud, and serverless deployments [18] | On-premises or cloud; supports OAS and Classic API definitions [14][16] |
One practical note: Kong's Upstream OAuth plugin is an Enterprise-only feature [18].
Application frameworks and libraries
Internal services should still validate tokens for themselves. Spring Security handles fine-grained RBAC or ABAC in Java services, including method-level scope and role checks, and it works with JOSE libraries such as Nimbus JOSE [3].
In other languages, the pattern stays much the same. Use JOSE libraries, then enforce scope or role checks at the route or method level [3].
Those service-level checks also carry over into internal API calls and transport-layer controls. Together, the edge layer and service layer set up the next step: service-to-service authentication.
Securing inter-service communication in microservices
Once the edge is locked down, internal traffic becomes the next weak spot. East-west calls are often trusted by default, and that makes lateral movement much easier.
Using client credentials and service-issued JWTs between services
For service-to-service calls, start with machine identities, not user sessions. Use the Client Credentials Grant for machine-to-machine traffic, then validate signed JWTs locally with cached keys. The same checks used at the edge still apply here: issuer, audience, expiry, and signature [3][17][22].
Keep access token TTLs short. In production, a common target is 5 to 15 minutes [3][4]. That keeps the blast radius smaller if a token leaks.
JWT, mTLS or both for internal traffic
Token validation handles authorisation. Transport security deals with interception and replay. Internal calls should not get a free pass just because they sit on a private network. These controls cover different jobs:
| Pattern | Identity Type | Encryption | Authorisation Granularity | Key Management Effort |
|---|---|---|---|---|
| JWT-only | User/Application | None (requires TLS) | Fine-grained (scopes, roles) | Medium (public keys via JWKS) |
| mTLS-only | Machine/Workload | Transport-layer | Coarse (service-level) | High (internal CA, cert rotation) |
| Combined (JWT + mTLS) | Both | Full encryption + signed claims | Fine-grained + proof of possession | High |
JWT + mTLS gives the strongest control, but it comes with real operational overhead. If you're rotating certificates by hand, you're asking for trouble. Automating rotation with tools like cert-manager in Kubernetes - usually on a 90-day cycle - helps prevent outages caused by manually managed certificates [3][21].
If a full internal PKI isn't a good fit, DPoP (RFC 9449) gives you application-layer proof-of-possession without the need for a shared certificate authority [23].
Gateway and service mesh patterns
Once you've picked the trust model, apply it the same way at the gateway and inside the cluster. If downstream services need user context, pass through the original user JWT instead of swapping it for a generic service token. That keeps audit trails intact and lets downstream services make fine-grained authorisation decisions [2][3].
For east-west traffic inside a cluster, a service mesh such as Istio can handle mTLS between pods automatically. That means each service doesn't have to manage its own certificates. Set Istio to STRICT mode and it will reject any non-mTLS connections in the production namespace, which enforces zero-trust at the transport layer [23][3].
Then add per-service AuthorizationPolicy rules with a default-deny stance. In plain English: nothing talks unless you say it can [3].
In hybrid or managed setups, keep the split simple: the gateway handles external traffic, and the mesh enforces east-west policy.
Controls, risk reduction and conclusion
Key management, monitoring and common misconfigurations
Once edge and service checks are in place, the main risk moves to key handling, revocation, and monitoring. This is where day-to-day controls matter. OAuth2 and JWT can look solid at launch, then drift into trouble if these checks aren't kept tight.
Use short access-token lifetimes and a low-latency deny list for immediate revocation [2][5][24].
Log failed validation events and set alerts for sudden spikes from a single IP. If you see more than 100 failed attempts per minute, treat it as brute-force activity [5]. In practice, the most common problems are operational rather than cryptographic.
| Risk / Issue | Mitigation |
|---|---|
Algorithm confusion (alg: none or the wrong signing algorithm) |
Allowlist specific algorithms, such as RS256; reject alg: none; always use jwt.verify(), never jwt.decode() [5][24]
|
| Long-lived tokens | Keep access token lifetimes short [6][5] |
| Over-broad scopes | Define granular scopes; validate them at the service level, not just the gateway [1][24] |
Missing aud / iss validation |
Validate aud and iss on every request [6][13]
|
| Replay attacks | Use DPoP or mTLS for high-risk internal routes [1][2][24] |
These controls sit above the gateway and service mesh. Put simply, they help keep the token model steady in production.
Conclusion: the shortest path to a defensible API security posture
The shortest path to a defensible model is simple: use short-lived tokens, apply strict claim checks, define granular scopes, and monitor continuously. Then apply those checks the same way at the edge, across internal services, and at the transport layer.
FAQs
When should I use JWTs instead of token introspection?
Use JWTs for stateless, high-performance setups such as microservices, where each service needs to check identity on its own without repeated database lookups or network calls to an identity provider. They also work well when the token needs to carry claims that a service can read straight away.
Use token introspection when you need central, immediate revocation, or when you're using opaque tokens in high-security environments where an extra network call is an acceptable trade-off.
How often should I rotate signing keys and client secrets?
For production security, rotate signing keys at least every quarter. The safest way to handle this is to automate rotation with a JWKS endpoint. That lets verifiers accept both the old and new keys during a transition window, often 24 hours, so in-flight token TTLs are still covered.
For client secrets and API keys, take the same approach. Use automated rotation, allow both old and new credentials for a fixed period such as 24 hours, and then retire the old secret.
Do all internal APIs need both mTLS and JWT validation?
No. Not all internal APIs need both. It depends on your threat model.
That said, using both is a common production pattern for defence in depth.
mTLS verifies the calling service at the transport layer. JWTs, meanwhile, carry user identity and authorisation claims at the application layer.
If you rely on mTLS alone, there’s a gap: it tells you which service is calling, but not which user is behind the request. It also doesn’t confirm that a given request is authorised for that user.