JSON API Authentication: OAuth 2.0, JWT & Best Practices 2026
Complete guide to securing JSON APIs with OAuth 2.0, JWT tokens, API keys, and modern authentication patterns. Includes code examples for Node.js, Express, and frontend integration.
Michael Rodriguez
• API & Security EngineerMichael is an API engineer and security specialist with over 7 years of experience building RESTful services, data conversion pipelines, and authentication systems. He writes practical guides on JSON Web Tokens, API debugging strategies, data science applications of JSON, and modern AI tooling workflows including MCP and JSON-RPC.
# JSON API Authentication: OAuth 2.0, JWT & Best Practices 2026
Securing JSON APIs is critical for protecting user data and preventing unauthorized access. This comprehensive guide covers OAuth 2.0, JWT (JSON Web Tokens), API keys, session-based auth, and production-ready implementation patterns.
Table of Contents
---
Authentication vs Authorization
Authentication: Verifying who the user is (login, credentials) Authorization: Determining what the user can access (permissions, roles)// Authentication: Is this user who they claim to be?
const user = await verifyCredentials(email, password);
// Authorization: Can this user perform this action?
if (user.role !== 'admin') {
throw new ForbiddenError('Admin access required');
}
---
API Keys
Simple authentication for server-to-server or low-risk scenarios.
Generating API Keys
import crypto from 'crypto';
function generateApiKey(): string {
return crypto.randomBytes(32).toString('hex');
}
// Example: sk_test_YOUR_API_KEY_HERE
const apiKey = sk_live_${generateApiKey()};
Storing API Keys (Hashed)
import bcrypt from 'bcrypt';
async function createApiKey(userId: string) {
const apiKey = generateApiKey();
const hashedKey = await bcrypt.hash(apiKey, 10);
await db.apiKeys.create({
userId,
keyHash: hashedKey,
keyPrefix: apiKey.substring(0, 12), // For identification
createdAt: new Date()
});
// Return plain key only once
return apiKey;
}
async function verifyApiKey(providedKey: string): Promise<User | null> {
const keys = await db.apiKeys.findAll();
for (const key of keys) {
const isValid = await bcrypt.compare(providedKey, key.keyHash);
if (isValid) {
return await db.users.findById(key.userId);
}
}
return null;
}
API Key Middleware (Express)
import { Request, Response, NextFunction } from 'express';
async function apiKeyAuth(req: Request, res: Response, next: NextFunction) {
const apiKey = req.headers['x-api-key'] as string;
if (!apiKey) {
return res.status(401).json({ error: 'API key required' });
}
const user = await verifyApiKey(apiKey);
if (!user) {
return res.status(401).json({ error: 'Invalid API key' });
}
req.user = user;
next();
}
// Usage
app.get('/api/protected', apiKeyAuth, (req, res) => {
res.json({ message: 'Access granted', user: req.user });
});
Client Usage
const response = await fetch('https://api.example.com/data', {
headers: {
'X-API-Key': 'your_api_key_here'
}
});
const data = await response.json();
---
Session-Based Authentication
Traditional cookie-based sessions with server-side storage.
Login Endpoint
import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';
const redisClient = createClient({ url: 'redis://localhost:6379' });
await redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET!,
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // Prevent XSS
maxAge: 24 60 60 1000 // 24 hours
}
}));
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
const user = await db.users.findByEmail(email);
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
req.session.userId = user.id;
res.json({
status: 'success',
user: { id: user.id, email: user.email, name: user.name }
});
});
Protected Route Middleware
function requireAuth(req: Request, res: Response, next: NextFunction) {
if (!req.session.userId) {
return res.status(401).json({ error: 'Authentication required' });
}
next();
}
app.get('/api/profile', requireAuth, async (req, res) => {
const user = await db.users.findById(req.session.userId!);
res.json({ user });
});
Logout
app.post('/api/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: 'Logout failed' });
}
res.json({ status: 'success' });
});
});
---
JSON Web Tokens (JWT)
Stateless authentication with signed tokens.
JWT Structure
header.payload.signature
Header:
{
"alg": "HS256",
"typ": "JWT"
}
Payload:
{
"sub": "user123",
"email": "alice@example.com",
"role": "user",
"iat": 1710000000,
"exp": 1710086400
}
Creating & Verifying JWTs
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET!;
const JWT_EXPIRES_IN = '7d';
interface JwtPayload {
sub: string;
email: string;
role: string;
}
function createToken(user: User): string {
const payload: JwtPayload = {
sub: user.id,
email: user.email,
role: user.role
};
return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
}
function verifyToken(token: string): JwtPayload {
try {
return jwt.verify(token, JWT_SECRET) as JwtPayload;
} catch (error) {
throw new Error('Invalid token');
}
}
JWT Authentication Middleware
async function jwtAuth(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.substring(7);
try {
const payload = verifyToken(token);
req.user = await db.users.findById(payload.sub);
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
// Usage
app.get('/api/me', jwtAuth, (req, res) => {
res.json({ user: req.user });
});
Login with JWT
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
const user = await db.users.findByEmail(email);
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = createToken(user);
res.json({
status: 'success',
token,
user: { id: user.id, email: user.email, name: user.name }
});
});
Refresh Tokens
interface RefreshTokenPayload {
sub: string;
type: 'refresh';
}
function createRefreshToken(userId: string): string {
return jwt.sign(
{ sub: userId, type: 'refresh' } as RefreshTokenPayload,
JWT_SECRET,
{ expiresIn: '30d' }
);
}
app.post('/api/login', async (req, res) => {
// ... authentication logic ...
const accessToken = createToken(user);
const refreshToken = createRefreshToken(user.id);
// Store refresh token in database
await db.refreshTokens.create({
userId: user.id,
token: refreshToken,
expiresAt: new Date(Date.now() + 30 24 60 60 1000)
});
res.json({
status: 'success',
accessToken,
refreshToken,
user: { id: user.id, email: user.email }
});
});
app.post('/api/refresh', async (req, res) => {
const { refreshToken } = req.body;
try {
const payload = jwt.verify(refreshToken, JWT_SECRET) as RefreshTokenPayload;
if (payload.type !== 'refresh') {
throw new Error('Invalid token type');
}
// Check if refresh token exists in database
const storedToken = await db.refreshTokens.findOne({
userId: payload.sub,
token: refreshToken
});
if (!storedToken) {
throw new Error('Token not found or revoked');
}
const user = await db.users.findById(payload.sub);
const newAccessToken = createToken(user);
res.json({ accessToken: newAccessToken });
} catch (error) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
});
---
OAuth 2.0
Delegated authorization (e.g., "Sign in with Google").
OAuth 2.0 Flows
Authorization Code Flow (most secure for web apps):Implementation with Passport.js
npm install passport passport-google-oauth20
import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
callbackURL: 'http://localhost:3000/auth/google/callback'
}, async (accessToken, refreshToken, profile, done) => {
// Find or create user
let user = await db.users.findByEmail(profile.emails![0].value);
if (!user) {
user = await db.users.create({
email: profile.emails![0].value,
name: profile.displayName,
googleId: profile.id
});
}
done(null, user);
}));
// Routes
app.get('/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
app.get('/auth/google/callback',
passport.authenticate('google', { session: false }),
(req, res) => {
const token = createToken(req.user as User);
res.redirect(http://localhost:3001/auth/callback?token=${token});
}
);
Frontend Integration
// Redirect to OAuth flow
function handleGoogleLogin() {
window.location.href = 'http://localhost:3000/auth/google';
}
// Handle callback
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
if (token) {
localStorage.setItem('accessToken', token);
// Fetch user data
fetchUserProfile(token);
}
---
OpenID Connect (OIDC)
OAuth 2.0 + identity layer. Provides standardized user info.
OIDC with Auth0
import { auth } from 'express-openid-connect';
app.use(auth({
authRequired: false,
auth0Logout: true,
baseURL: 'http://localhost:3000',
clientID: process.env.AUTH0_CLIENT_ID,
issuerBaseURL: process.env.AUTH0_ISSUER_BASE_URL,
secret: process.env.AUTH0_SECRET
}));
// Protected route
app.get('/api/profile', requiresAuth(), (req, res) => {
res.json({ user: req.oidc.user });
});
---
Security Best Practices
1. HTTPS Everywhere
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https' && process.env.NODE_ENV === 'production') {
return res.redirect(https://${req.header('host')}${req.url});
}
next();
});
2. Secure Cookie Settings
app.use(session({
cookie: {
secure: true, // HTTPS only
httpOnly: true, // No JavaScript access
sameSite: 'strict', // CSRF protection
maxAge: 24 60 60 1000
}
}));
3. CORS Configuration
import cors from 'cors';
app.use(cors({
origin: process.env.FRONTEND_URL,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
4. Password Hashing
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 12;
async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
5. Token Rotation
// Blacklist old tokens
const tokenBlacklist = new Set<string>();
app.post('/api/logout', jwtAuth, async (req, res) => {
const token = req.headers.authorization!.substring(7);
tokenBlacklist.add(token);
// Also revoke refresh token
await db.refreshTokens.deleteMany({ userId: req.user.id });
res.json({ status: 'success' });
});
// Check blacklist in middleware
function jwtAuth(req, res, next) {
const token = req.headers.authorization?.substring(7);
if (tokenBlacklist.has(token)) {
return res.status(401).json({ error: 'Token revoked' });
}
// ... rest of verification ...
}
---
Rate Limiting & Throttling
Express Rate Limit
import rateLimit from 'express-rate-limit';
const loginLimiter = rateLimit({
windowMs: 15 60 1000, // 15 minutes
max: 5, // 5 attempts
message: { error: 'Too many login attempts, try again later' }
});
app.post('/api/login', loginLimiter, async (req, res) => {
// ... login logic ...
});
const apiLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per minute
standardHeaders: true,
legacyHeaders: false
});
app.use('/api/', apiLimiter);
Custom Rate Limiter with Redis
async function rateLimitMiddleware(req: Request, res: Response, next: NextFunction) {
const key = ratelimit:${req.ip};
const limit = 100;
const window = 60; // seconds
const current = await redisClient.incr(key);
if (current === 1) {
await redisClient.expire(key, window);
}
if (current > limit) {
return res.status(429).json({
error: 'Rate limit exceeded',
retryAfter: await redisClient.ttl(key)
});
}
res.setHeader('X-RateLimit-Limit', limit.toString());
res.setHeader('X-RateLimit-Remaining', (limit - current).toString());
next();
}
---
Conclusion
Secure your JSON APIs with the right authentication strategy:
Choose based on use case:- API Keys: Server-to-server, simple integration
- Sessions: Traditional web apps with server-side rendering
- JWT: Stateless, mobile apps, microservices
- OAuth 2.0: Third-party integrations, social login
- OIDC: Enterprise SSO, identity federation
- HTTPS everywhere
- Rate limiting
- Password hashing
- Token expiration and rotation
- CORS configuration
- Input validation
Security is not optional—build it in from day one.
Related Resources
Related Articles
JSON Security Vulnerabilities: Complete Protection Guide 2026
Protect your APIs from JSON security vulnerabilities. Learn about injection attacks, prototype pollution, DoS attacks, and implement security best practices for safe JSON processing in production applications.
JSON APIs and REST Services: Complete Development Guide
Learn to build and consume JSON-based REST APIs. Covers HTTP methods, authentication, best practices, and real-world implementation examples.
JSON in Node.js: Complete Guide 2026
Master JSON handling in Node.js with streaming, parsing, validation, and performance optimization. Learn fs.readFile, streams, error handling, and production best practices with real examples.