홈으로 돌아가기

NextAuth + Django JWT: 통합 인증

이 기사는 NextAuth와 Django JWT 통합을 설명하며 중복 로그인 없는 통합 인증을 다룹니다. 사용자 지정 사용자 모델, credentials provider, OAuth 동기화, 세션 최적화를 다루며 전체 작동 코드가 제공됩니다.

NextAuth와 Django JWT: 토큰 혼란 없이
Advertisement 728x90

NextAuth + Django JWT: 중복 없는 원활한 SSO 구현

Next.js의 NextAuth와 Django REST Framework의 JWT 인증이 충돌하는 경우가 많습니다. 사용자가 프론트엔드에서 한 번만 로그인하면 Django에서 액세스 및 리프레시 토큰을 받고, 모든 API 호출은 단일 클라이언트로 자동 토큰 갱신을 처리합니다. OAuth 제공자와 백엔드가 동기화되어 두 번째 로그인 화면이 필요 없습니다.

아키텍처 역할 분리

NextAuth는 UI 세션, 로그인 폼, OAuth를 담당합니다. Django는 사용자 도메인 모델, JWT 발급, JWTAuthentication을 통한 API 보호를 처리합니다. 프론트엔드는 토큰을 직접 생성하지 않고, Django가 API 액세스의 유일한 원천입니다. 토큰은 NextAuth 세션에 저장됩니다.

이 설정의 주요 이점:

Google AdInline article slot
  • 액세스 토큰을 위한 수동 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를 추가합니다:

Google AdInline article slot
# 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를 발급합니다:

Google AdInline article slot
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

Advertisement 728x90

다음 읽기