Skip to content

Commit 0ec8ac8

Browse files
✨(web) add 2FA authentication on Django admin
1 parent ee3b2aa commit 0ec8ac8

5 files changed

Lines changed: 85 additions & 2 deletions

File tree

src/web/config/settings/base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@
8383
"django_htmx",
8484
"huey.contrib.djhuey",
8585
"dsfr",
86+
"django_otp",
87+
"django_otp.plugins.otp_totp",
88+
"django_otp.plugins.otp_static",
8689
"infrastructure.django_apps.shared",
8790
"infrastructure.django_apps.ingestion",
8891
"infrastructure.django_apps.candidate",
@@ -99,6 +102,7 @@
99102
"django.middleware.common.CommonMiddleware",
100103
"django.middleware.csrf.CsrfViewMiddleware",
101104
"django.contrib.auth.middleware.AuthenticationMiddleware",
105+
"django_otp.middleware.OTPMiddleware",
102106
"django.contrib.messages.middleware.MessageMiddleware",
103107
"django.middleware.csp.ContentSecurityPolicyMiddleware",
104108
"django.middleware.clickjacking.XFrameOptionsMiddleware",

src/web/config/urls.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from django.conf import settings
22
from django.contrib import admin
33
from django.urls import URLPattern, URLResolver, include, path
4+
from django_otp.admin import OTPAdminSite
45

56
from presentation.api import urls as api_urls
67
from presentation.ats import urls as ats_urls
@@ -9,6 +10,8 @@
910
from presentation.pages import urls as pages_urls
1011
from presentation.users import urls as users_urls
1112

13+
admin.site.__class__ = OTPAdminSite
14+
1215
urlpatterns: list[URLPattern | URLResolver] = [
1316
path("", include(pages_urls)),
1417
path("api/", include(api_urls)),

src/web/pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ dependencies = [
2626
"pypdf>=6.6.0,<7.0.0",
2727
"python-dateutil>=2.9.0.post0",
2828
"qdrant-client>=1.17.0",
29+
"django-otp>=1.5.4,<2.0.0",
30+
"qrcode>=8.0,<9.0",
2931
"redis>=7.4.0",
3032
"requests>=2.32.5,<2.33.0",
3133
"sentry-sdk[django]>=2.43.0,<3.0.0",
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from http import HTTPStatus
2+
3+
import pytest
4+
from django.test import Client
5+
from django_otp.oath import totp
6+
from django_otp.plugins.otp_totp.models import TOTPDevice
7+
8+
from tests.factories.utilisateur_factory import DEFAULT_PASSWORD, UtilisateurFactory
9+
10+
11+
@pytest.mark.django_db
12+
class TestAdminOTPRequired:
13+
def test_admin_shows_totp_input_for_staff_without_otp_device(self, client: Client):
14+
user = UtilisateurFactory.create_model()
15+
user.is_staff = True
16+
user.is_superuser = True
17+
user.save()
18+
19+
client.login(username=user.email, password=DEFAULT_PASSWORD)
20+
response = client.get("/admin/", follow=True)
21+
22+
assert response.status_code == HTTPStatus.OK
23+
assert b'id="id_otp_token"' in response.content
24+
25+
def test_admin_grants_access_with_valid_totp_token(self, client: Client):
26+
user = UtilisateurFactory.create_model()
27+
user.is_staff = True
28+
user.is_superuser = True
29+
user.save()
30+
31+
device = TOTPDevice.objects.create(user=user, confirmed=True)
32+
token = totp(device.bin_key)
33+
34+
response = client.post(
35+
"/admin/login/",
36+
{
37+
"username": user.email,
38+
"password": DEFAULT_PASSWORD,
39+
"otp_token": token,
40+
"next": "/admin/",
41+
},
42+
follow=True,
43+
)
44+
45+
assert response.status_code == HTTPStatus.OK
46+
assert response.wsgi_request.user.is_verified()

src/web/uv.lock

Lines changed: 30 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)