Added tests for FIDO auth

This commit is contained in:
Antoine Nguyen
2024-07-17 11:22:37 +02:00
parent 33c99f1c98
commit beae88dab7
5 changed files with 102 additions and 89 deletions
+28 -87
View File
@@ -197,20 +197,6 @@ class AccountViewSetTestCase(ModoAPITestCase):
resp = self.client.post(url, {"password": password_ok}, format="json")
self.assertEqual(resp.status_code, 200)
# @mock.patch("django_otp.match_token")
# def test_tfa_verify_code(self, match_mock):
# user = models.User.objects.get(username="admin")
# user.totpdevice_set.create(name="Device")
# user.tfa_enabled = True
# user.save()
# url = reverse("v2:account-tfa-verify-code")
# match_mock.side_effect = [user.totpdevice_set.first()]
# data = {"code": "1234"}
# resp = self.client.post(url, data, format="json")
# self.assertEqual(resp.status_code, 200)
# self.assertIn("access", resp.json())
def test_tfa_setup(self):
# Setup TFA
url = reverse("v2:account-tfa-setup-get-key")
@@ -275,41 +261,6 @@ class AccountViewSetTestCase(ModoAPITestCase):
user.refresh_from_db()
self.assertEqual(user.totp_enabled, False)
# def test_api_token(self):
# # 1. Obtain a JWT token so we can safely play with basic token
# url = reverse("v2:token_obtain_pair")
# data = {"username": "admin", "password": "password"}
# resp = self.client.post(url, data, format="json")
# self.assertEqual(resp.status_code, 200)
# self.client.credentials(
# HTTP_AUTHORIZATION="Bearer {}".format(resp.json()["access"])
# )
# url = reverse("v2:account-manage-api-token")
# resp = self.client.get(url)
# self.assertEqual(resp.status_code, 200)
# self.assertEqual(resp.json()["token"], self.token.key)
# resp = self.client.delete(url)
# self.assertEqual(resp.status_code, 204)
# resp = self.client.post(url)
# self.assertEqual(resp.status_code, 201)
# def test_failed_api_token(self):
# """Simulate a failed login attempt and check that it is logged."""
# url = reverse("v2:token_obtain_pair")
# data = {"username": "clearly_non_existent_user", "password": "password"}
# with self.assertLogs(logger="modoboa.auth", level="WARNING") as log:
# resp = self.client.post(url, data, format="json")
# self.assertEqual(resp.status_code, 401)
# self.assertIn(
# "WARNING:modoboa.auth:Failed connection attempt from '127.0.0.1'"
# " as user 'clearly_non_existent_user'",
# log.output,
# )
class PasswordResetTestCase(AccountViewSetTestCase):
def __init__(self, *args, **kwargs):
@@ -533,44 +484,6 @@ class PasswordResetTestCase(AccountViewSetTestCase):
# TODO: See why user doesn't update it's password --> self.test_me_password(password_ok="MyHardenedPass1!")
# class AuthenticationTestCase(ModoAPITestCase):
# @mock.patch("django_otp.match_token")
# def test_2fa(self, match_mock):
# url = reverse("v2:token_obtain_pair")
# me_url = reverse("v2:account-me")
# data = {"username": "admin", "password": "password"}
# resp = self.client.post(url, data, format="json")
# self.assertEqual(resp.status_code, 200)
# self.client.credentials(
# HTTP_AUTHORIZATION="Bearer {}".format(resp.json()["access"])
# )
# resp = self.client.get(me_url)
# self.assertEqual(resp.status_code, 200)
# # Now we enable 2FA
# user = models.User.objects.get(username="admin")
# user.totpdevice_set.create(name="Device")
# user.tfa_enabled = True
# user.save()
# resp = self.client.get(me_url)
# self.assertEqual(resp.status_code, 418)
# # Verify code
# url = reverse("v2:account-tfa-verify-code")
# match_mock.side_effect = [user.totpdevice_set.first()]
# data = {"code": "1234"}
# resp = self.client.post(url, data, format="json")
# self.assertEqual(resp.status_code, 200)
# self.client.credentials(
# HTTP_AUTHORIZATION="Bearer {}".format(resp.json()["access"])
# )
# resp = self.client.get(me_url)
# self.assertEqual(resp.status_code, 200)
class LanguageViewSetTestCase(ModoAPITestCase):
def test_list(self):
url = reverse("v2:language-list")
@@ -583,3 +496,31 @@ class ComponentAPITestCase(ModoAPITestCase):
url = reverse("v2:components_information")
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
class AuthenticatorData(bytes):
credential_data = b"RESPONSE"
class FIDOViewSetTestCase(ModoAPITestCase):
@mock.patch("fido2.server.Fido2Server.register_complete")
def test_registration(self, register_complete_mock):
url = reverse("v2:fido-registration-begin")
resp = self.client.post(url)
self.assertEqual(resp.status_code, 200)
register_complete_mock.side_effect = [AuthenticatorData()]
data = {
"type": "type",
"id": "XX",
"rawId": "XX",
"authenticatorAttachment": "attachment",
"response": {"key": "value"},
"name": "Name",
}
url = reverse("v2:fido-registration-end")
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertIn("tokens", resp.json())
+12
View File
@@ -1,5 +1,7 @@
"""Factories for core application."""
import base64
import factory
from django.contrib.auth.models import Group
@@ -58,3 +60,13 @@ class LogFactory(factory.django.DjangoModelFactory):
logger = "modoboa.admin"
message = "A message"
level = "info"
class UserFidoKeyFactory(factory.django.DjangoModelFactory):
"""Factory for UserFidoKey."""
class Meta:
model = models.UserFidoKey
name = factory.Sequence(lambda n: f"Name {n}")
credential_data = "data"
+2
View File
@@ -418,6 +418,8 @@ reversion.register(User)
class UserFidoKey(models.Model):
"""Model to store user fido keys."""
user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
added_on = models.DateTimeField(auto_now_add=True)
+60
View File
@@ -13,6 +13,7 @@ try:
except ImportError:
argon2 = None
from modoboa.core import constants
from modoboa.core.password_hashers import get_password_hasher
from modoboa.core.password_hashers.utils import get_dovecot_schemes
from modoboa.lib.tests import NO_SMTP, ModoTestCase
@@ -21,6 +22,16 @@ from .. import factories, models
DOVEADM_TEST_PATH = "{}/doveadm".format(os.path.dirname(__file__))
class MockedAttestedCredentialData:
def __init__(self, _):
pass
@staticmethod
def _parse(data):
return "1", "XX", "3", None
class AuthenticationTestCase(ModoTestCase):
"""Validate authentication scenarios."""
@@ -229,6 +240,55 @@ class AuthenticationTestCase(ModoTestCase):
supported_schemes = get_dovecot_schemes()[0]
self.assertEqual(supported_schemes, ["{MD5-CRYPT}", "{PLAIN}"])
def test_fido_auth_begin(self):
url = reverse("core:fido_auth_begin")
resp = self.client.post(url)
self.assertEqual(resp.status_code, 403)
session = self.client.session
session[constants.TFA_PRE_VERIFY_USER_PK] = self.account.pk
session.save()
resp = self.client.post(url)
self.assertEqual(resp.status_code, 200)
@mock.patch("fido2.webauthn.AttestedCredentialData", MockedAttestedCredentialData)
@mock.patch("fido2.server.Fido2Server.authenticate_complete")
@mock.patch("fido2.utils.websafe_decode")
def test_fido_auth_end(self, websafe_decode_mock, authenticate_complete_mock):
data = {
"authenticatorAttachment": "attachment",
"clientExtensionResults": '{"key": "value"}',
"id": "XX",
"rawId": "XX",
"response": '{"key": "value"}',
"type": "type",
}
url = reverse("core:fido_auth_end")
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 403)
session = self.client.session
session[constants.TFA_PRE_VERIFY_USER_PK] = self.account.pk
session[constants.TFA_PRE_VERIFY_USER_BACKEND] = (
"django.contrib.auth.backends.ModelBackend"
)
session["fido_state"] = "state"
session.save()
class AuthenticateMockResult:
credential_id = "XX"
key = factories.UserFidoKeyFactory(user=self.account)
authenticate_complete_mock.side_effect = [AuthenticateMockResult()]
websafe_decode_mock.side_effect = [
bytes.fromhex(
"f8a011f38c0a4d15800617111f9edc7d0040fe3aac036d14c1e1c65518b698dd1da8f596bc33e11072813466c6bf3845691509b80fb76d59309b8d39e0a93452688f6ca3a39a76f3fc52744fb73948b15783a5010203262001215820643566c206dd00227005fa5de69320616ca268043a38f08bde2e9dc45a5cafaf225820171353b2932434703726aae579fa6542432861fe591e481ea22d63997e1a5290" # noqa E501
)
]
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 200)
key.refresh_from_db()
self.assertEqual(key.use_count, 1)
class PasswordResetTestCase(ModoTestCase):
"""Test password reset service."""
-2
View File
@@ -7,11 +7,9 @@ from operator import itemgetter
import socket
import ssl
from django.utils.encoding import smart_bytes
from django.utils.translation import gettext as _
from modoboa.lib import imap_utf7 # NOQA
from modoboa.lib.connections import ConnectionsManager
from modoboa.lib.exceptions import ModoboaException, InternalError
from modoboa.parameters import tools as param_tools