NextAuth + Django JWT : Authentification unique fluide sans duplication
NextAuth sur Next.js et Django REST Framework avec JWT se heurtent souvent lors de l'authentification. Les utilisateurs se connectent une seule fois sur le frontend, obtiennent des jetons d'accès et de rafraîchissement de Django, et toutes les appels API utilisent un seul client avec rafraîchissement automatique des jetons. Les fournisseurs OAuth se synchronisent avec le backend — pas besoin d'un second écran de connexion.
Séparation des rôles dans l'architecture
NextAuth gère les sessions d'interface, les formulaires de connexion et OAuth. Django s'occupe du modèle de domaine utilisateur, de l'émission des JWT et de la protection des API via JWTAuthentication. Le frontend ne génère jamais de jetons lui-même — Django est la seule source d'accès aux API. Les jetons sont stockés dans la session NextAuth.
Avantages clés de cette configuration :
- Pas de localStorage manuel pour les jetons d'accès
- Rafraîchissement automatique sans erreurs 401
- Synchronisation OAuth sans connexions dupliquées
- Un seul client axios pour toutes les requêtes
Django : Modèle utilisateur priorisant l'email et JWT personnalisé
Le modèle utilisateur utilise l'email comme USERNAME_FIELD. Cela simplifie l'intégration OAuth, où les noms d'utilisateur ne sont pas toujours disponibles.
# 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("Le champ Email doit être renseigné")
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()
Dans settings.py : AUTH_USER_MODEL = "auth_app.User". Installez rest_framework_simplejwt, dj_rest_auth et allauth.
Le TokenObtainPairSerializer personnalisé ajoute email, name et provider au jeton :
# 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)
La vue de connexion vérifie la validation de l'email et renvoie les jetons + données utilisateur :
# 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="L'adresse email n'est pas vérifiée.")
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 : Fournisseur Credentials comme proxy Django
Le fournisseur Credentials transmet email/mot de passe à Django et stocke les jetons reçus dans la 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: "Email", type: "text" },
password: { label: "Mot de passe", type: "password" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials.password) {
throw new Error("L'email et le mot de passe doivent être fournis");
}
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;
},
}),
],
};
Synchronisation OAuth : Point de terminaison register-or-login
Après OAuth Google, appelez le point de terminaison register-or-login. Django crée ou met à jour l'utilisateur et émet un 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;
},
},
Point de terminaison 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": "Utilisateur synchronisé avec succès.",
"user": {
"email": user.email,
"name": user.name,
"provider": user.provider,
},
"access": access,
"refresh": str(refresh),
},
status=status.HTTP_200_OK,
)
Optimisation des callbacks JWT et session
JWT et session ne stockent que id, accessToken et refreshToken. Les données supplémentaires sont exclues pour minimiser la taille :
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;
},
Points clés
- Source unique de jetons : Django JWT est l'unique autorité d'accès aux API
- Synchronisation OAuth auto : register-or-login élimine les connexions dupliquées
- Sessions légères : Seulement id + jetons, sans surcharge
- Vérification email : Intégrée à la connexion pour la cohérence
- Intercepteur Axios : Rafraîchissement auto et déconnexion sur 401
— Editorial Team
Aucun commentaire pour le moment.