2FA 도입 배경과 NextAuth 선정
최초 계획은 이러했다.
- 소셜 로그인 구현
- 2FA 보안 강화
이에 Next.js와의 통합성과 세션 관리 기능, 커스텀 인증 플로우 지원 등의 이유로 기능 구현에 있어 NextAuth를 선택(당)했다.
NextAuth는 로그인 인증 및 세션 관리에 중점을 둔 인증 프레임워크로, 2FA와 같은 특정 인증 방식을 직접 제공하지는 않지만, 이를 기반으로 커스텀 구현이 가능하여 프로젝트 요구사항에 잘 부합한다고 보였다.
이미 구현을 위한 모든 준비를 끝냈을 때!! 소셜 로그인 기능을 제외시키고 이메일 로그인을 사용하기로 변경(당)했다.
2FA 구현 방법
- OTP(One-Time Password)
- 시간 기반 일회용 비밀번호(TOTP)를 사용하여 사용자에게 동적 비밀번호를 제공.
- Google Authenticator와 같은 앱과 연동 가능.
- 이메일/SMS 인증
- 인증 코드를 이메일이나 SMS를 통해 사용자에게 전송하여 본인 확인.
- 하드웨어 키
- 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의 데이터 흐름
- authorize(): 사용자가 로그인 시도를 하면 호출되어 인증을 처리.
- JWT callback: 인증 성공 시 JWT를 생성하거나 수정.
- 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와 라우팅 동작 방식
- Link 컴포넌트 동작:
- Next.js의 기본 컴포넌트
- 내부적으로 세션/인증 상태 추적
- 미들웨어와 자동 연동
- protected route 접근 시 자동으로 세션 체크
- 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를 이용하지 않는데 클라이언트 라우팅을 이용할 경우, 세션 체크를 수동으로 체크 처리 해야 한다!!
'React' 카테고리의 다른 글
[Next.js] 로컬은 폰트 로드되는데 프로덕션은 로드 안됨(Development VS Production 간단 설명) (1) | 2024.10.14 |
---|---|
web3-react 기본 개념 정리(+web3.js) (0) | 2022.09.01 |
[React.js] Function Component vs Class Component (0) | 2022.08.30 |
import 중괄호 {}의 의미 (0) | 2022.08.25 |
React + Typescript (0) | 2022.05.10 |