Skip to content

필사 모드: SSO Cookie/JWT Authentication System Complete Guide — Framework-Specific Practical Series Index

English
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

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:

- Should I put tokens in **cookies** or **localStorage**?

- Which should I choose between **JWT** and **sessions**?

- How should I combine **HttpOnly Cookies** and **CSRF tokens**?

- How do I share authentication state across multiple services in an **SSO (Single Sign-On)** environment?

- Should **Access Token refresh** happen on the frontend or backend?

- Why does **CORS** and **credentials** setup always cause headaches?

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:**

- How SSO / OAuth 2.0 / OIDC protocols work

- Differences, pros/cons, and hybrid strategies for cookie-based vs JWT-based authentication

- Security characteristics of each browser storage type (Cookie, localStorage, sessionStorage, Memory)

- Exact configuration for CORS + Credential transmission

- Practical implementation for each framework: Spring Boot, Django, React, Next.js

- Hybrid architecture design combining multiple frameworks

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. Code → Token │ │

│ │◀───────────────────│ │

│ │ │ (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.

| Storage | JS Access | Auto-sent to Server | XSS Vulnerable | CSRF Vulnerable |

| -------------------- | --------- | ------------------- | -------------- | --------------- |

| HttpOnly Cookie | No | Yes | No | Yes |

| Non-HttpOnly Cookie | Yes | Yes | Yes | Yes |

| localStorage | Yes | No | Yes | No |

| sessionStorage | Yes | No | Yes | No |

| Memory (JS variable) | Yes | No | Partial | No |

**In-depth analysis of each storage:**

- **HttpOnly Cookie**: Cannot be accessed from JavaScript, making it safe from XSS. However, since the browser automatically attaches cookies to every request, it is vulnerable to CSRF attacks. You must always combine it with the **SameSite attribute** and **CSRF tokens**.

- **Non-HttpOnly Cookie**: Accessible via `document.cookie`, making it vulnerable to both XSS and CSRF. Avoid using this when possible.

- **localStorage**: Data persists even after closing the tab. Safe from CSRF since it is not automatically sent to the server, but tokens can be stolen through XSS attacks.

- **sessionStorage**: Isolated per tab and deleted when the tab closes. Security characteristics are the same as localStorage.

- **Memory (JS variable)**: Destroyed on page refresh. Not completely safe from XSS (running scripts can access it), but the attack difficulty is higher. Works well when combined with **Silent Refresh**.

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

Cookie Security Attributes Quick Reference

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

Detailed Attribute Descriptions

| Attribute | Value | Description |

| ---------- | -------------- | ---------------------------------------------------------------------------------------- |

| `HttpOnly` | `true` | Blocks JavaScript access. Core of XSS defense |

| `Secure` | `true` | Cookie sent only over HTTPS connections |

| `SameSite` | `Strict` | Cookie not sent on any cross-site request. Most secure but login drops on external links |

| `SameSite` | `Lax` | Allows GET navigation, blocks POST etc. Recommended for most cases |

| `SameSite` | `None` | Cookie sent even on cross-site requests. Must use with `Secure`. Required for SSO |

| `Domain` | `.example.com` | Cookie sharing across subdomains. Important for SSO |

| `Path` | `/auth` | Cookie sent only under this path |

| `Max-Age` | `3600` | Expiration time in seconds. 0 means immediate deletion |

| `Expires` | `date string` | Absolute 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)?

├── Yes → SameSite=None; Secure (+ CSRF Token required)

└── No

├── Need to maintain login when arriving via external link?

│ ├── Yes → SameSite=Lax

│ └── No → SameSite=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

Header

{

"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)

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

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:**

| Item | JWT (Stateless) | Session (Stateful) |

| ------------------ | ---------------------------- | ----------------------------- |

| Server storage | Not required | Required (Redis, DB) |

| Horizontal scaling | Easy | Requires shared session store |

| Instant revocation | Difficult (blocklist needed) | Easy (delete session) |

| Token size | Large (includes claims) | Small (session ID only) |

| Suitable scenario | MSA, API Gateway | Monolith, 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.com ← Exact Origin

Access-Control-Allow-Credentials: true ← Required

Access-Control-Allow-Headers: Content-Type, X-CSRF-Token ← Allow custom headers

Access-Control-Allow-Methods: GET, POST, PUT, DELETE ← Allowed methods

Access-Control-Expose-Headers: X-Request-Id ← Headers 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:3000 → 127.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.

| Part | Title | Key Content |

| ---- | ------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------- |

| 0 | **Current article (Index)** | Complete authentication flow map, common concepts |

| 1 | [Spring Boot](/blog/architecture/2026-03-08-sso-cookie-jwt-auth-spring-boot) | SecurityFilterChain, JWT filter, CORS setup, CSRF defense |

| 2 | [Django](/blog/architecture/2026-03-08-sso-cookie-jwt-auth-django) | DRF + SimpleJWT, SessionMiddleware, django-cors-headers |

| 3 | [React](/blog/architecture/2026-03-08-sso-cookie-jwt-auth-react) | Axios interceptors, Silent Refresh, Protected Route |

| 4 | [Next.js](/blog/architecture/2026-03-08-sso-cookie-jwt-auth-nextjs) | Middleware, Server Actions, SSR cookie passing, next-auth |

| 5 | [Integrated Practical Guide — SSO/OIDC + Hybrid Architecture](/blog/architecture/2026-03-08-sso-cookie-jwt-auth-hybrid-architecture) | Keycloak/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.

Cookie Configuration

- [ ] Is the `HttpOnly` attribute set on the Access Token cookie?

- [ ] Is the `Secure` attribute set on all authentication cookies?

- [ ] Is the `SameSite` attribute configured appropriately for the service architecture?

- [ ] Is the Refresh Token cookie's `Path` restricted to the refresh endpoint?

- [ ] Is the cookie's `Max-Age` set to an appropriate duration?

- [ ] Is the cookie's `Domain` restricted to only the necessary scope?

Token Management

- [ ] Is the Access Token expiration time short enough? (Recommended: 5-15 minutes)

- [ ] Is the Refresh Token expiration time configured? (Recommended: 7-30 days)

- [ ] Is Refresh Token Rotation implemented?

- [ ] Is there a mechanism to revoke stolen Refresh Tokens?

- [ ] Is the JWT signature using asymmetric algorithms like RS256 or ES256?

- [ ] Are the JWT's `iss`, `aud`, and `exp` claims all being verified?

CSRF Defense

- [ ] Are CSRF tokens verified on state-changing requests (POST, PUT, DELETE)?

- [ ] Are CSRF tokens refreshed per request or per session?

- [ ] Is the `SameSite` cookie attribute and CSRF token applied as dual protection?

CORS Configuration

- [ ] Is `Access-Control-Allow-Origin` not using a wildcard (`*`)?

- [ ] Is `Access-Control-Allow-Credentials: true` configured?

- [ ] Is the allowed Origin list managed as a whitelist?

- [ ] Are Preflight requests (OPTIONS) configured to pass without authentication?

Transport Security

- [ ] Is all communication done over HTTPS?

- [ ] Is the HSTS (HTTP Strict Transport Security) header configured?

- [ ] Is TLS 1.2 or higher being used?

Logout

- [ ] Are server-side sessions/tokens invalidated on logout?

- [ ] Are all authentication cookies deleted on logout?

- [ ] Is Single Logout (SLO) implemented in SSO environments?

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=123 ← SameSite=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:**

- Refresh Token Rotation: Immediately revoke previous token on refresh

- Token Family tracking: If an already-used Refresh Token is used again, revoke the entire Family

- Store Refresh Token in HttpOnly Cookie with Path restriction

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)](https://datatracker.ietf.org/doc/html/rfc7519) — JWT standard specification. Defines claims, signatures, and verification procedures

2. [RFC 6749 — OAuth 2.0 Authorization Framework](https://datatracker.ietf.org/doc/html/rfc6749) — Core OAuth 2.0 framework. Defines flows for each Grant Type

3. [RFC 6750 — OAuth 2.0 Bearer Token Usage](https://datatracker.ietf.org/doc/html/rfc6750) — Bearer Token transmission methods (Authorization header, query parameters, etc.)

4. [RFC 7517 — JSON Web Key (JWK)](https://datatracker.ietf.org/doc/html/rfc7517) — JWK Set standard for public key distribution

5. [RFC 6265 — HTTP State Management Mechanism (Cookie)](https://datatracker.ietf.org/doc/html/rfc6265) — HTTP cookie standard specification

OpenID Connect

6. [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html) — Standard that adds an authentication layer on top of OAuth 2.0

Security Guidelines

7. [OWASP — Session Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html) — Session management security best practices

8. [OWASP — JSON Web Token Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html) — JWT security checklist

9. [OWASP — Cross-Site Request Forgery Prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html) — CSRF defense strategies

Framework Official Documentation

10. [MDN — HTTP Cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) — Detailed explanation of cookie attributes (SameSite, HttpOnly, Secure, etc.)

11. [Spring Security — OAuth 2.0 Resource Server](https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/index.html) — Official Spring Boot JWT authentication guide

12. [Django REST Framework — Authentication](https://www.django-rest-framework.org/api-guide/authentication/) — DRF authentication mechanisms official documentation

13. [Next.js — Authentication](https://nextjs.org/docs/app/building-your-application/authentication) — Next.js App Router based authentication implementation guide

14. [MDN — CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) — Complete Cross-Origin Resource Sharing guide

현재 단락 (1/327)

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

작성 글자: 0원문 글자: 19,019작성 단락: 0/327