Skip to content

Commit

Permalink
fix(mfa): WebAuthn authenticator was not recorded on 2FA login
Browse files Browse the repository at this point in the history
  • Loading branch information
pennersr committed Sep 21, 2024
1 parent 1de29cb commit db1d084
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 5 deletions.
6 changes: 5 additions & 1 deletion allauth/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,11 @@ def passkey(user):
authenticator = Authenticator.objects.create(
user=user,
type=Authenticator.Type.WEBAUTHN,
data={"name": "Test passkey", "passwordless": True},
data={
"name": "Test passkey",
"passwordless": True,
"credential": {},
},
)
return authenticator

Expand Down
9 changes: 8 additions & 1 deletion allauth/headless/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ def force_login(self, user):
self.session_token = self.session.session_key
return ret

def headless_session(self):
from allauth.headless.internal import sessionkit

return sessionkit.session_store(self.session_token)


@pytest.fixture
def app_client():
Expand All @@ -46,7 +51,9 @@ def app_client():
@pytest.fixture
def client(headless_client):
if headless_client == "browser":
return Client()
client = Client()
client.headless_session = lambda: client.session
return client
return AppClient()


Expand Down
5 changes: 5 additions & 0 deletions allauth/headless/mfa/tests/test_webauthn.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import pytest

from allauth.account.authentication import AUTHENTICATION_METHODS_SESSION_KEY
from allauth.headless.constants import Flow
from allauth.mfa.models import Authenticator

Expand Down Expand Up @@ -190,6 +191,10 @@ def test_2fa_login(
data = resp.json()
assert resp.status_code == 200
assert data["data"]["user"]["id"] == passkey.user_id
assert client.headless_session()[AUTHENTICATION_METHODS_SESSION_KEY] == [
{"method": "password", "at": ANY, "username": passkey.user.username},
{"method": "mfa", "at": ANY, "id": ANY, "type": Authenticator.Type.WEBAUTHN},
]


def test_passkey_signup(client, db, webauthn_registration_bypass, headless_reverse):
Expand Down
20 changes: 17 additions & 3 deletions allauth/mfa/webauthn/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
from allauth.core import context
from allauth.mfa import app_settings
from allauth.mfa.adapter import get_adapter
from allauth.mfa.base.internal.flows import check_rate_limit
from allauth.mfa.base.internal.flows import (
check_rate_limit,
post_authentication,
)
from allauth.mfa.models import Authenticator
from allauth.mfa.webauthn.internal import auth, flows

Expand Down Expand Up @@ -67,6 +70,8 @@ class SignupWebAuthnForm(_BaseAddWebAuthnForm):

class AuthenticateWebAuthnForm(forms.Form):
credential = forms.JSONField(required=True, widget=forms.HiddenInput)
reauthenticated = False
passwordless = False

def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user")
Expand All @@ -88,16 +93,25 @@ def clean_credential(self):

def save(self):
authenticator = self.cleaned_data["credential"]
authenticator.record_usage()
post_authentication(
context.request,
authenticator,
reauthenticated=self.reauthenticated,
passwordless=self.passwordless,
)


class LoginWebAuthnForm(AuthenticateWebAuthnForm):
reauthenticated = False
passwordless = True

def __init__(self, *args, **kwargs):
super().__init__(*args, user=None, **kwargs)


class ReauthenticateWebAuthnForm(AuthenticateWebAuthnForm):
pass
reauthenticated = True
passwordless = False


class EditWebAuthnForm(forms.Form):
Expand Down
31 changes: 31 additions & 0 deletions allauth/mfa/webauthn/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import pytest
from pytest_django.asserts import assertTemplateUsed

from allauth.account.authentication import AUTHENTICATION_METHODS_SESSION_KEY
from allauth.mfa.models import Authenticator


Expand All @@ -20,6 +21,15 @@ def test_passkey_login(client, passkey, webauthn_authentication_bypass):
reverse("mfa_login_webauthn"), data={"credential": credential}
)
assert resp["location"] == settings.LOGIN_REDIRECT_URL
assert client.session[AUTHENTICATION_METHODS_SESSION_KEY] == [
{
"at": ANY,
"id": ANY,
"method": "mfa",
"passwordless": True,
"type": "webauthn",
}
]


def test_reauthenticate(
Expand Down Expand Up @@ -169,3 +179,24 @@ def test_passkey_signup(client, db, webauthn_registration_bypass):
reverse("mfa_signup_webauthn"), data={"credential": credential}
)
assert resp["location"] == settings.LOGIN_REDIRECT_URL


def test_webauthn_login(
client, user_with_passkey, passkey, user_password, webauthn_authentication_bypass
):
resp = client.post(
reverse("account_login"),
{"login": user_with_passkey.username, "password": user_password},
)
assert resp.status_code == 302
assert resp["location"] == reverse("mfa_authenticate")
with webauthn_authentication_bypass(passkey) as credential:
resp = client.get(reverse("mfa_authenticate"))
assert resp.status_code == 200
resp = client.post(reverse("mfa_authenticate"), {"credential": credential})
assert resp.status_code == 302
assert resp["location"] == settings.LOGIN_REDIRECT_URL
assert client.session[AUTHENTICATION_METHODS_SESSION_KEY] == [
{"method": "password", "at": ANY, "username": user_with_passkey.username},
{"method": "mfa", "at": ANY, "id": ANY, "type": Authenticator.Type.WEBAUTHN},
]

0 comments on commit db1d084

Please sign in to comment.