API security is no longer optional — it is the foundation of trust in every modern application. One leaked token or misconfigured OAuth2 flow can expose millions of user records, trigger account takeovers, or result in regulatory fines. This guide walks you through OAuth2 and JWT implementation correctly, including PKCE, token expiry strategy, replay attack prevention, and a full developer checklist.
1. Why API Security Is Non-Negotiable in 2026
APIs are the backbone of modern software. They power mobile apps, web platforms, AI services, and third-party integrations. But with this connectivity comes significant risk. According to industry reports, over 80% of data breaches in recent years involved compromised API credentials, stolen tokens, or misconfigured access flows.
Traditional session-based authentication — where the server stores a session ID and checks it on every request — does not scale well for microservices or distributed architectures. This is why the industry has largely moved to token-based authentication using OAuth2 as the authorization framework and JWT (JSON Web Token) as the token format.
Understanding how each works — and more importantly, where each can go wrong — is what separates a secure API from a vulnerable one.
OAuth2 and JWT are not the same thing. OAuth2 is an authorization framework. JWT is a token format. You can use JWT inside an OAuth2 flow — and in most modern applications, you should.
2. OAuth2 vs JWT — Complete Comparison
Before deciding how to protect your API, you need to understand what OAuth2 and JWT each do — and how they complement each other.
| Aspect | OAuth2 | JWT |
|---|---|---|
| What it is | Authorization framework | Token format (self-contained claims) |
| Statefulness | Can be stateful or stateless | Stateless by design |
| Token size | Usually small (opaque reference) | Larger (encodes all claims inline) |
| Revocation | Easier — revoke at server or with refresh token | Hard — requires blacklist or very short expiry |
| Best for | Third-party access, mobile apps, SPAs | Internal microservices, high-throughput APIs |
| Main security risk | Authorization code interception (mitigated by PKCE) | Token theft and replay attacks |
| Verification | Token introspection endpoint | Local signature verification (no network call) |
Use OAuth2 as your authorization framework and JWT as your token format. This combination gives you scalable, stateless verification with the ability to manage access grants through OAuth2's flows.
3. Implementing a Secure OAuth2 Authorization Flow
The recommended OAuth2 flow for most web and mobile applications is the Authorization Code Grant with PKCE (Proof Key for Code Exchange). PKCE prevents authorization code interception attacks — a real threat for public clients like SPAs and mobile apps that cannot store a client secret safely.
How the PKCE Flow Works
- The client generates a random
code_verifierstring. - It hashes it using SHA-256 to produce a
code_challenge. - The authorization request sends the
code_challengeto the authorization server. - On callback, the client sends the original
code_verifier. - The server verifies the hash matches — confirming the same client made both requests.
Python Example: Generating PKCE Parameters
# Python — generate PKCE code_verifier and code_challenge
import os
import hashlib
import base64
def generate_pkce_pair():
# Step 1: Generate a cryptographically random code_verifier (43–128 chars)
code_verifier = base64.urlsafe_b64encode(os.urandom(40)).rstrip(b'=').decode('utf-8')
# Step 2: Hash it with SHA-256
digest = hashlib.sha256(code_verifier.encode('utf-8')).digest()
# Step 3: Base64url-encode the hash to get code_challenge
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b'=').decode('utf-8')
return code_verifier, code_challenge
verifier, challenge = generate_pkce_pair()
print(f"code_verifier: {verifier}")
print(f"code_challenge: {challenge}")Node.js Example: Initiating the Authorization Request
# Node.js — build the OAuth2 authorization URL with PKCE
const crypto = require('crypto');
const querystring = require('querystring');
function generateCodeVerifier() {
return crypto.randomBytes(32).toString('base64url');
}
function generateCodeChallenge(verifier) {
return crypto
.createHash('sha256')
.update(verifier)
.digest('base64url');
}
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);
// Store codeVerifier securely in session
const authUrl = 'https://auth.yourdomain.com/authorize?' + querystring.stringify({
response_type: 'code',
client_id: process.env.CLIENT_ID,
redirect_uri: 'https://yourapp.com/callback',
scope: 'openid profile email',
state: crypto.randomBytes(16).toString('hex'),
code_challenge: codeChallenge,
code_challenge_method: 'S256',
});The state parameter is your CSRF protection. Generate it randomly, store it in the session, and reject any callback where it does not match what you sent.
4. JWT Best Practices and Code Implementation
A JWT consists of three Base64url-encoded parts separated by dots: header.payload.signature. The header declares the algorithm, the payload carries claims, and the signature ensures integrity.
Key JWT Best Practices
- Use RS256 (asymmetric), not HS256 (symmetric) — RS256 lets any service verify tokens using the public key without sharing the secret signing key.
- Set short expiry (
exp) — Access tokens should expire in 5–15 minutes. Longer lifetimes make stolen tokens dangerous. - Always validate
iss,aud, andexp— Never skip claim validation. - Never put sensitive data in the payload — JWTs are Base64-encoded, not encrypted. Anyone can decode the payload.
- Use refresh token rotation — Issue a new refresh token on every use, and invalidate the old one.
Python Example: Creating and Verifying a JWT
# Python — create and verify JWT with RS256 using PyJWT
import jwt
import datetime
from cryptography.hazmat.primitives import serialization
# create_access_token Example
def create_access_token(user_id: str, roles: list) -> str:
payload = {
'sub': user_id,
'roles': roles,
'iss': 'https://auth.yourdomain.com',
'aud': 'https://api.yourdomain.com',
'iat': datetime.datetime.utcnow(),
'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=15),
}
return jwt.encode(payload, private_key, algorithm='RS256')
# verify_access_token Example
def verify_access_token(token: str) -> dict:
try:
return jwt.decode(
token,
public_key,
algorithms=['RS256'],
audience='https://api.yourdomain.com',
issuer='https://auth.yourdomain.com',
)
except jwt.ExpiredSignatureError:
raise Exception("Token has expired")
except jwt.InvalidTokenError as e:
raise Exception(f"Invalid token: {e}")algorithm: 'none'Some older JWT libraries accept unsigned tokens if you pass alg: none. This is a critical vulnerability. Always explicitly specify and enforce your algorithm — and reject any token declaring none.
5. Common Vulnerabilities and How to Prevent Them
Token Replay Attacks
A replay attack happens when an attacker captures a valid JWT — via XSS, a man-in-the-middle attack, or a logging leak — and reuses it to call your API.
In a fintech SaaS platform, attackers captured JWTs via an XSS vulnerability. Because access tokens had a 24-hour expiry with no rotation, the breach remained active for nearly 18 hours. Reducing access token expiry to 15 minutes and implementing HttpOnly refresh token rotation eliminated the risk window.
Prevention Strategies
- Short access token expiry (5–15 min) — Limits the damage window if a token is stolen.
- Refresh token rotation — Issue a new refresh token on every use; invalidate the previous one.
- HttpOnly cookies for refresh tokens — JavaScript cannot read HttpOnly cookies, which eliminates the XSS attack vector.
- Jti (JWT ID) blacklist for critical operations — For high-value actions, store the JWT ID and reject reuse.
6. How to Test Your API Security Headers
After implementing OAuth2 and JWT, verify your security configuration headers:
Strict-Transport-Security— forces HTTPS onlyX-Content-Type-Options: nosniff— prevents MIME sniffingX-Frame-Options: DENY— prevents clickjackingContent-Security-Policy— controls resource loadingCache-Control: no-store— prevents sensitive responses from being cached
Test With curl
# Test your API's security headers with curl
curl -s -o /dev/null -D - \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
https://api.yourdomain.com/v1/profile
# Look for:
# strict-transport-security: max-age=63072000; includeSubDomains
# x-content-type-options: nosniff
# cache-control: no-store
# content-security-policy: default-src 'self'Test your API and JWT tokens directly in the browser
Use LearnHubly's free tools to validate and debug your tokens and API responses.
7. Developer Security Checklist
- Implementing Authorization Code flow with PKCE for all public clients
- Access tokens expire in 15 minutes or less
- Refresh tokens are rotated on every use
- Refresh tokens are stored in HttpOnly, Secure, SameSite=Strict cookies
- JWT signing algorithm is RS256 or ES256 (not HS256 for distributed systems)
- Server explicitly validates
iss,aud, andexpon every request - Algorithm is hardcoded on the server — not trusted from the token header
- No sensitive data (PII, passwords, secrets) in the JWT payload
- State parameter is used and validated to prevent CSRF in OAuth2 flows
- All API traffic is served over HTTPS with HSTS enabled
- Security headers (CSP, X-Frame-Options, nosniff) are present on all responses
- Token revocation strategy exists for critical events (logout, account compromise)
- Rate limiting is applied to the token endpoint
8. Frequently Asked Questions
Should I use OAuth2 or JWT?
Use both together. OAuth2 handles the authorization flow; JWT is the token format used to carry and verify that grant.
Is JWT secure by default?
No. A JWT is only as secure as its implementation. Security comes from correct configuration, not the format itself.
What is the difference between access tokens and refresh tokens?
An access token is short-lived and sent with every API request. A refresh token is longer-lived and used only to obtain a new access token when the current one expires.
9. Conclusion
OAuth2 and JWT provide a robust foundation for modern API security when implemented correctly. Misconfiguration—tokens that live too long, unvalidated algorithms, or secrets hardcoded into source code—is the root of most breaches. Short-lived tokens, PKCE, RS256 signing, and HttpOnly cookies for refresh tokens are the benchmarks of a professional engineering approach.
JWT vs Session Cookies for Microservices (2026) · Securing Your Web Applications: Essential Tools · REST API Complete Guide