1편
이번 글에서는 Local Storage에 액세스 토큰을 관리하지 않고, NextJS 14의 서버 기능을 활용해 토큰을 안전하게 관리하는 방법을 알아보자
출처 : https://www.geeksforgeeks.org/access-token-vs-refresh-token-a-breakdown/
1-1. OAuth 토큰이란?
OAuth 인증 과정에서 받는 토큰은 크게 두 가지
1-2. 기존 클라이언트 저장 방식의 문제점
// ❌ 안전하지 않은 방식
localStorage.setItem('accessToken', data.accessToken);
이 방식의 위험성
1. XSS 취약점: 악성 스크립트가 localStorage에 접근 가능
2. 토큰 노출: 브라우저 개발자 도구로 쉽게 확인 가능
3. CSRF 공격: 적절한 보호 장치 없음
2-1. 서버 사이드 토큰 관리의 장점
1. 클라이언트 접근 차단: HttpOnly 쿠키 사용 HttpOnly 쿠키란?
2. 중앙화된 토큰 관리: 서버에서 모든 토큰 관리
3. 보안 강화: 민감한 정보를 서버에서만 처리
2-2. Route Handler를 활용한 구현
// app/api/auth/kakao/route.ts
import { cookies } from 'next/headers'
export async function POST(request: Request) {
const { code } = await request.json()
try {
// 1. 인가 코드로 토큰 받기
const tokens = await fetchKakaoTokens(code)
// 2. 토큰을 HttpOnly 쿠키에 저장
setCookieTokens(tokens)
// 3. 사용자에게는 성공 여부만 반환
return Response.json({ success: true })
} catch (error) {
return Response.json({ success: false }, { status: 500 })
}
}
// 토큰 요청 함수
async function fetchKakaoTokens(code: string) {
const response = await fetch('https://kauth.kakao.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env.KAKAO_CLIENT_ID!,
redirect_uri: process.env.KAKAO_REDIRECT_URI!,
code,
}),
})
return response.json()
}
// 쿠키 설정 함수
function setCookieTokens(tokens: any) {
// 액세스 토큰 설정
cookies().set('access_token', tokens.access_token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 // 1시간
})
// 리프레시 토큰 설정
if (tokens.refresh_token) {
cookies().set('refresh_token', tokens.refresh_token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 30 // 30일
})
}
}
2-3. 쿠키 옵션 설명
{
httpOnly: true, // 자바스크립트에서 쿠키 접근 불가
secure: true, // HTTPS에서만 쿠키 전송
sameSite: 'lax', // CSRF 공격 방지
maxAge: 3600 // 쿠키 유효 시간 (초)
}
3-1. 서버 사이드 API 요청
// app/api/protected/route.ts
import { cookies } from 'next/headers'
export async function GET() {
// 1. 쿠키에서 토큰 추출
const accessToken = cookies().get('access_token')?.value
if (!accessToken) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
// 2. 토큰을 사용하여 API 요청
const response = await fetch('https://kapi.kakao.com/v2/user/me', {
headers: {
Authorization: `Bearer ${accessToken}`
}
})
// 3. 결과 반환
const data = await response.json()
return Response.json(data)
} catch (error) {
return Response.json({ error: 'Failed to fetch data' }, { status: 500 })
}
}
3-2. 토큰 만료시 자동 갱신
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
try {
const response = await fetch(request)
// 토큰 만료 에러 감지
if (response.status === 401) {
// 리프레시 토큰으로 새로운 액세스 토큰 발급
const newTokens = await refreshTokens()
if (newTokens) {
// 새 토큰으로 재요청
return await fetch(request)
}
}
return response
} catch (error) {
return NextResponse.redirect('/login')
}
}
4-1. XSS 방지
4-2. CSRF 방지
4-3. 토큰 저장소 보안
5-1. 토큰 만료 처리
async function handleApiRequest() {
try {
const response = await fetch('/api/protected')
if (response.status === 401) {
// 1. 자동 갱신 시도
const refreshed = await refreshAccessToken()
if (refreshed) {
// 2. 요청 재시도
return await fetch('/api/protected')
} else {
// 3. 로그인 페이지로 리다이렉트
window.location.href = '/login'
}
}
return response
} catch (error) {
// 에러 처리
}
}
이렇게 구현된 토큰 관리 시스템은 다음과 같은 장점을 가질 수 있었다.
1. 클라이언트에서 토큰 노출 최소화
2. 중앙화된 토큰 관리로 일관된 보안 정책 적용
3. 자동화된 토큰 갱신으로 사용자 경험 향상
그러나 단점으로는 액세스 토큰이 필요한 요청이 생길 때마다 route.ts로 api요청을 인터셉트 해야 해서 보일러 플레이트가 많이 발생한다. NextAuth를 알게 되었는데 차차 적용해야 할 거 같다.