[Front] NextJS 14에서 OAuth 구현하기2

devicii·2024년 12월 23일
0

Front

목록 보기
3/8

0. 들어가기 전에

1편
이번 글에서는 Local Storage에 액세스 토큰을 관리하지 않고, NextJS 14의 서버 기능을 활용해 토큰을 안전하게 관리하는 방법을 알아보자

1. OAuth 토큰 관리의 중요성

출처 : https://www.geeksforgeeks.org/access-token-vs-refresh-token-a-breakdown/
1-1. OAuth 토큰이란?
OAuth 인증 과정에서 받는 토큰은 크게 두 가지

  • 액세스 토큰: API 접근 권한을 증명하는 짧은 수명의 토큰
  • 리프레시 토큰: 액세스 토큰을 갱신할 수 있는 긴 수명의 토큰

1-2. 기존 클라이언트 저장 방식의 문제점

// ❌ 안전하지 않은 방식
localStorage.setItem('accessToken', data.accessToken);

이 방식의 위험성
1. XSS 취약점: 악성 스크립트가 localStorage에 접근 가능
2. 토큰 노출: 브라우저 개발자 도구로 쉽게 확인 가능
3. CSRF 공격: 적절한 보호 장치 없음

2. NextJS의 서버 사이드 토큰 관리

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. 토큰을 사용한 API 요청 처리

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. 보안 고려사항

4-1. XSS 방지

  • HttpOnly 쿠키 사용으로 자바스크립트 접근 차단
  • 사용자 입력 데이터 이스케이프 처리

4-2. CSRF 방지

  • sameSite 쿠키 설정
  • 중요 작업에 추가 인증 요구

4-3. 토큰 저장소 보안

  • 프로덕션 환경에서 secure 플래그 필수 사용
  • 적절한 만료 시간 설정
  • 리프레시 토큰 순환 (Rotation) 구현

5. 에러 처리 전략

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를 알게 되었는데 차차 적용해야 할 거 같다.

profile
흘러가는대로 사는

0개의 댓글