OAuth 2.0 and JWT Authentication Implementation Guide

The Difference Between Authentication and Authorization

Authentication is the process of verifying “Who are you?”, while authorization is the process of determining “What are you allowed to do?”. Using a hotel analogy, checking your ID at the front desk is authentication, and the keycard granting access only to specific floors/rooms is authorization.

ConceptAuthentication (AuthN)Authorization (AuthZ)
QuestionWho are you?What can you do?
TimingAt loginWhen accessing resources
ResultUser identification infoPermissions/roles
ExamplesID/PW, OAuthRBAC, ABAC

JWT (JSON Web Token) Structure

A JWT consists of three parts: Header.Payload.Signature

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.    ← Header (algorithm, type)
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik  ← Payload (claims, user info)
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQ  ← Signature (tamper protection)

JWT Creation and Verification

import jwt from "jsonwebtoken";

const JWT_SECRET = process.env.JWT_SECRET; // Load secret from environment variable

// Generate JWT tokens
function generateTokens(user) {
  // Access Token — short-lived (15 minutes)
  const accessToken = jwt.sign(
    {
      sub: user.id,           // User unique ID
      email: user.email,
      role: user.role,        // Permission info
      type: "access"
    },
    JWT_SECRET,
    { expiresIn: "15m" }      // Expires after 15 minutes
  );

  // Refresh Token — long-lived (7 days)
  const refreshToken = jwt.sign(
    {
      sub: user.id,
      type: "refresh"
    },
    JWT_SECRET,
    { expiresIn: "7d" }       // Expires after 7 days
  );

  return { accessToken, refreshToken };
}

// Verify JWT token
function verifyToken(token) {
  try {
    const decoded = jwt.verify(token, JWT_SECRET);
    return { valid: true, payload: decoded };
  } catch (error) {
    if (error.name === "TokenExpiredError") {
      return { valid: false, error: "TOKEN_EXPIRED" };
    }
    return { valid: false, error: "INVALID_TOKEN" };
  }
}

// Usage example
const user = { id: "user_123", email: "alice@example.com", role: "admin" };
const tokens = generateTokens(user);
console.log("Access:", tokens.accessToken);
// Output: Access: eyJhbGciOiJIUzI1NiIs...

const result = verifyToken(tokens.accessToken);
console.log("Verification:", result);
// Output: Verification: { valid: true, payload: { sub: 'user_123', ... } }

Middleware Implementation

// Express.js authentication middleware
function authMiddleware(req, res, next) {
  // Extract token from Authorization header
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
    return res.status(401).json({
      error: { code: "NO_TOKEN", message: "Authentication token is required." }
    });
  }

  const token = authHeader.split(" ")[1];
  const result = verifyToken(token);

  if (!result.valid) {
    const status = result.error === "TOKEN_EXPIRED" ? 401 : 403;
    return res.status(status).json({
      error: { code: result.error, message: "Token is invalid." }
    });
  }

  // Add verified user info to request object
  req.user = result.payload;
  next();
}

// Role-based authorization middleware
function requireRole(...roles) {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({
        error: { code: "FORBIDDEN", message: "Access denied." }
      });
    }
    next();
  };
}

// Apply to routes
app.get("/api/users", authMiddleware, (req, res) => {
  // Only authenticated users can access
  res.json({ users: [...] });
});

app.delete("/api/users/:id",
  authMiddleware,
  requireRole("admin"),  // Only admin role can access
  (req, res) => {
    // Only admins can delete users
    res.json({ message: "Deleted successfully" });
  }
);

OAuth 2.0 Flow

OAuth 2.0 is a protocol for delegating authentication to third-party services. Social logins like “Sign in with Google” are the classic example.

1. User -> Client: Clicks "Sign in with Google"
2. Client -> Google: Redirects to authentication page
3. User -> Google: Logs in + grants consent
4. Google -> Client: Returns Authorization Code (redirect)
5. Client Server -> Google: Requests Token with Code + Client Secret
6. Google -> Client Server: Issues Access Token + Refresh Token
7. Client Server -> Google API: Fetches user info with Access Token

Implementation Example (Express + Passport)

import express from "express";
import passport from "passport";
import { Strategy as GoogleStrategy } from "passport-google-oauth20";

// Configure Google OAuth strategy
passport.use(new GoogleStrategy(
  {
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: "/auth/google/callback"
  },
  async (accessToken, refreshToken, profile, done) => {
    // Look up or create user from Google profile
    let user = await findUserByGoogleId(profile.id);
    if (!user) {
      user = await createUser({
        googleId: profile.id,
        email: profile.emails[0].value,
        name: profile.displayName,
        avatar: profile.photos[0].value
      });
    }
    return done(null, user);
  }
));

const app = express();

// Start OAuth login — redirect to Google auth page
app.get("/auth/google",
  passport.authenticate("google", {
    scope: ["profile", "email"]  // Requested permission scope
  })
);

// OAuth callback — redirect after Google auth completes
app.get("/auth/google/callback",
  passport.authenticate("google", { session: false }),
  (req, res) => {
    // Issue JWT tokens
    const tokens = generateTokens(req.user);

    // Set Refresh Token as httpOnly cookie
    res.cookie("refresh_token", tokens.refreshToken, {
      httpOnly: true,       // Not accessible via JavaScript (XSS protection)
      secure: true,         // Only sent over HTTPS
      sameSite: "strict",   // CSRF protection
      maxAge: 7 * 24 * 60 * 60 * 1000  // 7 days
    });

    // Access Token in response body
    res.json({ accessToken: tokens.accessToken });
  }
);

Token Refresh Strategy

// Reissue Access Token using Refresh Token
app.post("/auth/refresh", (req, res) => {
  const refreshToken = req.cookies.refresh_token;

  if (!refreshToken) {
    return res.status(401).json({
      error: { code: "NO_REFRESH_TOKEN", message: "Please log in again." }
    });
  }

  const result = verifyToken(refreshToken);
  if (!result.valid || result.payload.type !== "refresh") {
    // Clear cookie and prompt re-login
    res.clearCookie("refresh_token");
    return res.status(401).json({
      error: { code: "INVALID_REFRESH", message: "Please log in again." }
    });
  }

  // Issue new token pair (Refresh Token Rotation)
  const user = { id: result.payload.sub };
  const tokens = generateTokens(user);

  // Update cookie with new Refresh Token
  res.cookie("refresh_token", tokens.refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: "strict",
    maxAge: 7 * 24 * 60 * 60 * 1000
  });

  res.json({ accessToken: tokens.accessToken });
});

Security Checklist

ItemRecommendationRisk
Access Token storageMemory (variable)localStorage is vulnerable to XSS
Refresh Token storagehttpOnly cookieNot accessible via JavaScript
Token lifetimeAccess: 15min, Refresh: 7 daysToo long increases theft risk
Signing algorithmRS256 (asymmetric) or HS256Never allow none algorithm
CORS settingsSpecify allowed domainsNever use * (wildcard)
HTTPSRequiredTokens can be stolen over HTTP

Practical Tips

  • Keep Access Tokens short, Refresh Tokens long: Even if an Access Token is stolen, it expires in 15 minutes.
  • Apply Refresh Token Rotation: Issuing a new pair on each Refresh Token use means a stolen token can only be used once.
  • Never put sensitive information in JWTs: The Payload is only Base64-encoded, not encrypted. Do not include passwords or personal IDs.
  • Maintain a token blacklist on logout: Since JWTs cannot be invalidated server-side, maintain a blacklist in Redis or similar stores.
  • Use PKCE (Proof Key for Code Exchange): Prevents authorization code interception attacks when using OAuth in SPAs or mobile apps.

Was this article helpful?