# Token Management System Guide 완전한 Access Token & Refresh Token 시스템 구현 가이드 ## 📋 목차 1. [시스템 개요](#시스템-개요) 2. [토큰 라이프사이클](#토큰-라이프사이클) 3. [API 엔드포인트](#api-엔드포인트) 4. [자동 토큰 갱신](#자동-토큰-갱신) 5. [사용 예시](#사용-예시) 6. [보안 고려사항](#보안-고려사항) --- ## 시스템 개요 ### 토큰 구조 ```json { "access_token": "214|EU7drdTBYN1fru0MylLXwjJbi2svXcikn5ofvmTI354d09c7", "refresh_token": "215|6hAPWcO05jtfSDV9Yz4kLQi3qZDFuycMqrNITOV3c27bd0cb", "token_type": "Bearer", "expires_in": 7200, "expires_at": "2025-11-10 15:49:38" } ``` ### 저장 방식 **HttpOnly 쿠키** (XSS 공격 방지): - `access_token`: 2시간 만료 (7200초) - `refresh_token`: 7일 만료 (604800초) **보안 속성**: - `HttpOnly`: JavaScript 접근 불가 - `Secure`: HTTPS만 전송 - `SameSite=Strict`: CSRF 공격 방지 --- ## 토큰 라이프사이클 ### 1. 로그인 (Token 발급) ``` 사용자 로그인 ↓ POST /api/auth/login ↓ PHP Backend /api/v1/login ↓ access_token + refresh_token 발급 ↓ HttpOnly 쿠키에 저장 ↓ 대시보드로 이동 ``` ### 2. 인증된 요청 ``` 보호된 페이지 접근 ↓ Middleware 인증 체크 ↓ access_token 존재? ├─ Yes → 접근 허용 └─ No → refresh_token 확인 ├─ 있음 → 자동 갱신 시도 └─ 없음 → 로그인 페이지로 ``` ### 3. 토큰 갱신 ``` access_token 만료 (2시간 후) ↓ 보호된 API 호출 시도 ↓ 401 Unauthorized 응답 ↓ POST /api/auth/refresh ↓ refresh_token으로 새 토큰 발급 ↓ 새 access_token + refresh_token 쿠키 업데이트 ↓ 원래 API 호출 재시도 ↓ 성공 ``` ### 4. 로그아웃 ``` 사용자 로그아웃 ↓ POST /api/auth/logout ↓ PHP Backend /api/v1/logout (토큰 무효화) ↓ HttpOnly 쿠키 삭제 ↓ 로그인 페이지로 이동 ``` --- ## API 엔드포인트 ### 1. Login API **Endpoint**: `POST /api/auth/login` **Request**: ```typescript { user_id: string; user_pwd: string; } ``` **Response**: ```typescript { message: string; user: UserObject; tenant: TenantObject | null; menus: MenuItem[]; token_type: "Bearer"; expires_in: number; expires_at: string; } ``` **쿠키 설정**: - `access_token` (HttpOnly, 2시간) - `refresh_token` (HttpOnly, 7일) --- ### 2. Refresh Token API **Endpoint**: `POST /api/auth/refresh` **쿠키 필요**: `refresh_token` **Response** (성공): ```typescript { message: "Token refreshed successfully"; token_type: "Bearer"; expires_in: number; expires_at: string; } ``` **Response** (실패): ```typescript { error: "Token refresh failed"; needsReauth: true; } ``` **쿠키 업데이트**: - 새 `access_token` (2시간) - 새 `refresh_token` (7일) --- ### 3. Auth Check API **Endpoint**: `GET /api/auth/check` **기능**: 1. `access_token` 존재 → 200 OK with `authenticated: true` 2. `access_token` 없음 + `refresh_token` 있음 → 자동 갱신 시도 - 갱신 성공 → 200 OK with `authenticated: true, refreshed: true` - 갱신 실패 → 401 Unauthorized 3. 둘 다 없음 → 401 Unauthorized **Response**: ```typescript // ✅ 인증 성공 (200) { authenticated: true; refreshed?: boolean; // 자동 갱신 여부 } // ❌ 인증 실패 (401) { error: string; // 'Not authenticated' 또는 'Token refresh failed' } ``` **참고**: - 🔵 **Next.js 내부 API** (PHP 백엔드 X) - 성능 최적화: 로컬 쿠키만 확인하여 빠른 응답 - 로그인/회원가입 페이지에서 이미 로그인된 사용자를 대시보드로 리다이렉트하는 데 사용 --- ### 4. Logout API **Endpoint**: `POST /api/auth/logout` **기능**: 1. PHP 백엔드에 로그아웃 요청 (토큰 무효화) 2. `access_token`, `refresh_token` 쿠키 삭제 --- ## 자동 토큰 갱신 ### 1. Middleware에서 자동 갱신 `src/middleware.ts`: ```typescript // access_token 또는 refresh_token이 있으면 인증됨 const accessToken = request.cookies.get('access_token'); const refreshToken = request.cookies.get('refresh_token'); if ((accessToken && accessToken.value) || (refreshToken && refreshToken.value)) { return { isAuthenticated: true, authMode: 'bearer' }; } ``` ### 2. Auth Check에서 자동 갱신 `src/app/api/auth/check/route.ts`: ```typescript // access_token 없고 refresh_token만 있으면 자동 갱신 if (refreshToken && !accessToken) { const refreshResponse = await fetch('/api/v1/refresh', {...}); // 새 토큰을 HttpOnly 쿠키로 설정 } ``` ### 3. API Client에서 자동 갱신 `src/lib/api/client.ts`: ```typescript // withTokenRefresh 헬퍼 함수 사용 const data = await withTokenRefresh(() => apiClient.get('/protected/resource') ); ``` **동작 방식**: 1. API 호출 시도 2. 401 응답 받음 3. `/api/auth/refresh` 호출 4. 성공 시 원래 API 재시도 5. 실패 시 로그인 페이지로 리다이렉트 --- ## 사용 예시 ### 예시 1: 보호된 페이지에서 API 호출 ```typescript // src/app/[locale]/(protected)/dashboard/page.tsx import { withTokenRefresh } from '@/lib/api/client'; export default function Dashboard() { const fetchData = async () => { try { // 자동 토큰 갱신 포함 const data = await withTokenRefresh(() => fetch('/api/protected/data', { credentials: 'include' // 쿠키 포함 }) ); console.log('Data fetched:', data); } catch (error) { console.error('Fetch failed:', error); } }; return