Zurück zur Startseite

NextAuth + Django JWT: einheitliche Authentifizierung

Der Artikel beschreibt die Integration von NextAuth mit Django JWT für einheitliche Authentifizierung ohne doppelten Login. Er behandelt benutzerdefinierte User-Modelle, Credentials Provider, OAuth-Synchronisation und Session-Optimierung. Vollständiger funktionierender Code wird bereitgestellt.

NextAuth und Django JWT ohne Token-Chaos
Advertisement 728x90

NextAuth + Django JWT: Nahtlose Single Sign-On ohne Duplikate

NextAuth in Next.js und Django REST Framework mit JWT stoßen bei der Authentifizierung oft auf Konflikte. Nutzer melden sich einmal auf dem Frontend an, erhalten Zugriffs- und Refresh-Tokens von Django, und alle API-Aufrufe nutzen einen einzigen Client mit automatischer Token-Aktualisierung. OAuth-Provider synchronisieren mit dem Backend – kein zweiter Login-Bildschirm nötig.

Trennung der Architekturrollen

NextAuth kümmert sich um UI-Sessions, Login-Formulare und OAuth. Django verwaltet das User-Domain-Modell, JWT-Ausstellung und API-Schutz über JWTAuthentication. Das Frontend erzeugt nie selbst Tokens – Django ist die einzige Quelle für API-Zugriff. Tokens werden in der NextAuth-Session gespeichert.

Wichtige Vorteile dieses Setups:

Google AdInline article slot
  • Kein manuelles localStorage für Access-Tokens
  • Automatischer Refresh ohne 401-Fehler
  • OAuth-Sync ohne doppelte Logins
  • Einziger Axios-Client für alle Anfragen

Django: Email-First User-Modell und Custom JWT

Das User-Modell nutzt E-Mail als USERNAME_FIELD. Das vereinfacht die OAuth-Integration, wo Benutzernamen nicht immer verfügbar sind.

# 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("Das E-Mail-Feld muss gesetzt werden")
        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()

In settings.py: AUTH_USER_MODEL = "auth_app.User". Installieren Sie rest_framework_simplejwt, dj_rest_auth und allauth.

Der custom TokenObtainPairSerializer fügt email, name und provider zum Token hinzu:

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)

Die Login-View prüft die E-Mail-Verifizierung und gibt Tokens + User-Daten zurück:

# 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="E-Mail-Adresse ist nicht verifiziert.")

        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: Credentials Provider als Django-Proxy

Der Credentials Provider leitet E-Mail/Passwort an Django weiter und speichert die erhaltenen Tokens in der Session:

// 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: "E-Mail", type: "text" },
        password: { label: "Passwort", type: "password" },
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials.password) {
          throw new Error("E-Mail und Passwort müssen angegeben werden");
        }

        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-Sync: register-or-login Endpoint

Nach Google OAuth den register-or-login Endpoint aufrufen. Django erstellt oder aktualisiert den User und stellt JWT aus:

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-Endpoint:

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": "Benutzer erfolgreich synchronisiert.",
                "user": {
                    "email": user.email,
                    "name": user.name,
                    "provider": user.provider,
                },
                "access": access,
                "refresh": str(refresh),
            },
            status=status.HTTP_200_OK,
        )

JWT- und Session-Callback-Optimierung

JWT und Session speichern nur id, accessToken und refreshToken. Zusattdaten werden ausgeschlossen, um die Größe zu minimieren:

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;
},

Wichtige Erkenntnisse

  • Einheitliche Token-Quelle: Django JWT ist die einzige API-Zugriffsbehörde
  • Automatischer OAuth-Sync: register-or-login eliminiert doppelte Logins
  • Schlanke Sessions: Nur ID + Tokens, kein Ballast
  • E-Mail-Verifizierung: Integriert im Login für Konsistenz
  • Axios-Interceptor: Automatischer Refresh und Logout bei 401

— Editorial Team

Advertisement 728x90

Weiterlesen