LabHub

Blog

SSO Cookie/JWT Authentication System Complete Guide — Framework-Specific Practical Series Index

한국어English日本語

Series Introduction — Why This Series

Web authentication is not simple. It is easy to think "just build a login feature," but in practice, questions keep piling up:

The answers to these questions differ by framework. Spring Boot's SecurityFilterChain and Django's SessionMiddleware have different default strategies, and React SPA and Next.js SSR handle cookies in fundamentally different ways. Official documentation alone is not enough to see the full picture.

This series first lays out the complete map of the authentication system, then implements practical code for each framework.

What the series covers:


Complete Authentication Flow Map

Let us grasp the complete flow of the authentication system at a glance. Below is the entire process from SSO/OIDC-based login through token refresh to logout.

Login Flow

┌──────────┐     ┌──────────────┐     ┌──────────────┐     ┌──────────┐
Browser  │     │  Frontend    │     │  Backend     │     │   IdP (User) (React/Next) (Spring/ (Keycloak│
│          │     │              │     │  Django)     │     │  Okta)└────┬─────┘     └──────┬───────┘     └──────┬───────┘     └────┬─────┘
1. Click login  │                    │                  │
     │─────────────────▶│                    │                  │
     │                  │  2. /auth/login    │                  │
     │                  │───────────────────▶│                  │
     │                  │                    │  3. Redirect URL     │                  │                    │─────────────────▶│
4. IdP login page                    │                  │
     │◀──────────────────────────────────────────────────────────│
5. User authentication (ID/PW, MFA)  │                  │
     │──────────────────────────────────────────────────────────▶│
     │                  │                    │  6. Auth Code     │                  │                    │◀─────────────────│
     │                  │  7. CodeToken   │                  │
     │                  │◀───────────────────│                  │
     │                  │                      (id_token +     │                  │                    │   access_token +     │                  │                    │   refresh_token)8. Set-Cookie:  │                    │                  │
     │     access_token │                    │                  │
          (HttpOnly)   │                    │                  │
     │◀─────────────────│                    │                  │
9. Auth complete,│                    │                  │
     │     go to dashboard│                   │                  │
     │◀─────────────────│                    │                  │

Authenticated Request Flow

┌──────────┐     ┌──────────────┐     ┌──────────────┐
Browser  │     │  Frontend    │     │  Backend└────┬─────┘     └──────┬───────┘     └──────┬───────┘
API request      │                    │
     │─────────────────▶│                    │
     │                  │  Cookie auto-sent   │
  (access_token)     │                  │───────────────────▶│
     │                  │                    │  JWT verification
     │                  │                      (signature, expiry, claims)
     │                  │   200 + data       │
     │                  │◀───────────────────│
Render           │                    │
     │◀─────────────────│                    │

Token Refresh Flow

┌──────────┐     ┌──────────────┐     ┌──────────────┐
Browser  │     │  Frontend    │     │  Backend└────┬─────┘     └──────┬───────┘     └──────┬───────┘
API request      │                    │
     │─────────────────▶│                    │
     │                  │  Cookie sent        │
     │                  │───────────────────▶│
     │                  │   401 Unauthorized     │                  │◀───────────────────│
     │                  │                    │
     │                  │  POST /auth/refresh│
  (refresh_token    │
     │                  │   Cookie sent)     │                  │───────────────────▶│
     │                  │   New access_token │
     │                  │   Set-Cookie     │                  │◀───────────────────│
     │                  │                    │
     │                  │  Retry original    │
     │                  │  request           │
     │                  │───────────────────▶│
     │                  │   200 + data       │
     │                  │◀───────────────────│
Render           │                    │
     │◀─────────────────│                    │

Logout Flow

┌──────────┐     ┌──────────────┐     ┌──────────────┐     ┌──────────┐
Browser  │     │  Frontend    │     │  Backend     │     │   IdP└────┬─────┘     └──────┬───────┘     └──────┬───────┘     └────┬─────┘
Click logout     │                    │                  │
     │─────────────────▶│                    │                  │
     │                  │  POST /auth/logout │                  │
     │                  │───────────────────▶│                  │
     │                  │                    │  Revoke Token     │                  │                    │─────────────────▶│
     │                  │   Set-Cookie:      │                  │
     │                  │   access_token=""  │                  │
     │                  │   Max-Age=0        │                  │
     │                  │◀───────────────────│                  │
Cookie deleted,  │                    │                  │
     │  login page       │                    │                  │
     │◀─────────────────│                    │                  │

Key point: When the backend manages tokens with HttpOnly Cookies, frontend JavaScript cannot directly access the tokens. This is the most effective strategy for defending against XSS attacks.


Browser Storage Access Characteristics

Where to store tokens is a trade-off between security and convenience. You must accurately understand each storage type to make the right choice.

StorageJS AccessAuto-sent to ServerXSS VulnerableCSRF Vulnerable
HttpOnly CookieNoYesNoYes
Non-HttpOnly CookieYesYesYesYes
localStorageYesNoYesNo
sessionStorageYesNoYesNo
Memory (JS variable)YesNoPartialNo

In-depth analysis of each storage:

Practical recommendation: Access Token in HttpOnly Cookie, CSRF defense with dual SameSite=Lax + CSRF Token. Store Refresh Token in HttpOnly Cookie as well, but restrict the Path to /auth/refresh.


Cookies are not just simple key=value pairs. Missing a single security attribute can bring down the entire authentication system.

Detailed Attribute Descriptions

AttributeValueDescription
HttpOnlytrueBlocks JavaScript access. Core of XSS defense
SecuretrueCookie sent only over HTTPS connections
SameSiteStrictCookie not sent on any cross-site request. Most secure but login drops on external links
SameSiteLaxAllows GET navigation, blocks POST etc. Recommended for most cases
SameSiteNoneCookie sent even on cross-site requests. Must use with Secure. Required for SSO
Domain.example.comCookie sharing across subdomains. Important for SSO
Path/authCookie sent only under this path
Max-Age3600Expiration time in seconds. 0 means immediate deletion
Expiresdate stringAbsolute expiration time. Max-Age takes precedence

Practical Configuration Example

Set-Cookie: access_token=eyJhbGciOi...;
  HttpOnly;
  Secure;
  SameSite=Lax;
  Path=/;
  Max-Age=900;
  Domain=.myservice.com

Set-Cookie: refresh_token=dGhpcyBpcyBh...;
  HttpOnly;
  Secure;
  SameSite=Strict;
  Path=/api/auth/refresh;
  Max-Age=604800;
  Domain=.myservice.com

Set-Cookie: csrf_token=abc123def456;
  Secure;
  SameSite=Lax;
  Path=/;
  Max-Age=900;
  Domain=.myservice.com

Note: csrf_token is not HttpOnly. The frontend needs to read this value and include it in request headers (X-CSRF-Token), so JavaScript access is required.

SameSite Strategy Decision Tree:

Need SSO (cross-domain)?
├── YesSameSite=None; Secure (+ CSRF Token required)
└── No
    ├── Need to maintain login when arriving via external link?
    │   ├── YesSameSite=Lax
    │   └── NoSameSite=Strict
    └── API-only cookie?
        └── SameSite=Strict (most secure)

JWT Structure and Key Claims

JWT (JSON Web Token) consists of three parts separated by ..

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.     Header (Base64URL)
eyJzdWIiOiJ1c2VyMTIzIiwicm9sZXMiOlsiQU...Payload (Base64URL)
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQ...Signature
{
  "alg": "RS256", // Signing algorithm (RS256, HS256, ES256, etc.)
  "typ": "JWT", // Token type
  "kid": "key-id-001" // Key ID (for identifying public key in JWK Set)
}

Payload (Claims)

{
  "sub": "user123", // Subject — unique user identifier
  "iss": "https://idp.myservice.com", // Issuer — token issuer
  "aud": "https://api.myservice.com", // Audience — intended recipient
  "exp": 1741420800, // Expiration — expiry time (Unix timestamp)
  "iat": 1741417200, // Issued At — issuance time
  "nbf": 1741417200, // Not Before — invalid before this time
  "jti": "unique-token-id-789", // JWT ID — unique token ID (replay prevention)
  "roles": ["ADMIN", "USER"], // Custom — user permissions
  "tenant_id": "tenant-abc", // Custom — multi-tenant identifier
  "email": "user@example.com" // Custom — user email
}

Signature Verification

RSASHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  publicKey
)

JWT Parsing Example (Pseudocode)

import base64, json

def parse_jwt(token):
    header_b64, payload_b64, signature = token.split(".")

    # Base64URL decoding (padding correction)
    header = json.loads(
        base64.urlsafe_b64decode(header_b64 + "==")
    )
    payload = json.loads(
        base64.urlsafe_b64decode(payload_b64 + "==")
    )

    # Expiry verification
    import time
    if payload["exp"] < time.time():
        raise Exception("Token has expired")

    # Issuer verification
    if payload["iss"] != "https://idp.myservice.com":
        raise Exception("Untrusted issuer")

    return header, payload

Warning: In production environments, you must verify the signature. The above code is for structural understanding only. Use verified libraries such as PyJWT, jose, or jsonwebtoken.

JWT vs Session Comparison:

ItemJWT (Stateless)Session (Stateful)
Server storageNot requiredRequired (Redis, DB)
Horizontal scalingEasyRequires shared session store
Instant revocationDifficult (blocklist needed)Easy (delete session)
Token sizeLarge (includes claims)Small (session ID only)
Suitable scenarioMSA, API GatewayMonolith, single server

CORS and Credential Transmission

The area where most developers struggle with cookie-based authentication is CORS (Cross-Origin Resource Sharing) configuration. When frontend and backend domains differ, cookie transmission being blocked is a problem nearly every project encounters at least once.

Correct Configuration

Frontend (fetch API):

// credentials: 'include' is the key
const response = await fetch('https://api.myservice.com/users/me', {
  method: 'GET',
  credentials: 'include', // Send cookies along
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': getCsrfToken(), // CSRF token header
  },
})

Frontend (Axios):

const api = axios.create({
  baseURL: 'https://api.myservice.com',
  withCredentials: true, // Send cookies along
})

// Auto-attach CSRF token via interceptor
api.interceptors.request.use((config) => {
  const csrfToken = getCookieValue('csrf_token')
  if (csrfToken) {
    config.headers['X-CSRF-Token'] = csrfToken
  }
  return config
})

Backend response headers:

Access-Control-Allow-Origin: https://app.myservice.comExact Origin
Access-Control-Allow-Credentials: trueRequired
Access-Control-Allow-Headers: Content-Type, X-CSRF-TokenAllow custom headers
Access-Control-Allow-Methods: GET, POST, PUT, DELETEAllowed methods
Access-Control-Expose-Headers: X-Request-IdHeaders readable by frontend

Common Mistakes and Solutions

Mistake 1: Using Access-Control-Allow-Origin: * with credentials

# This will be blocked by the browser
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
# → Error: Wildcard Origin not allowed in credentials mode

Solution: Specify the Origin exactly. If you need to support multiple Origins, check the request's Origin header and set it dynamically if it is on the whitelist.

# Django example
ALLOWED_ORIGINS = [
    'https://app.myservice.com',
    'https://admin.myservice.com',
]

# django-cors-headers configuration
CORS_ALLOWED_ORIGINS = ALLOWED_ORIGINS
CORS_ALLOW_CREDENTIALS = True

Mistake 2: Expecting cookies on Preflight (OPTIONS) requests

# Preflight (OPTIONS) requests do not include cookies!
# Do not require authentication on OPTIONS responses.
# → Exclude OPTIONS methods from your backend auth filters.

Mistake 3: SameSite=Strict cookies not sent on cross-origin requests

# Frontend: https://app.myservice.com
# Backend: https://api.myservice.com
# → Different Origins, so SameSite=Strict cookies are not sent
# → Use SameSite=None; Secure or the same parent domain

Mistake 4: localhost vs 127.0.0.1 in local dev

# localhost:3000 → localhost:8080 : Same site (cookies sent)
# localhost:3000127.0.0.1:8080 : Different site! (cookies not sent)
# → Use the same hostname even in development

Series Table of Contents

This series consists of 5 parts. After learning common concepts on this index page, move to the practical implementation for each framework.

PartTitleKey Content
0Current article (Index)Complete authentication flow map, common concepts
1Spring BootSecurityFilterChain, JWT filter, CORS setup, CSRF defense
2DjangoDRF + SimpleJWT, SessionMiddleware, django-cors-headers
3ReactAxios interceptors, Silent Refresh, Protected Route
4Next.jsMiddleware, Server Actions, SSR cookie passing, next-auth
5Integrated Practical Guide — SSO/OIDC + Hybrid ArchitectureKeycloak/Okta integration, BFF pattern, multi-service SSO

Recommended reading order:

  1. Grasp the overall flow from this index article.
  2. Read the backend framework part (Part 1 or Part 2) that you use.
  3. Read the frontend part (Part 3 or Part 4).
  4. Learn the architecture that integrates everything in Part 5.

Security Checklist (Series-Wide)

Security items that must be applied to all authentication systems regardless of framework. Always verify before deployment.

Token Management

CSRF Defense

CORS Configuration

Transport Security

Logout


Common Bugs and Misconceptions

Misconception 1: "JWT is encrypted"

JWT is signed, not encrypted. Base64URL encoding is not encryption. Anyone can decode the Payload and read its contents.

# Decoding JWT Payload (anyone can do this)
echo "eyJzdWIiOiJ1c2VyMTIzIn0" | base64 -d
# Output: {"sub":"user123"}

Never put sensitive information (passwords, social security numbers, etc.) in the JWT Payload. If you truly need encryption, use JWE (JSON Web Encryption).

Misconception 2: "It is okay to store JWT in localStorage"

Many SPA framework tutorials show examples of storing JWT in localStorage. This is vulnerable to XSS attacks. A vulnerability in a single third-party library can lead to token theft.

// Dangerous pattern
localStorage.setItem('token', jwt)

// Safe pattern: Use HttpOnly Cookie (set by server)
// Frontend does not handle tokens directly

Misconception 3: "SameSite=Lax completely defends against CSRF"

SameSite=Lax defends against most CSRF but is not complete. Cookies are sent on cross-site GET requests. If you have an API that changes state via GET requests, it is still vulnerable.

# GET /api/users/delete?id=123SameSite=Lax cannot defend against this!
# → State changes must always use POST/PUT/DELETE

Misconception 4: "Refresh Token is more secure than Access Token"

Refresh Tokens are valid for longer periods, so if stolen they are more dangerous than Access Tokens. A stolen Refresh Token allows an attacker to issue new Access Tokens indefinitely.

Defense strategies:

Misconception 5: "CORS protects the server"

CORS is a browser policy. CORS does not apply to curl, Postman, or server-to-server communication. CORS only prevents malicious websites from using the user's browser to call APIs -- it does not protect the API itself.

# Works regardless of CORS
curl -X POST https://api.myservice.com/data \
  -H "Cookie: access_token=stolen_token"
# → Server-side token verification is essential

Common Bug: Token Refresh Race Condition

When multiple API requests simultaneously receive 401, multiple Refresh requests occur. When using Refresh Token Rotation, only the first request succeeds and the rest fail.

// Solution: Pattern to consolidate refresh requests into one
let refreshPromise = null

async function refreshToken() {
  if (refreshPromise) return refreshPromise // Wait if already refreshing

  refreshPromise = axios.post('/auth/refresh').finally(() => {
    refreshPromise = null
  })

  return refreshPromise
}

References

Official documents and standard specifications referenced while writing this series. Always check the original sources when designing authentication systems.

Standard Specifications (RFC)

  1. RFC 7519 — JSON Web Token (JWT) — JWT standard specification. Defines claims, signatures, and verification procedures
  2. RFC 6749 — OAuth 2.0 Authorization Framework — Core OAuth 2.0 framework. Defines flows for each Grant Type
  3. RFC 6750 — OAuth 2.0 Bearer Token Usage — Bearer Token transmission methods (Authorization header, query parameters, etc.)
  4. RFC 7517 — JSON Web Key (JWK) — JWK Set standard for public key distribution
  5. RFC 6265 — HTTP State Management Mechanism (Cookie) — HTTP cookie standard specification

OpenID Connect

  1. OpenID Connect Core 1.0 — Standard that adds an authentication layer on top of OAuth 2.0

Security Guidelines

  1. OWASP — Session Management Cheat Sheet — Session management security best practices
  2. OWASP — JSON Web Token Cheat Sheet — JWT security checklist
  3. OWASP — Cross-Site Request Forgery Prevention — CSRF defense strategies

Framework Official Documentation

  1. MDN — HTTP Cookies — Detailed explanation of cookie attributes (SameSite, HttpOnly, Secure, etc.)
  2. Spring Security — OAuth 2.0 Resource Server — Official Spring Boot JWT authentication guide
  3. Django REST Framework — Authentication — DRF authentication mechanisms official documentation
  4. Next.js — Authentication — Next.js App Router based authentication implementation guide
  5. MDN — CORS — Complete Cross-Origin Resource Sharing guide

Comments

No comments yet.

Sign in to leave a comment