Introduction
Dans l’article précédent, nous avons mis en place une authentification via l’adresse email, une méthode courante sur les plateformes modernes. Cependant, cette méthode nécessite une vérification de l'email pour s'assurer que celui-ci appartient bien à l’utilisateur. Dans cet article, nous allons voir comment réaliser cette vérification à l’aide d’un code OTP (One-Time Password) généré aléatoirement.
1. Création du modèle OTP
Dans l’application users, créez un modèle Otp pour représenter les OTP. Ce modèle contiendra l'utilisateur associé, le code OTP, et une date d'expiration.
class Otp(models.Model):
user = models.ForeignKey(
"User",
on_delete=models.CASCADE,
)
code = models.CharField(max_length=6)
expiration_at = models.DateTimeField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.user.email}'s OTP"
Puis on réalise une modification sur le modèle User en ajoutant un nouveau champ et on réalise les migrations :
email_verified_at = models.DateTimeField(blank=True, null=True)
2. Configuration des paramètres dans settings.py
Ajoutez les paramètres suivants pour définir le nombre de chiffres de l’OTP, sa durée de validité, et les paramètres d'email :
# OTP constant
OTP_CODE_NUMBER_OF_DIGITS = 7
OTP_VALID_MINUTES = 10
# Email settings
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.gmail.com"
EMAIL_FROM = "*******************@gmail.com"
EMAIL_PORT = 587
EMAIL_HOST_USER = "*******************@gmail.com"
EMAIL_HOST_PASSWORD = "*******************"
EMAIL_USE_TLS = True
3. Mise en œuvre de l'envoi d’email de vérification
Créez un fichier email.py dans l’application users pour gérer l'envoi des emails de vérification. Ce fichier contiendra les fonctions pour envoyer le code OTP à l’utilisateur par email.
from django.conf import settings
from django.core.mail import EmailMessage
def send_mail(subject, body, to):
email = EmailMessage(
subject=subject, body=body, from_email=settings.EMAIL_FROM, to=to
)
email.send()
def send_opt(otp):
subject = "Vérification OTP"
body = f"Your is {otp.code}\n It's valid {settings.OTP_VALID_MINUTES}"
to_email = otp.user.email
send_mail(subject, body, [to_email])
4. Mise en place du service de vérification OTP
En tout premier lieux, on cree un fichier exceptions.py dans l’application users et on y ajoute la classe suivante :
class OtpVerifyError(Exception):
pass
Puis dans le fichier services.py, ajoutez le service qui générera les OTP et vérifiera les codes saisis par les utilisateurs. Ce service gérera également l'activation du compte en cas de vérification réussie.
import datetime
import random
from django.conf import settings
from django.utils import timezone
from users.exceptions import OtpVerifyError
from users.models import User, Otp
class OtpService:
def __init__(self):
self.number_of_digits = settings.OTP_CODE_NUMBER_OF_DIGITS
def create(self, user: User) -> Otp:
digits = self.__get_digits()
otp = Otp.objects.create(
code=str(random.randrange(0, digits)).zfill(self.number_of_digits),
expiration_at=timezone.now()
+ datetime.timedelta(minutes=settings.OTP_VALID_MINUTES),
user_id=user.id,
)
return otp
def __get_digits(self) -> int:
digits = ""
for num in range(self.number_of_digits):
digits += "9"
return int(digits)
class OtpVerifyService:
def done(self, user: User, send_code: str):
if not user.otp_set.all():
raise OtpVerifyError("Invalid otp verify")
otp = user.otp_set.all().last()
if send_code != otp.code:
raise OtpVerifyError("Wrong otp code")
if timezone.now() > otp.expiration_at:
raise OtpVerifyError("Expiration of validity")
self.verify_done(user)
def verify_done(self, user: User) -> None:
user.email_verified_at = timezone.now()
user.last_login = timezone.now()
user.save()
5. Création de la vue de vérification et de renvoie du code
Implémentez une vue VerifyEmailView pour afficher un formulaire de saisie de l’OTP. Cette vue permet à l'utilisateur d'entrer le code reçu par email et de valider son adresse email.
class VerifyEmailView(View):
template_name = 'registration/verification.html'
def get(self, request, user_id):
user = get_object_or_404(User, id=user_id)
form = VerificationEmailForm()
return render(self.request, template_name=self.template_name, context={"form": form, "user_id": user.id})
def post(self, request, user_id):
user = get_object_or_404(User, id=user_id)
form = VerificationEmailForm(self.request.POST)
if form.is_valid():
try:
otp = form.cleaned_data["otp"]
OtpVerifyService().done(user, otp)
messages.success(request, "Verification successful", "success")
return redirect("login")
except OtpVerifyError as e:
messages.success(request, e, "success")
return render(self.request, template_name=self.template_name, context={"form": form, "user_id": user.id})
def resend_verification_email(request, user_id):
user = get_object_or_404(User, id=user_id)
with transaction.atomic():
otp = OtpService().create(user)
messages.success(request, "We have resend a verification mail", "success")
try:
send_opt(otp)
except smtplib.SMTPException:
user.delete()
messages.error(request, "Error when sending mail please try again later", "danger")
return redirect(reverse("users:verify-email", kwargs={"user_id": user.id}))
Puis on ajoute les URLs correspondant dans le fichier urls.py de l’application users :
urlpatterns = [
path('signup/', SignUpView.as_view(), name='signup'),
path('users/logout/', CustomLogoutView.as_view(), name='logout'),
path('verify-email/', VerifyEmailView.as_view(), name='verify-email'),
path('verify-email/<int:user_id>/', VerifyEmailView.as_view(), name='verify-email'),
path('resend-verification-email/<int:user_id>/', resend_verification_email, name='resend-verification-email'),
]
6. Modification de la vue d’inscription pour la redirection vers la vérification
Modifiez la vue d’inscription existante pour rediriger l’utilisateur vers la page de vérification après l’inscription. L’utilisateur sera marqué comme inactif tant que son email n’aura pas été vérifié.
class SignUpView(CreateView):
form_class = CustomUserCreationForm
template_name = 'registration/signup.html'
def get_success_url(self):
return reverse('users:verify-email', kwargs={'user_id': self.object.id})
def form_valid(self, form):
response = super().form_valid(form)
user = form.instance
with transaction.atomic():
otp = OtpService().create(user)
messages.success(self.request, "We have send a verification mail", "success")
try:
send_opt(otp)
except smtplib.SMTPException:
user.delete()
messages.error(self.request, "Error when sending mail please try again later", "danger")
return response
7. Extension : Redirection des utilisateurs non vérifiés via un Middleware
Pour améliorer l’expérience utilisateur, nous allons configurer un middleware qui redirige automatiquement les utilisateurs vers la page de vérification s'ils tentent de se connecter avec un compte non vérifié. Ce middleware intercepte les tentatives de connexion, vérifie l’état d’activation de l’utilisateur et redirige l’utilisateur vers la page de vérification si son compte est inactif.
-
Création du Middleware de Redirection
Dans le fichier users/middlewares.py, créez le middleware InactiveUserRedirectMiddleware. Lorsqu’un utilisateur tente de se connecter avec un compte inactif, le middleware intercepte la requête et redirige vers la page de vérification avec un message d’avertissement.
from django.contrib import messages
from django.contrib.auth import authenticate
from django.shortcuts import redirect
from django.urls import reverse
class InactiveUserRedirectMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.path == reverse('login') and request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
user = authenticate(request, username=username, password=password)
if user is not None and not user.email_verified_at:
messages.warning(request, 'Account not verified, verify your email box or send another verification '
'email.',
extra_tags='warning')
return redirect(reverse('users:verify-email', kwargs={"user_id": user.id}))
return self.get_response(request)
-
Paramétrage du Middleware dans les Paramètres
Ajoutez InactiveUserRedirectMiddleware à la liste des middlewares dans settings.py pour qu'il soit actif dans l’application.
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'users.middlewares.InactiveUserRedirectMiddleware',
]
8. Tests visuels
Présentez des captures d’écran pour illustrer chaque étape du processus :

-
Page de vérification avec saisie de l’OTP
-
Redirection vers la page de connexion après la vérification
-
Connexion réussie après vérification

Conclusion
En implémentant ce système de vérification par OTP, nous avons non seulement sécurisé le processus d'inscription, mais nous avons également ajouté un contrôle automatique de la vérification de l'e-mail lors de la connexion. Ce mécanisme garantit que seuls les utilisateurs ayant une adresse e-mail confirmée peuvent accéder à leur compte. Cette approche renforce ainsi la sécurité et améliore l’expérience utilisateur en simplifiant le processus de validation d’e-mail. Ainsi s’achève cet article, prochainement nous verrons comment gérer les mots de passe oublier et le changement de mot de passe. Vous pouvez à tout moment voire le code de cet article en cliquant ici.