Skip to content
Published on

OAuth 2.0 & Authentication Complete Guide 2025: JWT, Sessions, SSO, OIDC, Passkey

Authors

Introduction

In web applications, "Authentication" and "Authorization" are the cornerstones of security. From user login to API access control and inter-microservice communication, you cannot build a secure system without a proper authentication scheme.

As of 2025, the authentication ecosystem is evolving rapidly. From traditional session-based authentication to JWT token-based authentication, OAuth 2.0, OIDC, and Passkey, the choices are more diverse than ever.

This guide covers everything about web authentication, from the fundamental difference between Authentication vs Authorization, through JWT deep dive, all OAuth 2.0 flows, SSO, Passkey, to security best practices.


1. Authentication vs Authorization

1.1 Core Difference

Authentication - "Who are you?"
- The process of verifying a user's identity
- The login process itself
- Examples: username/password, biometrics, OTP

Authorization - "What are you allowed to do?"
- The process of checking an authenticated user's permissions
- Resource access control
- Examples: only admins can delete, users can only view their own data

1.2 Authentication/Authorization Flow

1. User sends login request (Authentication)
   POST /auth/login
   Body: { "email": "user@example.com", "password": "..." }

2. Server verifies credentials (Authentication)
   - Look up user in DB
   - Compare password hash
   - Issue token/session on success

3. Include token/session with API requests
   GET /admin/users
   Authorization: Bearer eyJhbGci...

4. Server checks permissions (Authorization)
   - Extract role from token
   - Verify access to endpoint
   - Has permission -> 200 OK
   - No permission -> 403 Forbidden

1.3 HTTP Status Codes

401 Unauthorized = Authentication failure
- "I don't know who you are. Please log in."
- Missing token, expired token, invalid credentials

403 Forbidden = Authorization failure
- "I know who you are, but you don't have access to this resource."
- Regular user calling admin-only API

2. Session-based Authentication

2.1 How It Works

1. Client: POST /login (email, password)
2. Server: Validates credentials
3. Server: Creates session (stored in server memory/Redis)
   Session ID: "sess_abc123"
   Data: { userId: 42, role: "admin", createdAt: "..." }
4. Server: Returns session ID via Set-Cookie header
   Set-Cookie: session_id=sess_abc123; HttpOnly; Secure; SameSite=Strict
5. Client: Automatically sends cookie with every subsequent request
   Cookie: session_id=sess_abc123
6. Server: Looks up user info using session ID

2.2 Express.js Session Implementation

const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const redis = require('redis');

const app = express();
const redisClient = redis.createClient({ url: 'redis://localhost:6379' });

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: true,        // Only sent over HTTPS
    httpOnly: true,      // Block JavaScript access (XSS prevention)
    sameSite: 'strict',  // CSRF prevention
    maxAge: 24 * 60 * 60 * 1000  // 24 hours
  }
}));

// Login
app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await db.findUserByEmail(email);

  if (!user || !await bcrypt.compare(password, user.passwordHash)) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  req.session.userId = user.id;
  req.session.role = user.role;
  res.json({ message: 'Login successful' });
});

// Authentication middleware
function requireAuth(req, res, next) {
  if (!req.session.userId) {
    return res.status(401).json({ error: 'Authentication required' });
  }
  next();
}

// Authorization middleware
function requireRole(role) {
  return (req, res, next) => {
    if (req.session.role !== role) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    next();
  };
}

// Logout
app.post('/logout', (req, res) => {
  req.session.destroy((err) => {
    res.clearCookie('connect.sid');
    res.json({ message: 'Logout successful' });
  });
});

2.3 Session Store Comparison

StoreProsConsBest For
MemoryFastestLost on restart, not sharedDevelopment
RedisFast, shared, TTL supportExtra infrastructureProduction (recommended)
DB (PostgreSQL)Persistent, auditableSlowAudit requirements
FileSimple implementationSlow, not sharedSmall apps

3. Token-based Authentication

3.1 JWT Structure

JWT = Header.Payload.Signature (Base64URL encoded, separated by dots)

Header:
{
  "alg": "RS256",    // Signing algorithm
  "typ": "JWT",      // Token type
  "kid": "key-id-1"  // Key identifier (for Key Rotation)
}

Payload (Claims):
{
  "iss": "https://auth.example.com",   // Issuer
  "sub": "user-123",                    // Subject
  "aud": "https://api.example.com",    // Audience
  "exp": 1711356600,                    // Expiration
  "iat": 1711353000,                    // Issued At
  "nbf": 1711353000,                    // Not Before
  "jti": "unique-token-id",             // JWT ID
  "email": "user@example.com",          // Custom claim
  "roles": ["admin", "user"]            // Custom claim
}

Signature:
RS256(
  base64url(header) + "." + base64url(payload),
  privateKey
)

3.2 Signing Algorithm Comparison

AlgorithmTypeKeyUse Cases
HS256SymmetricSingle secret keySingle server, internal services
RS256AsymmetricPublic/private key pairMicroservices, external verification
ES256Asymmetric (ECDSA)Shorter key lengthMobile, IoT (performance critical)
// HS256 - Sign and verify with the same key
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET;

// Sign
const token = jwt.sign({ sub: 'user-123' }, SECRET, { algorithm: 'HS256' });

// Verify (same key required)
const decoded = jwt.verify(token, SECRET);
// RS256 - Sign with private key, verify with public key
const fs = require('fs');
const privateKey = fs.readFileSync('private.pem');
const publicKey = fs.readFileSync('public.pem');

// Sign (Auth server)
const token = jwt.sign(
  { sub: 'user-123', roles: ['admin'] },
  privateKey,
  { algorithm: 'RS256', expiresIn: '15m' }
);

// Verify (API server - only needs public key)
const decoded = jwt.verify(token, publicKey);

3.3 JWT Verification Checklist

1. Signature Verification
   - Verify signed with the correct algorithm
   - Prevent alg: "none" attacks (always specify algorithm)

2. Expiration Check (exp)
   - Ensure current time is before exp
   - Allow slight leeway for clock skew

3. Issuer Check (iss)
   - Verify from a trusted issuer

4. Audience Check (aud)
   - Verify this token is meant for my service

5. Not Before Check (nbf)
   - Ensure current time is after nbf

6. Additional Security Checks
   - Check if token is on blacklist
   - Check if user is deactivated

4. JWT Deep Dive

4.1 Access Token and Refresh Token

Access Token:
- Short validity (15 minutes - 1 hour)
- Included in Authorization header for API requests
- Limited damage window if stolen
- Not stored on server (Stateless)

Refresh Token:
- Long validity (7 days - 30 days)
- Used only to renew Access Tokens
- Stored in server DB/Redis (Stateful)
- Stored in HttpOnly cookie (XSS prevention)

4.2 Token Renewal Flow

1. Initial Login
   POST /auth/login -> Access Token + Refresh Token issued

2. API Request
   GET /api/data
   Authorization: Bearer [access_token]
   -> 200 OK (success)

3. Access Token Expired
   GET /api/data
   Authorization: Bearer [expired_access_token]
   -> 401 Unauthorized

4. Token Renewal
   POST /auth/refresh
   Cookie: refresh_token=...
   -> New Access Token + New Refresh Token issued

5. Refresh Token Expired
   POST /auth/refresh
   -> 401 Unauthorized -> Re-login required

4.3 Refresh Token Rotation

// Refresh Token Rotation implementation
async function refreshTokens(refreshToken) {
  // 1. Look up Refresh Token in DB
  const storedToken = await db.findRefreshToken(refreshToken);

  if (!storedToken) {
    // Already used Refresh Token -> invalidate all tokens (theft detected)
    await db.revokeAllTokensForUser(storedToken.userId);
    throw new Error('Refresh Token reuse detected');
  }

  // 2. Validate
  if (storedToken.expiresAt < Date.now()) {
    throw new Error('Refresh Token expired');
  }

  // 3. Invalidate previous Refresh Token
  await db.revokeRefreshToken(refreshToken);

  // 4. Issue new token pair
  const newAccessToken = jwt.sign(
    { sub: storedToken.userId, roles: storedToken.roles },
    privateKey,
    { algorithm: 'RS256', expiresIn: '15m' }
  );

  const newRefreshToken = crypto.randomUUID();
  await db.saveRefreshToken({
    token: newRefreshToken,
    userId: storedToken.userId,
    expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000
  });

  return { accessToken: newAccessToken, refreshToken: newRefreshToken };
}

4.4 Token Blacklist

// Redis-based blacklist
const redis = require('redis');
const client = redis.createClient();

// Add token to blacklist (on logout)
async function blacklistToken(token) {
  const decoded = jwt.decode(token);
  const ttl = decoded.exp - Math.floor(Date.now() / 1000);

  if (ttl > 0) {
    await client.set(`blacklist:${decoded.jti}`, '1', { EX: ttl });
  }
}

// Check blacklist during verification
async function verifyToken(token) {
  const decoded = jwt.verify(token, publicKey);
  const isBlacklisted = await client.get(`blacklist:${decoded.jti}`);

  if (isBlacklisted) {
    throw new Error('Token has been revoked');
  }

  return decoded;
}

4.5 JWT Best Practices

DO:
- Keep Access Token validity short (15 minutes)
- Use RS256 or ES256 (asymmetric keys)
- Use jti claim for unique token identification
- Manage Refresh Tokens server-side
- Never put sensitive data in Payload

DON'T:
- Store JWT in localStorage (vulnerable to XSS)
- Use HS256 across microservices
- Include passwords/card numbers in tokens
- Allow alg: "none"
- Issue tokens without expiration

5. OAuth 2.0 Flows

5.1 OAuth 2.0 Roles

Resource Owner:
  - The end user (e.g., Google account holder)

Client:
  - The application requesting resource access (e.g., your web app)

Authorization Server:
  - The server issuing tokens (e.g., Google OAuth server)

Resource Server:
  - The server hosting protected resources (e.g., Google API)

5.2 Authorization Code Flow (+ PKCE)

The most recommended flow. Used in web apps, SPAs, and mobile apps.
PKCE (Proof Key for Code Exchange) is mandatory for SPAs/mobile.

1. Client generates PKCE parameters
   code_verifier = random(43-128 chars)
   code_challenge = BASE64URL(SHA256(code_verifier))

2. Authorization request
   GET /authorize?
     response_type=code&
     client_id=my-app&
     redirect_uri=https://myapp.com/callback&
     scope=openid profile email&
     state=random-csrf-token&
     code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
     code_challenge_method=S256

3. User logs in + consents

4. Authorization Server returns Authorization Code
   302 Redirect: https://myapp.com/callback?code=AUTH_CODE&state=random-csrf-token

5. Exchange Authorization Code for Access Token
   POST /token
   Content-Type: application/x-www-form-urlencoded

   grant_type=authorization_code&
   code=AUTH_CODE&
   redirect_uri=https://myapp.com/callback&
   client_id=my-app&
   code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

6. Token response
   {
     "access_token": "eyJhbGci...",
     "token_type": "Bearer",
     "expires_in": 3600,
     "refresh_token": "dGhpcyBpcyBhIH...",
     "id_token": "eyJhbGci...",
     "scope": "openid profile email"
   }

5.3 Client Credentials Flow

Server-to-server communication (Machine-to-Machine). No user involvement.

POST /token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&
client_id=service-a&
client_secret=secret-value&
scope=read:data write:data

Response:
{
  "access_token": "eyJhbGci...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "read:data write:data"
}

Use cases:
- Microservice-to-microservice API calls
- Batch processing systems
- API access in CI/CD pipelines

5.4 Device Code Flow

For input-constrained devices (Smart TVs, CLI tools, IoT)

1. Device requests a code
   POST /device/code
   client_id=tv-app

   Response:
   {
     "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
     "user_code": "WDJB-MJHT",
     "verification_uri": "https://auth.example.com/device",
     "interval": 5,
     "expires_in": 1800
   }

2. Display to user
   "Go to https://auth.example.com/device and enter code WDJB-MJHT"

3. User enters code on another device (phone/PC) + logs in

4. Device polls
   POST /token
   grant_type=urn:ietf:params:oauth:grant-type:device_code&
   device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS&
   client_id=tv-app

   (User not yet authenticated -> "authorization_pending")
   (User authenticated -> Access Token returned)

5.5 Implicit Flow (Deprecated)

No longer recommended! Use Authorization Code Flow with PKCE instead.

Why deprecated:
1. Access Token exposed in URL Fragment (#access_token=...)
2. Token recorded in browser history
3. Cannot issue Refresh Tokens
4. Vulnerable to Token Substitution Attacks

Alternative: Authorization Code + PKCE
- Safe to use even in SPAs
- Supports Refresh Tokens
- code_verifier prevents code interception

6. OpenID Connect (OIDC)

6.1 What is OIDC

An authentication layer built on top of OAuth 2.0.
OAuth 2.0 = Handles Authorization only
OIDC = Adds Authentication

OAuth 2.0: "Can this app access your Google Drive?"
OIDC: "Verify that this user is actually alice@gmail.com"

Key additions:
1. ID Token (user information in JWT format)
2. UserInfo endpoint
3. Standardized claims (name, email, picture, etc.)
4. Discovery document (/.well-known/openid-configuration)

6.2 ID Token

{
  "iss": "https://accounts.google.com",
  "sub": "110169484474386276334",
  "aud": "my-app-client-id",
  "exp": 1711356600,
  "iat": 1711353000,
  "nonce": "n-0S6_WzA2Mj",
  "email": "alice@gmail.com",
  "email_verified": true,
  "name": "Alice Kim",
  "picture": "https://lh3.googleusercontent.com/a/..."
}

6.3 Discovery Document

GET https://accounts.google.com/.well-known/openid-configuration

Response (key fields):
{
  "issuer": "https://accounts.google.com",
  "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
  "token_endpoint": "https://oauth2.googleapis.com/token",
  "userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo",
  "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
  "scopes_supported": ["openid", "email", "profile"],
  "response_types_supported": ["code", "token", "id_token"],
  "subject_types_supported": ["public"],
  "id_token_signing_alg_values_supported": ["RS256"]
}

7. SSO (Single Sign-On)

7.1 SSO Concept

A mechanism to access multiple services with a single login.

Example:
Logging into Google gives you access to Gmail, Drive, YouTube, Calendar.

Pros:
- Improved user experience (single login)
- Reduced password fatigue
- Centralized user management
- Consistent security policies

Cons:
- Single point of failure (IdP outage affects all services)
- Implementation complexity
- Dependency on IdP

7.2 SAML 2.0 vs OIDC

FeatureSAML 2.0OIDC
Data FormatXMLJSON/JWT
TransportHTTP Redirect/POSTHTTP Redirect
TokenAssertion (XML)ID Token (JWT)
Primary UseEnterprise SSOWeb/Mobile apps
ComplexityHighLow
Mobile SupportLimitedExcellent
Standard Year20052014
SAML 2.0 Flow:
1. User accesses SP (Service Provider)
2. SP generates SAML Request -> redirects to IdP
3. User authenticates at IdP
4. IdP generates SAML Assertion (XML) -> POSTs to SP
5. SP validates Assertion -> creates session

OIDC Flow:
1. User accesses app
2. App redirects Authorization request to IdP
3. User authenticates at IdP
4. IdP returns Authorization Code
5. App exchanges Code for ID Token + Access Token

8. Social Login Implementation

8.1 Google Login (Next.js + NextAuth.js)

// app/api/auth/[...nextauth]/route.js
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import GitHubProvider from 'next-auth/providers/github';

const handler = NextAuth({
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
    GitHubProvider({
      clientId: process.env.GITHUB_CLIENT_ID,
      clientSecret: process.env.GITHUB_CLIENT_SECRET,
    }),
  ],
  callbacks: {
    async jwt({ token, account, profile }) {
      if (account) {
        token.accessToken = account.access_token;
        token.provider = account.provider;
      }
      return token;
    },
    async session({ session, token }) {
      session.accessToken = token.accessToken;
      session.provider = token.provider;
      return session;
    },
  },
  pages: {
    signIn: '/auth/signin',
    error: '/auth/error',
  },
});

export { handler as GET, handler as POST };

8.2 GitHub Login (Express.js + Passport.js)

const passport = require('passport');
const GitHubStrategy = require('passport-github2').Strategy;

passport.use(new GitHubStrategy({
    clientID: process.env.GITHUB_CLIENT_ID,
    clientSecret: process.env.GITHUB_CLIENT_SECRET,
    callbackURL: 'https://myapp.com/auth/github/callback'
  },
  async (accessToken, refreshToken, profile, done) => {
    let user = await db.findUserByGitHubId(profile.id);

    if (!user) {
      user = await db.createUser({
        githubId: profile.id,
        name: profile.displayName,
        email: profile.emails[0].value,
        avatar: profile.photos[0].value,
      });
    }

    return done(null, user);
  }
));

// Routes
app.get('/auth/github',
  passport.authenticate('github', { scope: ['user:email'] })
);

app.get('/auth/github/callback',
  passport.authenticate('github', { failureRedirect: '/login' }),
  (req, res) => {
    res.redirect('/dashboard');
  }
);

8.3 Apple Login Specifics

Apple Login differs from other social logins:

1. Hide My Email (Private Email Relay)
   - User can provide a unique relay address instead of real email
   - Example: abc123@privaterelay.appleid.com

2. User information provided only on first login
   - Name and email are returned only on first login
   - You MUST save them on the first response!

3. client_secret is a JWT
   - Unlike other providers, you generate client_secret as a JWT
   - Signed with keys from Apple Developer Portal

4. Web uses Form POST method
   - Callback is delivered via POST

9. Passkey / WebAuthn / FIDO2

9.1 What is Passkey

The future of passwordless authentication.
Log in with biometrics (fingerprint/face), device PIN, or security keys.

Passkey = WebAuthn + FIDO2 + Cloud Sync

Pros:
- Phishing-proof (bound to domain)
- No password leaks
- Improved user experience (login with just touch/face)
- Cross-device support (iCloud Keychain, Google Password Manager)

Browser Support:
- Chrome 108+ (2022.12)
- Safari 16+ (2022.09)
- Firefox 122+ (2024.01)
- Edge 108+ (2022.12)

9.2 Registration Flow

// Server: Generate Registration Options
app.post('/webauthn/register/options', async (req, res) => {
  const user = req.user;

  const options = {
    challenge: crypto.randomBytes(32),
    rp: {
      name: 'My App',
      id: 'myapp.com'
    },
    user: {
      id: Buffer.from(user.id),
      name: user.email,
      displayName: user.name
    },
    pubKeyCredParams: [
      { alg: -7, type: 'public-key' },   // ES256
      { alg: -257, type: 'public-key' }  // RS256
    ],
    authenticatorSelection: {
      authenticatorAttachment: 'platform',
      userVerification: 'preferred',
      residentKey: 'required'
    },
    timeout: 60000
  };

  req.session.challenge = options.challenge;
  res.json(options);
});

// Client: Create Passkey
const credential = await navigator.credentials.create({
  publicKey: registrationOptions
});

// Server: Complete Registration
app.post('/webauthn/register/verify', async (req, res) => {
  const { credential } = req.body;

  // Verify challenge, verify origin, extract and save public key
  const verified = await verifyRegistration(credential, req.session.challenge);

  if (verified) {
    await db.saveCredential({
      credentialId: credential.id,
      publicKey: verified.publicKey,
      userId: req.user.id,
      counter: verified.counter
    });
    res.json({ success: true });
  }
});

9.3 Authentication Flow

// Server: Generate Authentication Options
app.post('/webauthn/login/options', async (req, res) => {
  const options = {
    challenge: crypto.randomBytes(32),
    rpId: 'myapp.com',
    userVerification: 'preferred',
    timeout: 60000
  };

  req.session.challenge = options.challenge;
  res.json(options);
});

// Client: Authenticate with Passkey
const assertion = await navigator.credentials.get({
  publicKey: authenticationOptions
});

// Server: Verify Authentication
app.post('/webauthn/login/verify', async (req, res) => {
  const { assertion } = req.body;

  const credential = await db.findCredential(assertion.id);
  const verified = await verifyAuthentication(
    assertion,
    credential.publicKey,
    req.session.challenge,
    credential.counter
  );

  if (verified) {
    // Update counter (replay attack prevention)
    await db.updateCounter(assertion.id, verified.newCounter);

    // Issue session/token
    const token = jwt.sign({ sub: credential.userId }, privateKey);
    res.json({ token });
  }
});

10. Multi-Factor Authentication (MFA)

10.1 MFA Method Comparison

MethodSecurity LevelUXImplementation
SMS OTPLow (SIM swapping)OKLow
TOTP (Google Authenticator)MediumOKMedium
Push Notification (Duo, Okta)HighGoodHigh
Hardware Key (YubiKey)Very HighOKMedium
PasskeyVery HighExcellentMedium

10.2 TOTP Implementation

const speakeasy = require('speakeasy');
const QRCode = require('qrcode');

// MFA Setup - Generate secret
app.post('/mfa/setup', async (req, res) => {
  const secret = speakeasy.generateSecret({
    name: `MyApp (${req.user.email})`,
    issuer: 'MyApp'
  });

  // Generate QR code
  const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);

  // Temporarily save secret (before verification)
  await db.saveTempMfaSecret(req.user.id, secret.base32);

  res.json({
    qrCode: qrCodeUrl,
    manualEntry: secret.base32
  });
});

// MFA Setup - Verify code and activate
app.post('/mfa/verify-setup', async (req, res) => {
  const { code } = req.body;
  const secret = await db.getTempMfaSecret(req.user.id);

  const verified = speakeasy.totp.verify({
    secret: secret,
    encoding: 'base32',
    token: code,
    window: 1  // Allow 30 seconds before/after
  });

  if (verified) {
    await db.enableMfa(req.user.id, secret);
    // Generate backup codes
    const backupCodes = Array.from({ length: 10 }, () =>
      crypto.randomBytes(4).toString('hex')
    );
    await db.saveBackupCodes(req.user.id, backupCodes);
    res.json({ success: true, backupCodes });
  } else {
    res.status(400).json({ error: 'Invalid code' });
  }
});

// MFA verification during login
app.post('/auth/mfa-verify', async (req, res) => {
  const { code, userId } = req.body;
  const user = await db.getUser(userId);

  const verified = speakeasy.totp.verify({
    secret: user.mfaSecret,
    encoding: 'base32',
    token: code,
    window: 1
  });

  if (verified) {
    const token = jwt.sign({ sub: user.id, mfa: true }, privateKey);
    res.json({ token });
  } else {
    res.status(401).json({ error: 'Invalid MFA code' });
  }
});

11. Security Best Practices

11.1 CSRF Prevention

CSRF (Cross-Site Request Forgery):
A malicious site forges requests using an authenticated user's privileges.

Defense methods:
1. SameSite cookie attribute
   Set-Cookie: session=abc; SameSite=Strict

2. CSRF Tokens
   - Server issues a unique token with the form
   - Token included with form submission
   - Server validates the token

3. Double Submit Cookie
   - Include CSRF token in both cookie and request body
   - Verify both values match

4. Origin/Referer validation
   - Check that the request Origin header is from an allowed domain

11.2 XSS Prevention and Token Storage

Token storage security comparison by location:

localStorage:
- Vulnerable to XSS (accessible via JavaScript)
- Safe from CSRF
- Strongly not recommended

sessionStorage:
- Vulnerable to XSS
- Not shared between tabs
- Slightly better than localStorage

HttpOnly Cookie:
- Safe from XSS (JavaScript cannot access)
- Needs CSRF protection (SameSite=Strict)
- Most recommended approach

Memory (variable):
- Safest from both XSS/CSRF
- Lost on page refresh
- Usable in SPAs, Refresh Token in HttpOnly cookie

11.3 CORS Configuration

// Express.js CORS configuration
const cors = require('cors');

// Bad: Allow all domains
app.use(cors());

// Good: Allow only specific domains
app.use(cors({
  origin: ['https://myapp.com', 'https://admin.myapp.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,  // Allow cookies
  maxAge: 86400       // Preflight cache 24 hours
}));

11.4 Security Headers

# Essential security headers
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 0
Content-Security-Policy: default-src 'self'; script-src 'self'
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()

11.5 Password Policy

// Password hashing with bcrypt
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 12;

// Hash password
async function hashPassword(plainPassword) {
  return await bcrypt.hash(plainPassword, SALT_ROUNDS);
}

// Verify password
async function verifyPassword(plainPassword, hashedPassword) {
  return await bcrypt.compare(plainPassword, hashedPassword);
}

// Password strength validation
function validatePasswordStrength(password) {
  const rules = [
    { test: /.{8,}/, message: 'Minimum 8 characters' },
    { test: /[A-Z]/, message: 'At least 1 uppercase letter' },
    { test: /[a-z]/, message: 'At least 1 lowercase letter' },
    { test: /[0-9]/, message: 'At least 1 number' },
    { test: /[^A-Za-z0-9]/, message: 'At least 1 special character' },
  ];

  const failures = rules.filter(r => !r.test.test(password));
  return { valid: failures.length === 0, failures };
}

12. Auth Libraries Comparison

12.1 Major Libraries

LibraryTypeLanguagePrimary Use Case
NextAuth.js (Auth.js)LibraryJS/TSNext.js social login
Passport.jsMiddlewareJSExpress multi-strategy
LuciaLibraryJS/TSSession-based, direct control
ClerkBaaSJS/TSFull-stack auth UI
Auth0IDaaSVariousEnterprise SSO
KeycloakSelf-hostedJavaEnterprise IdP
Supabase AuthBaaSJS/TSSupabase ecosystem
Firebase AuthBaaSVariousGoogle ecosystem

12.2 Selection Criteria

Small project + quick implementation:
  -> NextAuth.js / Clerk / Supabase Auth

Direct control + custom requirements:
  -> Lucia / Passport.js

Enterprise SSO + SAML:
  -> Auth0 / Keycloak

Microservices + self-hosting:
  -> Keycloak

Serverless + Google ecosystem:
  -> Firebase Auth

13. Session vs JWT Comparison Table

DimensionSession-basedJWT-based
State ManagementStateful (server-stored)Stateless (self-contained token)
Storage LocationServer (Redis/DB)Client (cookie/memory)
ScalabilityRequires session sharingNo cross-server sharing needed
Security (on theft)Immediate server invalidationValid until expiration
SizeCookie: small (session ID)Token: large (includes claims)
MicroservicesCentral session store neededIndependent verification per service
Mobile SupportCookie management neededAuthorization header
LogoutDelete session (instant)Blacklist needed
Server LoadDB/Redis lookup per requestSignature verification only (CPU)
Cross-domainCookie domain restrictionsHeaders pass freely
ImplementationLow complexityMedium (Refresh Token, etc.)
CSRFVulnerable (auto cookie send)Safe (Authorization header)
XSSSafe (HttpOnly cookie)Vulnerable (if in localStorage)
Offline UseNot possiblePossible (info in token)
DebuggingCheck server logsDecode at jwt.io

14. Quiz

Q1. Authentication vs Authorization

Explain the difference between 401 Unauthorized and 403 Forbidden with specific scenarios.

Answer:

  • 401 Unauthorized (Authentication failure): The user has not proven their identity. Examples: API call without Authorization header, using an expired JWT token, wrong password. "I don't know who you are."

  • 403 Forbidden (Authorization failure): The user is authenticated but lacks permission for the resource. Examples: regular user calling DELETE /admin/users API, attempting to access another user's private data. "I know who you are, but you don't have permission for this."

Q2. JWT Security

Explain why JWT should not be stored in localStorage and the recommended storage approach.

Answer: Storing in localStorage is vulnerable to XSS (Cross-Site Scripting) attacks. If a malicious script is injected, it can steal the token via localStorage.getItem('token').

Recommended approach:

  1. Access Token: Store in memory (JavaScript variable). Lost on page refresh but most secure.
  2. Refresh Token: Store in HttpOnly + Secure + SameSite=Strict cookie. Inaccessible from JavaScript, safe from XSS.
  3. When Access Token expires, renew using the Refresh Token cookie.

Q3. OAuth 2.0 PKCE

Explain why PKCE is mandatory for Authorization Code Flow in SPAs.

Answer: SPAs have their source code fully exposed in the browser, making it impossible to securely store a client_secret. Without PKCE, if an Authorization Code is intercepted, an attacker can exchange it for an Access Token.

PKCE solves this:

  1. Client generates a code_verifier (random string)
  2. Sends code_challenge = SHA256(code_verifier) with the authorization request
  3. Sends the original code_verifier during token exchange
  4. Server verifies SHA256(code_verifier) == code_challenge

Even if an attacker intercepts the Authorization Code, they cannot exchange it without the code_verifier.

Q4. Passkey

Explain 3 reasons why Passkey is more secure than passwords.

Answer:

  1. Phishing-proof: Passkeys are bound to the registered domain (RP ID). They will not work on fake sites, fundamentally blocking phishing attacks.

  2. Safe from server breaches: Only public keys are stored on the server. Even if the DB is leaked, public keys cannot be used to forge authentication. Passwords, while hashed, can be cracked if weak.

  3. No reuse: Unique key pairs are generated for each service. A compromise at one service has no impact on others. In contrast, users frequently reuse passwords across multiple sites.

Q5. Session vs JWT

Explain why JWT is advantageous over session-based auth in a microservices environment.

Answer: Why JWT is advantageous in microservices:

  1. Independent verification: Each service can independently verify tokens with just the public key. Sessions require all services to share a central session store (Redis).

  2. Reduced network overhead: JWT completes authentication with just signature verification, no DB/Redis lookup needed per request.

  3. Easy inter-service propagation: Simply pass the JWT in the Authorization header between services. User information is embedded in the token, eliminating additional lookups.

  4. Easy scaling: Adding new service instances requires no session store connection setup, just public key distribution.


References

  1. OAuth 2.0 RFC 6749 - https://tools.ietf.org/html/rfc6749
  2. JWT RFC 7519 - https://tools.ietf.org/html/rfc7519
  3. PKCE RFC 7636 - https://tools.ietf.org/html/rfc7636
  4. OpenID Connect Core - https://openid.net/specs/openid-connect-core-1_0.html
  5. WebAuthn Specification - https://www.w3.org/TR/webauthn-2/
  6. FIDO2 Specifications - https://fidoalliance.org/fido2/
  7. OWASP Authentication Cheat Sheet - https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
  8. OWASP Session Management - https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
  9. Auth0 Documentation - https://auth0.com/docs
  10. NextAuth.js (Auth.js) Docs - https://authjs.dev/
  11. Passkeys.dev - https://passkeys.dev/
  12. SAML 2.0 Technical Overview - https://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html
  13. OAuth 2.0 Security Best Current Practice - https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics
  14. Keycloak Documentation - https://www.keycloak.org/documentation