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:
- 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:
# 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:
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
Noch keine Kommentare.