← Back to Blog

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 Rodriguez16 min readadvanced
M

Michael Rodriguez

API & Security Engineer

Michael 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.

REST APIsJWT & SecurityData ScienceJSON PathMCP / AI ToolingAPI Debugging
16 min read

# 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
  • API Keys
  • Session-Based Authentication
  • JSON Web Tokens (JWT)
  • OAuth 2.0
  • OpenID Connect (OIDC)
  • Security Best Practices
  • Rate Limiting & Throttling
  • ---

    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):
  • User clicks "Sign in with Google"
  • Redirect to Google with client_id and redirect_uri
  • User authorizes
  • Google redirects back with authorization code
  • Exchange code for access token
  • Use access token to fetch user data
  • 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();

    });

    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

    Always implement:
    • HTTPS everywhere
    • Rate limiting
    • Password hashing
    • Token expiration and rotation
    • CORS configuration
    • Input validation

    Security is not optional—build it in from day one.

    Share:

    Related Articles