feat(auth): two-factor authentication (TOTP, passkeys, recovery codes)
Adopt django-allauth with allauth.mfa, giving TOTP, WebAuthn passkeys and recovery codes — and the signup/password-reset flows we'll need next. There was no login UI at all before this (only /admin/), so this brings the auth stack. The critical piece is authentication/adapters.py. A passkey is bound to a WebAuthn Relying Party ID (a domain), and allauth derives that from the request host — which under our subdomain tenancy would bind a passkey to a *single* club (ajax-united.clubmanager.app) and silently fail at every other one. The adapter pins the RP ID to CLUBMANAGER_BASE_DOMAIN so one passkey works across all clubs. Note this cuts both ways: changing that base domain invalidates every existing passkey. RequireMFAMiddleware makes a second factor mandatory for anyone who can change other people's data — Django staff/superusers and holders of an elevated ClubRole (ADMIN/EDITOR), via the access service — while leaving it optional for regular members. /admin/login/ is routed through allauth, since Django's own admin login knows nothing about second factors. allauth is installed WITHOUT django.contrib.sites (optional since allauth 65), so ARCHITECTURE.md's rejection of the Sites framework stands and no Club.site bridge is needed. Sessions are shared across club subdomains, matching the one-passkey-everywhere model; tenancy still scopes what you can see. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import datetime
|
||||
import uuid
|
||||
from contextlib import contextmanager
|
||||
|
||||
from allauth.mfa.models import Authenticator
|
||||
from django.contrib import admin as django_admin
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -461,6 +462,9 @@ class AdminRegistrationSmokeTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.admin = get_user_model().objects.create_superuser(email="root@club.test", password="pw-secret-123")
|
||||
# Staff must hold a second factor (RequireMFAMiddleware), else they are
|
||||
# redirected to enrolment instead of reaching the admin.
|
||||
Authenticator.objects.create(user=self.admin, type=Authenticator.Type.TOTP, data={"secret": "JBSWY3DPEHPK3PXP"})
|
||||
self.client.force_login(self.admin)
|
||||
|
||||
def test_every_model_is_registered_in_admin(self):
|
||||
|
||||
Reference in New Issue
Block a user