React

[인증 Framework] NextAuth야 너 뭐니?

Bittersweet- 2024. 12. 19. 13:50
728x90

2FA 도입 배경과 NextAuth 선정

최초 계획은 이러했다.

  • 소셜 로그인 구현
  • 2FA 보안 강화

이에 Next.js와의 통합성과 세션 관리 기능, 커스텀 인증 플로우 지원 등의 이유로 기능 구현에 있어 NextAuth를 선택(당)했다.

 

NextAuth는 로그인 인증 및 세션 관리에 중점을 둔 인증 프레임워크로, 2FA와 같은 특정 인증 방식을 직접 제공하지는 않지만, 이를 기반으로 커스텀 구현이 가능하여 프로젝트 요구사항에 잘 부합한다고 보였다.

 

이미 구현을 위한 모든 준비를 끝냈을 때!! 소셜 로그인 기능을 제외시키고 이메일 로그인을 사용하기로 변경(당)했다.

 

2FA 구현 방법

  1. OTP(One-Time Password)
    • 시간 기반 일회용 비밀번호(TOTP)를 사용하여 사용자에게 동적 비밀번호를 제공.
    • Google Authenticator와 같은 앱과 연동 가능.
  2. 이메일/SMS 인증
    • 인증 코드를 이메일이나 SMS를 통해 사용자에게 전송하여 본인 확인.
  3. 하드웨어 키
    • USB 보안 키 또는 생체 인증(지문, 얼굴 인식 등)과 같은 물리적 인증 수단을 활용.

 

 

 

 

NextAuth 주요 인증 방식

1. OAuth 인증

Google, Github, Facebook 등의 소셜 미디어 및 타사 서비스와 연동하여 사용자 인증 처리

providers: [
  GoogleProvider({
    clientId: process.env.GOOGLE_ID,
    clientSecret: process.env.GOOGLE_SECRET,
  }),
  GitHubProvider({
    clientId: process.env.GITHUB_ID,
    clientSecret: process.env.GITHUB_SECRET,
  }),
]

 

2. Credentials 인증

이메일/비밀번호와 같은 자격 증명을 사용한 커스텀 인증을 구현 가능

providers: [
  CredentialsProvider({
    name: 'Credentials',
    credentials: {
      email: { label: "Email", type: "text" },
      password: { label: "Password", type: "password" }
    },
    async authorize(credentials) {
      // 사용자 검증 로직
      const user = await validateUser(credentials)
      return user || null
    }
  })
]

 

3. Email 인증

이메일을 통한 비밀번호 없는 인증을 제공. 매직 링크나 OTP를 이메일로 전송하여 인증 수행

providers: [
  EmailProvider({
    server: process.env.EMAIL_SERVER,
    from: process.env.EMAIL_FROM,
    maxAge: 24 * 60 * 60, // 24시간 유효
  })
]

 

 

주요 특징

  • 다양한 인증 제공자 지원: OAuth(Google, GitHub 등), Credentials(이메일/비밀번호), Email.
  • JWT와 세션 기반 인증 지원: 서버리스 환경 및 데이터베이스 통합에 적합.
  • 자동 CSRF 보호: 보안성을 높이기 위한 기본 기능.
  • TypeScript 지원: 타입 안정성을 제공하여 개발 생산성 향상.

 

NextAuth의 데이터 흐름

  1. authorize(): 사용자가 로그인 시도를 하면 호출되어 인증을 처리.
  2. JWT callback: 인증 성공 시 JWT를 생성하거나 수정.
  3. Session callback: 클라이언트로 전달할 세션 데이터를 구성.

 

주요 설정 옵션 샘플.

export const authOptions = {
  providers: [], // 인증 제공자 설정
  pages: {
	  signIn: '/auth/signin' // signin, signout 커스텀 페이지 라우팅 설정**필수값 아님
	},
  session: {
    strategy: "jwt", // 'jwt' 또는 'database'
    maxAge: 30 * 24 * 60 * 60, // 세션 유효기간
  },
  callbacks: {
    async jwt({ token, user }) {
      // JWT 토큰 커스터마이징
      return token
    },
    async session({ session, token }) {
      // 세션 데이터 커스터마이징
      return session
    }
  }
}

 

보안 고려사항

  • JWT 토큰의 만료 시간을 적절히 설정
  • 중요한 정보는 토큰에 저장하지 않기
  • HTTPS 사용 권장
  • 환경변수로 비밀키 관리

위 예시 코드에서는 OTP 인증과 함께 JWT 토큰의 만료 시간을 관리하는 방법을 보여주고 있다.

 

[NextAuth.js의 데이터 흐름]

authorize() => JWT callback => session callback 

 

예시

import { authenticator } from 'otplab';

// 상수로 관리
const MAX_AGE = 1800; // 30분
// session maxAge 설정
  session: {
    strategy: "jwt",
    maxAge: MAX_AGE
  },
callbacks: {
  // 1. JWT 콜백에서 먼저 토큰에 저장
  async jwt({ token, user }) {
      // 명시적으로 토큰 만료 시간 설정
	    token.exp = Math.floor(Date.now() / 1000) + MAX_AGE;
	    
	    // 로그인 시 토큰 생성시간 기록
      if (trigger === "signIn") {
        // 로그인 경우에만 토큰 생성시간 업데이트
        token.create = Math.floor(new Date().getTime() / 1000);
      }
      // OTP 검증(update 트리거 시)
      if (trigger === "update") {
        try {
          // client에서 받은 passcode를 check
          const isValid = authenticator.check(
          session.passcode, // 사용자가 입력한 6자리
          token.passcode // 최초 생성 시 저장된 비밀키
          );
          if (isValid) {
            token.is_valid_otp = true;
          } else {
            token.is_valid_otp = false;
          }
        } catch (error) {
          token.is_valid_otp = false;
          console.error("OTP Verification Error:", error);
        }
      }
      return { ...user, ...token };
  },

  // 2. Session 콜백에서 토큰의 데이터를 세션으로 복사
  async session({ session, token }) {
    session.user.custom_field = token.custom_field;
    return session;
  }
}
  • trigger는 NextAuth 내부 동작을 위한 것으로 추가할 수 없다.(signIn, signUp, update, signOut) 대신 jwt callback에서 필요한 조건을 추가할 수 있다.
// authOptions.ts
callbacks: {
  async jwt({ token, trigger, session }) {
    // 기본 trigger 처리
    if (trigger === "update") {
      // 기본 업데이트 로직
    }

    // 커스텀 조건 추가
    if (session?.customField) {
      token.customField = session.customField;
    }

    return token;
  }
}

 

 

NextAuth.js의 기본 엔드포인트 설명

  • /api/auth/signin: 로그인 페이지 제공 (커스텀 가능).
  • /api/auth/signout: 로그아웃 처리.
  • /api/auth/session: 현재 세션 상태 확인/갱신.
  • /api/auth/providers: 설정된 인증 제공자 목록 반환.
  • /api/auth/csrf: CSRF 토큰 제공.
  • /api/auth/callback/:provider: OAuth 콜백 처리.

 

NextAuth와 라우팅

NextAuth 클라이언트 사이드 보호 메커니즘으로 클라이언트 사이드에서 라우팅을 시도할 때 session의 status를 체크하지 않으면 라우팅을 차단한다.

즉, NextAuth는 클라이언트 사이드에서 보호된 라우트로의 이동을 시도할 때 status 체크를 강제하는 보안 메커니즘을 가지고 있다는 말이다.

단, Link 컴포넌트를 통한 라우팅은 Next.js의 라우팅 시스템을 이용하며, 미들웨어와 자동으로 연동되어 세션 체크를 처리하기 때문에 추가 세션 체크가 필요 없다.

*** 세션이 있더라도 status 체크가 없으면 nextAuth의 클라이언트 보안 메커니즘이 라우팅을 차단한다.*

NextAuth와 라우팅 동작 방식

  1. Link 컴포넌트 동작:
    • Next.js의 기본 컴포넌트
    • 내부적으로 세션/인증 상태 추적
    • 미들웨어와 자동 연동
    • protected route 접근 시 자동으로 세션 체크
  2. router.push() 동작:
    • 프로그래매틱 네비게이션 방식
    • 수동으로 세션 상태 체크 필요
    • NextAuth의 보안 메커니즘으로 인해 세션 체크 없이는 차단
session 없음 A[버튼 클릭] => B[NextAuth 클라이언트 체크] => C[라우팅 시도] => D[미들웨어 세션 체크] => E[리다이렉트]
session 있음  A[버튼 클릭] => B[NextAuth 클라이언트 체크] => C[라우팅 차단] => D[동작 없음]

 

클라이언트 체크 예시)

'use client'

import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useSession } from 'next-auth/react'

function ExampleComponent() {
  const router = useRouter()
  const { status } = useSession()

  return (
    <>
      {/* 자동으로 세션 체크 + 미들웨어 동작 */}
      <Link href="/protected">Protected Page</Link>

      {/* 수동으로 세션 체크 필요 */}
      <button onClick={() => {
        // status 체크 없으면 NextAuth가 라우팅 차단
        if (status === 'authenticated') {
          router.push('/protected')
        }
      }}>
        Go to Protected
      </button>
    </>
  )
}

 

 

한줄 요약!!

Link를 이용하지 않는데 클라이언트 라우팅을 이용할 경우, 세션 체크를 수동으로 체크 처리 해야 한다!!