DEV Community

Atlas Whoff
Atlas Whoff

Posted on

OAuth 2.0 and JWT: How Authentication Actually Works

OAuth 2.0 and JWT: How Authentication Actually Works

Every developer uses OAuth and JWTs but few understand the flow. Here's the full picture — without the hand-waving.

OAuth 2.0 Authorization Code Flow

User clicks 'Sign in with Google'
       ↓
Your app redirects to Google:
  accounts.google.com/o/oauth2/auth?
    client_id=YOUR_ID
    redirect_uri=https://yourapp.com/callback
    response_type=code
    scope=openid email profile
    state=RANDOM_STRING (CSRF protection)
       ↓
Google shows consent screen
User approves
       ↓
Google redirects to your callback:
  yourapp.com/callback?code=AUTH_CODE&state=RANDOM_STRING
       ↓
Your server exchanges code for tokens:
  POST accounts.google.com/o/oauth2/token
  { code, client_id, client_secret, redirect_uri }
       ↓
Google returns:
  { access_token, refresh_token, id_token (JWT) }
       ↓
You decode id_token to get user info
Create/login user in your database
Issue your own session
Enter fullscreen mode Exit fullscreen mode

What Is a JWT

// A JWT is three base64url-encoded parts separated by dots:
// header.payload.signature

const header = { alg: 'HS256', typ: 'JWT' };

const payload = {
  sub: 'user_123',       // Subject (user ID)
  email: 'user@test.com',
  iat: 1700000000,        // Issued at (Unix timestamp)
  exp: 1700086400,        // Expires at
};

// Signature = HMAC-SHA256(base64(header) + '.' + base64(payload), SECRET)
// Anyone can decode the payload — it's not encrypted
// But only the server can verify the signature
Enter fullscreen mode Exit fullscreen mode

Creating and Verifying JWTs

import { SignJWT, jwtVerify } from 'jose';

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

// Create
async function createToken(userId: string): Promise<string> {
  return new SignJWT({ sub: userId })
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('24h')
    .sign(secret);
}

// Verify
async function verifyToken(token: string) {
  try {
    const { payload } = await jwtVerify(token, secret);
    return payload;
  } catch {
    return null; // Expired, tampered, or invalid
  }
}
Enter fullscreen mode Exit fullscreen mode

JWT vs Database Sessions

JWT Database Session
Storage Client (cookie/localStorage) Server DB
Revocation Hard (must wait for expiry) Easy (delete row)
Scale No DB lookup per request DB lookup per request
Best for Stateless APIs, microservices Web apps needing instant revocation

Refresh Token Pattern

// Short-lived access token + long-lived refresh token
const accessToken = await createToken(userId, '15m');   // Expires fast
const refreshToken = await createToken(userId, '30d');  // Lives longer

// Store refresh token in httpOnly cookie (not localStorage)
res.cookie('refreshToken', refreshToken, {
  httpOnly: true,  // JS can't read it
  secure: true,    // HTTPS only
  sameSite: 'lax', // CSRF protection
  maxAge: 30 * 24 * 60 * 60 * 1000,
});

// /api/refresh endpoint issues new access token using refresh token
Enter fullscreen mode Exit fullscreen mode

PKCE (For Single-Page Apps)

// Generate code verifier and challenge
const codeVerifier = crypto.randomUUID() + crypto.randomUUID();
const codeChallenge = btoa(
  String.fromCharCode(...new Uint8Array(
    await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier))
  ))
).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');

// Include code_challenge in authorization URL
// Include code_verifier when exchanging code for tokens
// Prevents auth code interception attacks
Enter fullscreen mode Exit fullscreen mode

Auth with JWT and OAuth ships pre-configured in the AI SaaS Starter Kit — Google + GitHub OAuth, session management, and protected routes. $99 at whoffagents.com.

Top comments (0)