NextAuth + Django JWT: 중복 없는 원활한 SSO 구현
Next.js의 NextAuth와 Django REST Framework의 JWT 인증이 충돌하는 경우가 많습니다. 사용자가 프론트엔드에서 한 번만 로그인하면 Django에서 액세스 및 리프레시 토큰을 받고, 모든 API 호출은 단일 클라이언트로 자동 토큰 갱신을 처리합니다. OAuth 제공자와 백엔드가 동기화되어 두 번째 로그인 화면이 필요 없습니다.
아키텍처 역할 분리
NextAuth는 UI 세션, 로그인 폼, OAuth를 담당합니다. Django는 사용자 도메인 모델, JWT 발급, JWTAuthentication을 통한 API 보호를 처리합니다. 프론트엔드는 토큰을 직접 생성하지 않고, Django가 API 액세스의 유일한 원천입니다. 토큰은 NextAuth 세션에 저장됩니다.
이 설정의 주요 이점:
- 액세스 토큰을 위한 수동 localStorage 불필요
- 401 에러 없이 자동 갱신
- 중복 로그인 없는 OAuth 동기화
- 모든 요청에 단일 axios 클라이언트
Django: 이메일 우선 사용자 모델과 커스텀 JWT
사용자 모델은 이메일을 USERNAME_FIELD로 사용합니다. 사용자명이 항상 사용 가능하지 않은 OAuth 통합을 간소화합니다.
# auth_app/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.contrib.auth.models import BaseUserManager
class CustomUserManager(BaseUserManager):
def create_user(self, email, password=None, **extra_fields):
if not email:
raise ValueError("이메일 필드가 반드시 설정되어야 합니다")
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password=None, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
return self.create_user(email, password, **extra_fields)
class User(AbstractUser):
email = models.EmailField(unique=True)
name = models.CharField(max_length=100, blank=True)
provider = models.CharField(max_length=50, default="credentials", blank=True, null=True)
USERNAME_FIELD = "email"
REQUIRED_FIELDS = []
objects = CustomUserManager()
settings.py에서: AUTH_USER_MODEL = "auth_app.User". rest_framework_simplejwt, dj_rest_auth, allauth를 설치하세요.
커스텀 TokenObtainPairSerializer는 토큰에 email, name, provider를 추가합니다:
# auth_app/serializers.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super().get_token(user)
token["email"] = user.email
token["name"] = user.name
token["provider"] = user.provider
return token
def validate(self, attrs):
credentials = {
"email": attrs.get("email"),
"password": attrs.get("password"),
}
return super().validate(credentials)
로그인 뷰는 이메일 인증 확인 후 토큰과 사용자 데이터를 반환합니다:
# auth_app/views.py
class CustomTokenObtainPairView(TokenObtainPairView):
serializer_class = CustomTokenObtainPairSerializer
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.user
email_address = EmailAddress.objects.filter(user=user, email=user.email).first()
if not email_address or not email_address.verified:
raise AuthenticationFailed(detail="이메일 주소가 인증되지 않았습니다.")
return Response(
{
"access": serializer.validated_data["access"],
"refresh": serializer.validated_data["refresh"],
"user": {
"email": user.email,
"name": user.name,
"id": user.id,
},
},
status=status.HTTP_200_OK,
)
Next.js: Django 프록시로서의 Credentials Provider
Credentials 제공자는 이메일/비밀번호를 Django로 전달하고 받은 토큰을 세션에 저장합니다:
// src/lib/auth/authOptions.ts
import { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import CredentialsProvider from "next-auth/providers/credentials";
import apiClient from "@/services/authClientService";
const baseURL = process.env.NEXT_PUBLIC_API_BASE_URL;
export const authOptions: NextAuthOptions = {
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
}),
CredentialsProvider({
name: "Credentials",
credentials: {
email: { label: "이메일", type: "text" },
password: { label: "비밀번호", type: "password" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials.password) {
throw new Error("이메일과 비밀번호가 모두 필요합니다");
}
const { data } = await apiClient.post(`${baseURL}/api/auth/custom/login/`, {
email: credentials.email,
password: credentials.password,
});
if (data?.user) {
const { id, name, email } = data.user;
return {
id: id.toString(),
name,
email,
accessToken: data.access || null,
refreshToken: data.refresh || null,
};
}
return null;
},
}),
],
};
OAuth 동기화: register-or-login 엔드포인트
Google OAuth 후 register-or-login 엔드포인트를 호출합니다. Django가 사용자를 생성하거나 업데이트하고 JWT를 발급합니다:
callbacks: {
async signIn({ user, account }) {
const allowedProviders = ["google", "apple"];
if (account?.provider && allowedProviders.includes(account.provider)) {
const res = await apiClient.post(
`${baseURL}/api/auth/custom/oauth/register-or-login/`,
{
id: user.id,
name: user.name,
email: user.email,
provider: account.provider,
}
);
if (res.status === 200) {
user.accessToken = res.data.access;
user.refreshToken = res.data.refresh;
return true;
}
return false;
}
return true;
},
},
Django 엔드포인트:
class CustomOAuthRegisterOrLoginView(APIView):
permission_classes = [AllowAny]
def post(self, request, *args, **kwargs):
serializer = OAuthUserSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
provider = data["provider"]
email = data["email"]
name = data["name"]
user_id = data["id"]
user, created = User.objects.get_or_create(
email=email,
defaults={
"name": name,
"provider": provider,
"username": user_id,
},
)
if not created:
user.name = name
user.provider = provider
user.save()
refresh = RefreshToken.for_user(user)
access = str(refresh.access_token)
return Response(
{
"message": "사용자 동기화가 성공했습니다.",
"user": {
"email": user.email,
"name": user.name,
"provider": user.provider,
},
"access": access,
"refresh": str(refresh),
},
status=status.HTTP_200_OK,
)
JWT와 세션 콜백 최적화
JWT와 세션은 id, accessToken, refreshToken만 저장합니다. 크기를 최소화하기 위해 추가 데이터는 제외합니다:
async jwt({ token, user }) {
if (user) {
token.id = user.id.toString();
token.accessToken = user.accessToken || null;
token.refreshToken = user.refreshToken || null;
}
return token;
},
async session({ session, token }) {
session.user = {
...session.user,
id: token.id as string,
};
session.accessToken = token.accessToken || null;
session.refreshToken = token.refreshToken || null;
return session;
},
주요 요점
- 단일 토큰 원천: Django JWT가 API 액세스의 유일한 권한
- 자동 OAuth 동기화: register-or-login으로 중복 로그인 제거
- 간결한 세션: id + 토큰만, 불필요한 데이터 없음
- 이메일 인증: 로그인에 내장되어 일관성 유지
- Axios 인터셉터: 401 시 자동 갱신 및 로그아웃
— Editorial Team
아직 댓글이 없습니다.