events: use custom login failed signal, also send for mfa errors, add stage and more to context (#3039)
* use custom login failed signal, also send for mfa errors, add stage and more to context closes #3027 Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * include device class in event Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * update tests Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
6739ded5a9
commit
fa04883ac1
|
@ -12,6 +12,8 @@ from django.http.request import HttpRequest
|
||||||
|
|
||||||
# Arguments: user: User, password: str
|
# Arguments: user: User, password: str
|
||||||
password_changed = Signal()
|
password_changed = Signal()
|
||||||
|
# Arguments: credentials: dict[str, any], request: HttpRequest, stage: Stage
|
||||||
|
login_failed = Signal()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from authentik.core.models import AuthenticatedSession, User
|
from authentik.core.models import AuthenticatedSession, User
|
||||||
|
|
|
@ -2,15 +2,16 @@
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed
|
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
||||||
from django.db.models.signals import post_save, pre_delete
|
from django.db.models.signals import post_save, pre_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.core.signals import password_changed
|
from authentik.core.signals import login_failed, password_changed
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.events.tasks import event_notification_handler, gdpr_cleanup
|
from authentik.events.tasks import event_notification_handler, gdpr_cleanup
|
||||||
|
from authentik.flows.models import Stage
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan
|
from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan
|
||||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||||
from authentik.stages.invitation.models import Invitation
|
from authentik.stages.invitation.models import Invitation
|
||||||
|
@ -77,11 +78,18 @@ def on_user_write(sender, request: HttpRequest, user: User, data: dict[str, Any]
|
||||||
thread.run()
|
thread.run()
|
||||||
|
|
||||||
|
|
||||||
@receiver(user_login_failed)
|
@receiver(login_failed)
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def on_user_login_failed(sender, credentials: dict[str, str], request: HttpRequest, **_):
|
def on_login_failed(
|
||||||
"""Failed Login"""
|
signal,
|
||||||
thread = EventNewThread(EventAction.LOGIN_FAILED, request, **credentials)
|
sender,
|
||||||
|
credentials: dict[str, str],
|
||||||
|
request: HttpRequest,
|
||||||
|
stage: Optional[Stage] = None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""Failed Login, authentik custom event"""
|
||||||
|
thread = EventNewThread(EventAction.LOGIN_FAILED, request, **credentials, stage=stage, **kwargs)
|
||||||
thread.run()
|
thread.run()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ from authentik.flows.challenge import (
|
||||||
)
|
)
|
||||||
from authentik.flows.models import InvalidResponseAction
|
from authentik.flows.models import InvalidResponseAction
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_PENDING_USER
|
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_PENDING_USER
|
||||||
|
from authentik.lib.utils.reflection import class_to_path
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from authentik.flows.views.executor import FlowExecutorView
|
from authentik.flows.views.executor import FlowExecutorView
|
||||||
|
@ -44,7 +45,7 @@ class StageView(View):
|
||||||
current_stage = getattr(self.executor, "current_stage", None)
|
current_stage = getattr(self.executor, "current_stage", None)
|
||||||
self.logger = get_logger().bind(
|
self.logger = get_logger().bind(
|
||||||
stage=getattr(current_stage, "name", None),
|
stage=getattr(current_stage, "name", None),
|
||||||
stage_view=self,
|
stage_view=class_to_path(type(self)),
|
||||||
)
|
)
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
"""authentik reputation request signals"""
|
"""authentik reputation request signals"""
|
||||||
from django.contrib.auth.signals import user_logged_in, user_login_failed
|
from django.contrib.auth.signals import user_logged_in
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.core.signals import login_failed
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.lib.utils.http import get_client_ip
|
from authentik.lib.utils.http import get_client_ip
|
||||||
from authentik.policies.reputation.models import CACHE_KEY_PREFIX
|
from authentik.policies.reputation.models import CACHE_KEY_PREFIX
|
||||||
|
@ -35,7 +36,7 @@ def update_score(request: HttpRequest, identifier: str, amount: int):
|
||||||
save_reputation.delay()
|
save_reputation.delay()
|
||||||
|
|
||||||
|
|
||||||
@receiver(user_login_failed)
|
@receiver(login_failed)
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def handle_failed_login(sender, request, credentials, **_):
|
def handle_failed_login(sender, request, credentials, **_):
|
||||||
"""Lower Score for failed login attempts"""
|
"""Lower Score for failed login attempts"""
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
"""test reputation signals and policy"""
|
"""test reputation signals and policy"""
|
||||||
from django.contrib.auth import authenticate
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.test import RequestFactory, TestCase
|
from django.test import RequestFactory, TestCase
|
||||||
|
|
||||||
|
@ -7,6 +6,8 @@ from authentik.core.models import User
|
||||||
from authentik.policies.reputation.models import CACHE_KEY_PREFIX, Reputation, ReputationPolicy
|
from authentik.policies.reputation.models import CACHE_KEY_PREFIX, Reputation, ReputationPolicy
|
||||||
from authentik.policies.reputation.tasks import save_reputation
|
from authentik.policies.reputation.tasks import save_reputation
|
||||||
from authentik.policies.types import PolicyRequest
|
from authentik.policies.types import PolicyRequest
|
||||||
|
from authentik.stages.password import BACKEND_INBUILT
|
||||||
|
from authentik.stages.password.stage import authenticate
|
||||||
|
|
||||||
|
|
||||||
class TestReputationPolicy(TestCase):
|
class TestReputationPolicy(TestCase):
|
||||||
|
@ -21,11 +22,14 @@ class TestReputationPolicy(TestCase):
|
||||||
cache.delete_many(keys)
|
cache.delete_many(keys)
|
||||||
# We need a user for the one-to-one in userreputation
|
# We need a user for the one-to-one in userreputation
|
||||||
self.user = User.objects.create(username=self.test_username)
|
self.user = User.objects.create(username=self.test_username)
|
||||||
|
self.backends = [BACKEND_INBUILT]
|
||||||
|
|
||||||
def test_ip_reputation(self):
|
def test_ip_reputation(self):
|
||||||
"""test IP reputation"""
|
"""test IP reputation"""
|
||||||
# Trigger negative reputation
|
# Trigger negative reputation
|
||||||
authenticate(self.request, username=self.test_username, password=self.test_username)
|
authenticate(
|
||||||
|
self.request, self.backends, username=self.test_username, password=self.test_username
|
||||||
|
)
|
||||||
# Test value in cache
|
# Test value in cache
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
cache.get(CACHE_KEY_PREFIX + self.test_ip + self.test_username),
|
cache.get(CACHE_KEY_PREFIX + self.test_ip + self.test_username),
|
||||||
|
@ -38,7 +42,9 @@ class TestReputationPolicy(TestCase):
|
||||||
def test_user_reputation(self):
|
def test_user_reputation(self):
|
||||||
"""test User reputation"""
|
"""test User reputation"""
|
||||||
# Trigger negative reputation
|
# Trigger negative reputation
|
||||||
authenticate(self.request, username=self.test_username, password=self.test_username)
|
authenticate(
|
||||||
|
self.request, self.backends, username=self.test_username, password=self.test_username
|
||||||
|
)
|
||||||
# Test value in cache
|
# Test value in cache
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
cache.get(CACHE_KEY_PREFIX + self.test_ip + self.test_username),
|
cache.get(CACHE_KEY_PREFIX + self.test_ip + self.test_username),
|
||||||
|
|
|
@ -18,9 +18,12 @@ from webauthn.helpers.structs import AuthenticationCredential
|
||||||
|
|
||||||
from authentik.core.api.utils import PassiveSerializer
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
|
from authentik.core.signals import login_failed
|
||||||
|
from authentik.flows.stage import StageView
|
||||||
from authentik.lib.utils.http import get_client_ip
|
from authentik.lib.utils.http import get_client_ip
|
||||||
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
|
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
|
||||||
from authentik.stages.authenticator_sms.models import SMSDevice
|
from authentik.stages.authenticator_sms.models import SMSDevice
|
||||||
|
from authentik.stages.authenticator_validate.models import DeviceClasses
|
||||||
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
||||||
from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE
|
from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE
|
||||||
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
|
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
|
||||||
|
@ -92,24 +95,32 @@ def select_challenge_sms(request: HttpRequest, device: SMSDevice):
|
||||||
device.stage.send(device.token, device)
|
device.stage.send(device.token, device)
|
||||||
|
|
||||||
|
|
||||||
def validate_challenge_code(code: str, request: HttpRequest, user: User) -> Device:
|
def validate_challenge_code(code: str, stage_view: StageView, user: User) -> Device:
|
||||||
"""Validate code-based challenges. We test against every device, on purpose, as
|
"""Validate code-based challenges. We test against every device, on purpose, as
|
||||||
the user mustn't choose between totp and static devices."""
|
the user mustn't choose between totp and static devices."""
|
||||||
device = match_token(user, code)
|
device = match_token(user, code)
|
||||||
if not device:
|
if not device:
|
||||||
|
login_failed.send(
|
||||||
|
sender=__name__,
|
||||||
|
credentials={"username": user.username},
|
||||||
|
request=stage_view.request,
|
||||||
|
stage=stage_view.executor.current_stage,
|
||||||
|
device_class=DeviceClasses.TOTP.value,
|
||||||
|
)
|
||||||
raise ValidationError(_("Invalid Token"))
|
raise ValidationError(_("Invalid Token"))
|
||||||
return device
|
return device
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def validate_challenge_webauthn(data: dict, request: HttpRequest, user: User) -> Device:
|
def validate_challenge_webauthn(data: dict, stage_view: StageView, user: User) -> Device:
|
||||||
"""Validate WebAuthn Challenge"""
|
"""Validate WebAuthn Challenge"""
|
||||||
|
request = stage_view.request
|
||||||
challenge = request.session.get(SESSION_KEY_WEBAUTHN_CHALLENGE)
|
challenge = request.session.get(SESSION_KEY_WEBAUTHN_CHALLENGE)
|
||||||
credential_id = data.get("id")
|
credential_id = data.get("id")
|
||||||
|
|
||||||
device = WebAuthnDevice.objects.filter(credential_id=credential_id).first()
|
device = WebAuthnDevice.objects.filter(credential_id=credential_id).first()
|
||||||
if not device:
|
if not device:
|
||||||
raise ValidationError("Device does not exist.")
|
raise Http404()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
authentication_verification = verify_authentication_response(
|
authentication_verification = verify_authentication_response(
|
||||||
|
@ -121,16 +132,23 @@ def validate_challenge_webauthn(data: dict, request: HttpRequest, user: User) ->
|
||||||
credential_current_sign_count=device.sign_count,
|
credential_current_sign_count=device.sign_count,
|
||||||
require_user_verification=False,
|
require_user_verification=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
except InvalidAuthenticationResponse as exc:
|
except InvalidAuthenticationResponse as exc:
|
||||||
LOGGER.warning("Assertion failed", exc=exc)
|
LOGGER.warning("Assertion failed", exc=exc)
|
||||||
|
login_failed.send(
|
||||||
|
sender=__name__,
|
||||||
|
credentials={"username": user.username},
|
||||||
|
request=stage_view.request,
|
||||||
|
stage=stage_view.executor.current_stage,
|
||||||
|
device=device,
|
||||||
|
device_class=DeviceClasses.WEBAUTHN.value,
|
||||||
|
)
|
||||||
raise ValidationError("Assertion failed") from exc
|
raise ValidationError("Assertion failed") from exc
|
||||||
|
|
||||||
device.set_sign_count(authentication_verification.new_sign_count)
|
device.set_sign_count(authentication_verification.new_sign_count)
|
||||||
return device
|
return device
|
||||||
|
|
||||||
|
|
||||||
def validate_challenge_duo(device_pk: int, request: HttpRequest, user: User) -> Device:
|
def validate_challenge_duo(device_pk: int, stage_view: StageView, user: User) -> Device:
|
||||||
"""Duo authentication"""
|
"""Duo authentication"""
|
||||||
device = get_object_or_404(DuoDevice, pk=device_pk)
|
device = get_object_or_404(DuoDevice, pk=device_pk)
|
||||||
if device.user != user:
|
if device.user != user:
|
||||||
|
@ -140,13 +158,20 @@ def validate_challenge_duo(device_pk: int, request: HttpRequest, user: User) ->
|
||||||
response = stage.client.auth(
|
response = stage.client.auth(
|
||||||
"auto",
|
"auto",
|
||||||
user_id=device.duo_user_id,
|
user_id=device.duo_user_id,
|
||||||
ipaddr=get_client_ip(request),
|
ipaddr=get_client_ip(stage_view.request),
|
||||||
type="authentik Login request",
|
type="authentik Login request",
|
||||||
display_username=user.username,
|
display_username=user.username,
|
||||||
device="auto",
|
device="auto",
|
||||||
)
|
)
|
||||||
# {'result': 'allow', 'status': 'allow', 'status_msg': 'Success. Logging you in...'}
|
# {'result': 'allow', 'status': 'allow', 'status_msg': 'Success. Logging you in...'}
|
||||||
if response["result"] == "deny":
|
if response["result"] == "deny":
|
||||||
|
login_failed.send(
|
||||||
|
sender=__name__,
|
||||||
|
credentials={"username": user.username},
|
||||||
|
request=stage_view.request,
|
||||||
|
stage=stage_view.executor.current_stage,
|
||||||
|
device_class=DeviceClasses.DUO.value,
|
||||||
|
)
|
||||||
raise ValidationError("Duo denied access")
|
raise ValidationError("Duo denied access")
|
||||||
device.save()
|
device.save()
|
||||||
return device
|
return device
|
||||||
|
|
|
@ -14,7 +14,7 @@ class DeviceClasses(models.TextChoices):
|
||||||
"""Device classes this stage can validate"""
|
"""Device classes this stage can validate"""
|
||||||
|
|
||||||
# device class must match Device's class name so StaticDevice -> static
|
# device class must match Device's class name so StaticDevice -> static
|
||||||
STATIC = "static"
|
STATIC = "static", _("Static")
|
||||||
TOTP = "totp", _("TOTP")
|
TOTP = "totp", _("TOTP")
|
||||||
WEBAUTHN = "webauthn", _("WebAuthn")
|
WEBAUTHN = "webauthn", _("WebAuthn")
|
||||||
DUO = "duo", _("Duo")
|
DUO = "duo", _("Duo")
|
||||||
|
|
|
@ -85,9 +85,7 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
|
||||||
def validate_code(self, code: str) -> str:
|
def validate_code(self, code: str) -> str:
|
||||||
"""Validate code-based response, raise error if code isn't allowed"""
|
"""Validate code-based response, raise error if code isn't allowed"""
|
||||||
self._challenge_allowed([DeviceClasses.TOTP, DeviceClasses.STATIC, DeviceClasses.SMS])
|
self._challenge_allowed([DeviceClasses.TOTP, DeviceClasses.STATIC, DeviceClasses.SMS])
|
||||||
self.device = validate_challenge_code(
|
self.device = validate_challenge_code(code, self.stage, self.stage.get_pending_user())
|
||||||
code, self.stage.request, self.stage.get_pending_user()
|
|
||||||
)
|
|
||||||
return code
|
return code
|
||||||
|
|
||||||
def validate_webauthn(self, webauthn: dict) -> dict:
|
def validate_webauthn(self, webauthn: dict) -> dict:
|
||||||
|
@ -95,14 +93,14 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
|
||||||
or response is invalid"""
|
or response is invalid"""
|
||||||
self._challenge_allowed([DeviceClasses.WEBAUTHN])
|
self._challenge_allowed([DeviceClasses.WEBAUTHN])
|
||||||
self.device = validate_challenge_webauthn(
|
self.device = validate_challenge_webauthn(
|
||||||
webauthn, self.stage.request, self.stage.get_pending_user()
|
webauthn, self.stage, self.stage.get_pending_user()
|
||||||
)
|
)
|
||||||
return webauthn
|
return webauthn
|
||||||
|
|
||||||
def validate_duo(self, duo: int) -> int:
|
def validate_duo(self, duo: int) -> int:
|
||||||
"""Initiate Duo authentication"""
|
"""Initiate Duo authentication"""
|
||||||
self._challenge_allowed([DeviceClasses.DUO])
|
self._challenge_allowed([DeviceClasses.DUO])
|
||||||
self.device = validate_challenge_duo(duo, self.stage.request, self.stage.get_pending_user())
|
self.device = validate_challenge_duo(duo, self.stage, self.stage.get_pending_user())
|
||||||
return duo
|
return duo
|
||||||
|
|
||||||
def validate_selected_challenge(self, challenge: dict) -> dict:
|
def validate_selected_challenge(self, challenge: dict) -> dict:
|
||||||
|
|
|
@ -5,7 +5,9 @@ from django.test.client import RequestFactory
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
from authentik.core.tests.utils import create_test_admin_user
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
|
from authentik.flows.stage import StageView
|
||||||
from authentik.flows.tests import FlowTestCase
|
from authentik.flows.tests import FlowTestCase
|
||||||
|
from authentik.flows.views.executor import FlowExecutorView
|
||||||
from authentik.lib.generators import generate_id, generate_key
|
from authentik.lib.generators import generate_id, generate_key
|
||||||
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
|
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
|
||||||
from authentik.stages.authenticator_validate.challenge import validate_challenge_duo
|
from authentik.stages.authenticator_validate.challenge import validate_challenge_duo
|
||||||
|
@ -22,7 +24,7 @@ class AuthenticatorValidateStageDuoTests(FlowTestCase):
|
||||||
"""Test duo"""
|
"""Test duo"""
|
||||||
request = self.request_factory.get("/")
|
request = self.request_factory.get("/")
|
||||||
stage = AuthenticatorDuoStage.objects.create(
|
stage = AuthenticatorDuoStage.objects.create(
|
||||||
name="test",
|
name=generate_id(),
|
||||||
client_id=generate_id(),
|
client_id=generate_id(),
|
||||||
client_secret=generate_key(),
|
client_secret=generate_key(),
|
||||||
api_hostname="",
|
api_hostname="",
|
||||||
|
@ -45,10 +47,21 @@ class AuthenticatorValidateStageDuoTests(FlowTestCase):
|
||||||
"authentik.stages.authenticator_duo.models.AuthenticatorDuoStage.client",
|
"authentik.stages.authenticator_duo.models.AuthenticatorDuoStage.client",
|
||||||
duo_mock,
|
duo_mock,
|
||||||
):
|
):
|
||||||
self.assertEqual(duo_device, validate_challenge_duo(duo_device.pk, request, self.user))
|
self.assertEqual(
|
||||||
|
duo_device,
|
||||||
|
validate_challenge_duo(
|
||||||
|
duo_device.pk,
|
||||||
|
StageView(FlowExecutorView(current_stage=stage), request=request),
|
||||||
|
self.user,
|
||||||
|
),
|
||||||
|
)
|
||||||
with patch(
|
with patch(
|
||||||
"authentik.stages.authenticator_duo.models.AuthenticatorDuoStage.client",
|
"authentik.stages.authenticator_duo.models.AuthenticatorDuoStage.client",
|
||||||
failed_duo_mock,
|
failed_duo_mock,
|
||||||
):
|
):
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
validate_challenge_duo(duo_device.pk, request, self.user)
|
validate_challenge_duo(
|
||||||
|
duo_device.pk,
|
||||||
|
StageView(FlowExecutorView(current_stage=stage), request=request),
|
||||||
|
self.user,
|
||||||
|
)
|
||||||
|
|
|
@ -7,6 +7,7 @@ from django.urls.base import reverse
|
||||||
from authentik.core.tests.utils import create_test_admin_user
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
from authentik.flows.models import Flow, FlowStageBinding, NotConfiguredAction
|
from authentik.flows.models import Flow, FlowStageBinding, NotConfiguredAction
|
||||||
from authentik.flows.tests import FlowTestCase
|
from authentik.flows.tests import FlowTestCase
|
||||||
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.stages.authenticator_sms.models import AuthenticatorSMSStage, SMSDevice, SMSProviders
|
from authentik.stages.authenticator_sms.models import AuthenticatorSMSStage, SMSDevice, SMSProviders
|
||||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
||||||
from authentik.stages.authenticator_validate.stage import COOKIE_NAME_MFA
|
from authentik.stages.authenticator_validate.stage import COOKIE_NAME_MFA
|
||||||
|
@ -28,7 +29,7 @@ class AuthenticatorValidateStageSMSTests(FlowTestCase):
|
||||||
def test_last_auth_threshold(self):
|
def test_last_auth_threshold(self):
|
||||||
"""Test last_auth_threshold"""
|
"""Test last_auth_threshold"""
|
||||||
ident_stage = IdentificationStage.objects.create(
|
ident_stage = IdentificationStage.objects.create(
|
||||||
name="conf",
|
name=generate_id(),
|
||||||
user_fields=[
|
user_fields=[
|
||||||
UserFields.USERNAME,
|
UserFields.USERNAME,
|
||||||
],
|
],
|
||||||
|
@ -40,7 +41,7 @@ class AuthenticatorValidateStageSMSTests(FlowTestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
stage = AuthenticatorValidateStage.objects.create(
|
stage = AuthenticatorValidateStage.objects.create(
|
||||||
name="foo",
|
name=generate_id(),
|
||||||
last_auth_threshold="milliseconds=0",
|
last_auth_threshold="milliseconds=0",
|
||||||
not_configured_action=NotConfiguredAction.CONFIGURE,
|
not_configured_action=NotConfiguredAction.CONFIGURE,
|
||||||
device_classes=[DeviceClasses.SMS],
|
device_classes=[DeviceClasses.SMS],
|
||||||
|
@ -65,7 +66,7 @@ class AuthenticatorValidateStageSMSTests(FlowTestCase):
|
||||||
def test_last_auth_threshold_valid(self):
|
def test_last_auth_threshold_valid(self):
|
||||||
"""Test last_auth_threshold"""
|
"""Test last_auth_threshold"""
|
||||||
ident_stage = IdentificationStage.objects.create(
|
ident_stage = IdentificationStage.objects.create(
|
||||||
name="conf",
|
name=generate_id(),
|
||||||
user_fields=[
|
user_fields=[
|
||||||
UserFields.USERNAME,
|
UserFields.USERNAME,
|
||||||
],
|
],
|
||||||
|
@ -77,7 +78,7 @@ class AuthenticatorValidateStageSMSTests(FlowTestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
stage = AuthenticatorValidateStage.objects.create(
|
stage = AuthenticatorValidateStage.objects.create(
|
||||||
name="foo",
|
name=generate_id(),
|
||||||
last_auth_threshold="hours=1",
|
last_auth_threshold="hours=1",
|
||||||
not_configured_action=NotConfiguredAction.CONFIGURE,
|
not_configured_action=NotConfiguredAction.CONFIGURE,
|
||||||
device_classes=[DeviceClasses.SMS],
|
device_classes=[DeviceClasses.SMS],
|
||||||
|
@ -120,7 +121,7 @@ class AuthenticatorValidateStageSMSTests(FlowTestCase):
|
||||||
def test_sms_hashed(self):
|
def test_sms_hashed(self):
|
||||||
"""Test hashed SMS device"""
|
"""Test hashed SMS device"""
|
||||||
ident_stage = IdentificationStage.objects.create(
|
ident_stage = IdentificationStage.objects.create(
|
||||||
name="conf",
|
name=generate_id(),
|
||||||
user_fields=[
|
user_fields=[
|
||||||
UserFields.USERNAME,
|
UserFields.USERNAME,
|
||||||
],
|
],
|
||||||
|
@ -133,7 +134,7 @@ class AuthenticatorValidateStageSMSTests(FlowTestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
stage = AuthenticatorValidateStage.objects.create(
|
stage = AuthenticatorValidateStage.objects.create(
|
||||||
name="foo",
|
name=generate_id(),
|
||||||
last_auth_threshold="hours=1",
|
last_auth_threshold="hours=1",
|
||||||
not_configured_action=NotConfiguredAction.DENY,
|
not_configured_action=NotConfiguredAction.DENY,
|
||||||
device_classes=[DeviceClasses.SMS],
|
device_classes=[DeviceClasses.SMS],
|
||||||
|
|
|
@ -9,6 +9,7 @@ from authentik.flows.models import Flow, FlowStageBinding, NotConfiguredAction
|
||||||
from authentik.flows.stage import StageView
|
from authentik.flows.stage import StageView
|
||||||
from authentik.flows.tests import FlowTestCase
|
from authentik.flows.tests import FlowTestCase
|
||||||
from authentik.flows.views.executor import FlowExecutorView
|
from authentik.flows.views.executor import FlowExecutorView
|
||||||
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.lib.tests.utils import dummy_get_response
|
from authentik.lib.tests.utils import dummy_get_response
|
||||||
from authentik.stages.authenticator_validate.api import AuthenticatorValidateStageSerializer
|
from authentik.stages.authenticator_validate.api import AuthenticatorValidateStageSerializer
|
||||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
|
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
|
||||||
|
@ -29,13 +30,13 @@ class AuthenticatorValidateStageTests(FlowTestCase):
|
||||||
def test_not_configured_action(self):
|
def test_not_configured_action(self):
|
||||||
"""Test not_configured_action"""
|
"""Test not_configured_action"""
|
||||||
conf_stage = IdentificationStage.objects.create(
|
conf_stage = IdentificationStage.objects.create(
|
||||||
name="conf",
|
name=generate_id(),
|
||||||
user_fields=[
|
user_fields=[
|
||||||
UserFields.USERNAME,
|
UserFields.USERNAME,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
stage = AuthenticatorValidateStage.objects.create(
|
stage = AuthenticatorValidateStage.objects.create(
|
||||||
name="foo",
|
name=generate_id(),
|
||||||
not_configured_action=NotConfiguredAction.CONFIGURE,
|
not_configured_action=NotConfiguredAction.CONFIGURE,
|
||||||
)
|
)
|
||||||
stage.configuration_stages.set([conf_stage])
|
stage.configuration_stages.set([conf_stage])
|
||||||
|
@ -67,12 +68,12 @@ class AuthenticatorValidateStageTests(FlowTestCase):
|
||||||
"""Test serializer validation"""
|
"""Test serializer validation"""
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
serializer = AuthenticatorValidateStageSerializer(
|
serializer = AuthenticatorValidateStageSerializer(
|
||||||
data={"name": "foo", "not_configured_action": NotConfiguredAction.CONFIGURE}
|
data={"name": generate_id(), "not_configured_action": NotConfiguredAction.CONFIGURE}
|
||||||
)
|
)
|
||||||
self.assertFalse(serializer.is_valid())
|
self.assertFalse(serializer.is_valid())
|
||||||
self.assertIn("not_configured_action", serializer.errors)
|
self.assertIn("not_configured_action", serializer.errors)
|
||||||
serializer = AuthenticatorValidateStageSerializer(
|
serializer = AuthenticatorValidateStageSerializer(
|
||||||
data={"name": "foo", "not_configured_action": NotConfiguredAction.DENY}
|
data={"name": generate_id(), "not_configured_action": NotConfiguredAction.DENY}
|
||||||
)
|
)
|
||||||
self.assertTrue(serializer.is_valid())
|
self.assertTrue(serializer.is_valid())
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,10 @@ from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||||
from authentik.flows.models import FlowDesignation, FlowStageBinding, NotConfiguredAction
|
from authentik.flows.models import FlowDesignation, FlowStageBinding, NotConfiguredAction
|
||||||
|
from authentik.flows.stage import StageView
|
||||||
from authentik.flows.tests import FlowTestCase
|
from authentik.flows.tests import FlowTestCase
|
||||||
|
from authentik.flows.views.executor import FlowExecutorView
|
||||||
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.stages.authenticator_validate.challenge import (
|
from authentik.stages.authenticator_validate.challenge import (
|
||||||
get_challenge_for_device,
|
get_challenge_for_device,
|
||||||
validate_challenge_code,
|
validate_challenge_code,
|
||||||
|
@ -35,7 +38,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
|
||||||
def test_last_auth_threshold(self):
|
def test_last_auth_threshold(self):
|
||||||
"""Test last_auth_threshold"""
|
"""Test last_auth_threshold"""
|
||||||
ident_stage = IdentificationStage.objects.create(
|
ident_stage = IdentificationStage.objects.create(
|
||||||
name="conf",
|
name=generate_id(),
|
||||||
user_fields=[
|
user_fields=[
|
||||||
UserFields.USERNAME,
|
UserFields.USERNAME,
|
||||||
],
|
],
|
||||||
|
@ -49,7 +52,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
|
||||||
sleep(1)
|
sleep(1)
|
||||||
self.assertTrue(device.verify_token(totp.token()))
|
self.assertTrue(device.verify_token(totp.token()))
|
||||||
stage = AuthenticatorValidateStage.objects.create(
|
stage = AuthenticatorValidateStage.objects.create(
|
||||||
name="foo",
|
name=generate_id(),
|
||||||
last_auth_threshold="milliseconds=0",
|
last_auth_threshold="milliseconds=0",
|
||||||
not_configured_action=NotConfiguredAction.CONFIGURE,
|
not_configured_action=NotConfiguredAction.CONFIGURE,
|
||||||
device_classes=[DeviceClasses.TOTP],
|
device_classes=[DeviceClasses.TOTP],
|
||||||
|
@ -76,7 +79,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
|
||||||
def test_last_auth_threshold_valid(self) -> SimpleCookie:
|
def test_last_auth_threshold_valid(self) -> SimpleCookie:
|
||||||
"""Test last_auth_threshold"""
|
"""Test last_auth_threshold"""
|
||||||
ident_stage = IdentificationStage.objects.create(
|
ident_stage = IdentificationStage.objects.create(
|
||||||
name="conf",
|
name=generate_id(),
|
||||||
user_fields=[
|
user_fields=[
|
||||||
UserFields.USERNAME,
|
UserFields.USERNAME,
|
||||||
],
|
],
|
||||||
|
@ -86,7 +89,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
|
||||||
confirmed=True,
|
confirmed=True,
|
||||||
)
|
)
|
||||||
stage = AuthenticatorValidateStage.objects.create(
|
stage = AuthenticatorValidateStage.objects.create(
|
||||||
name="foo",
|
name=generate_id(),
|
||||||
last_auth_threshold="hours=1",
|
last_auth_threshold="hours=1",
|
||||||
not_configured_action=NotConfiguredAction.CONFIGURE,
|
not_configured_action=NotConfiguredAction.CONFIGURE,
|
||||||
device_classes=[DeviceClasses.TOTP],
|
device_classes=[DeviceClasses.TOTP],
|
||||||
|
@ -133,7 +136,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
|
||||||
def test_last_auth_stage_pk(self):
|
def test_last_auth_stage_pk(self):
|
||||||
"""Test MFA cookie with wrong stage PK"""
|
"""Test MFA cookie with wrong stage PK"""
|
||||||
ident_stage = IdentificationStage.objects.create(
|
ident_stage = IdentificationStage.objects.create(
|
||||||
name="conf",
|
name=generate_id(),
|
||||||
user_fields=[
|
user_fields=[
|
||||||
UserFields.USERNAME,
|
UserFields.USERNAME,
|
||||||
],
|
],
|
||||||
|
@ -143,7 +146,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
|
||||||
confirmed=True,
|
confirmed=True,
|
||||||
)
|
)
|
||||||
stage = AuthenticatorValidateStage.objects.create(
|
stage = AuthenticatorValidateStage.objects.create(
|
||||||
name="foo",
|
name=generate_id(),
|
||||||
last_auth_threshold="hours=1",
|
last_auth_threshold="hours=1",
|
||||||
not_configured_action=NotConfiguredAction.CONFIGURE,
|
not_configured_action=NotConfiguredAction.CONFIGURE,
|
||||||
device_classes=[DeviceClasses.TOTP],
|
device_classes=[DeviceClasses.TOTP],
|
||||||
|
@ -154,7 +157,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
|
||||||
self.client.cookies[COOKIE_NAME_MFA] = encode(
|
self.client.cookies[COOKIE_NAME_MFA] = encode(
|
||||||
payload={
|
payload={
|
||||||
"device": device.pk,
|
"device": device.pk,
|
||||||
"stage": stage.pk.hex + "foo",
|
"stage": stage.pk.hex + generate_id(),
|
||||||
"exp": (datetime.now() + timedelta(days=3)).timestamp(),
|
"exp": (datetime.now() + timedelta(days=3)).timestamp(),
|
||||||
},
|
},
|
||||||
key=sha256(f"{settings.SECRET_KEY}:{stage.pk.hex}".encode("ascii")).hexdigest(),
|
key=sha256(f"{settings.SECRET_KEY}:{stage.pk.hex}".encode("ascii")).hexdigest(),
|
||||||
|
@ -172,7 +175,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
|
||||||
def test_last_auth_stage_device(self):
|
def test_last_auth_stage_device(self):
|
||||||
"""Test MFA cookie with wrong device PK"""
|
"""Test MFA cookie with wrong device PK"""
|
||||||
ident_stage = IdentificationStage.objects.create(
|
ident_stage = IdentificationStage.objects.create(
|
||||||
name="conf",
|
name=generate_id(),
|
||||||
user_fields=[
|
user_fields=[
|
||||||
UserFields.USERNAME,
|
UserFields.USERNAME,
|
||||||
],
|
],
|
||||||
|
@ -182,7 +185,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
|
||||||
confirmed=True,
|
confirmed=True,
|
||||||
)
|
)
|
||||||
stage = AuthenticatorValidateStage.objects.create(
|
stage = AuthenticatorValidateStage.objects.create(
|
||||||
name="foo",
|
name=generate_id(),
|
||||||
last_auth_threshold="hours=1",
|
last_auth_threshold="hours=1",
|
||||||
not_configured_action=NotConfiguredAction.CONFIGURE,
|
not_configured_action=NotConfiguredAction.CONFIGURE,
|
||||||
device_classes=[DeviceClasses.TOTP],
|
device_classes=[DeviceClasses.TOTP],
|
||||||
|
@ -211,7 +214,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
|
||||||
def test_last_auth_stage_expired(self):
|
def test_last_auth_stage_expired(self):
|
||||||
"""Test MFA cookie with expired cookie"""
|
"""Test MFA cookie with expired cookie"""
|
||||||
ident_stage = IdentificationStage.objects.create(
|
ident_stage = IdentificationStage.objects.create(
|
||||||
name="conf",
|
name=generate_id(),
|
||||||
user_fields=[
|
user_fields=[
|
||||||
UserFields.USERNAME,
|
UserFields.USERNAME,
|
||||||
],
|
],
|
||||||
|
@ -221,7 +224,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
|
||||||
confirmed=True,
|
confirmed=True,
|
||||||
)
|
)
|
||||||
stage = AuthenticatorValidateStage.objects.create(
|
stage = AuthenticatorValidateStage.objects.create(
|
||||||
name="foo",
|
name=generate_id(),
|
||||||
last_auth_threshold="hours=1",
|
last_auth_threshold="hours=1",
|
||||||
not_configured_action=NotConfiguredAction.CONFIGURE,
|
not_configured_action=NotConfiguredAction.CONFIGURE,
|
||||||
device_classes=[DeviceClasses.TOTP],
|
device_classes=[DeviceClasses.TOTP],
|
||||||
|
@ -251,6 +254,14 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
|
||||||
"""Test device challenge"""
|
"""Test device challenge"""
|
||||||
request = self.request_factory.get("/")
|
request = self.request_factory.get("/")
|
||||||
totp_device = TOTPDevice.objects.create(user=self.user, confirmed=True, digits=6)
|
totp_device = TOTPDevice.objects.create(user=self.user, confirmed=True, digits=6)
|
||||||
|
stage = AuthenticatorValidateStage.objects.create(
|
||||||
|
name=generate_id(),
|
||||||
|
last_auth_threshold="hours=1",
|
||||||
|
not_configured_action=NotConfiguredAction.CONFIGURE,
|
||||||
|
device_classes=[DeviceClasses.TOTP],
|
||||||
|
)
|
||||||
self.assertEqual(get_challenge_for_device(request, totp_device), {})
|
self.assertEqual(get_challenge_for_device(request, totp_device), {})
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
validate_challenge_code("1234", request, self.user)
|
validate_challenge_code(
|
||||||
|
"1234", StageView(FlowExecutorView(current_stage=stage), request=request), self.user
|
||||||
|
)
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
"""Test validator stage"""
|
"""Test validator stage"""
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
|
from django.http import Http404
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
from django.urls.base import reverse
|
from django.urls.base import reverse
|
||||||
from rest_framework.exceptions import ValidationError
|
|
||||||
from webauthn.helpers import bytes_to_base64url
|
from webauthn.helpers import bytes_to_base64url
|
||||||
|
|
||||||
from authentik.core.tests.utils import create_test_admin_user
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
from authentik.flows.models import Flow, FlowStageBinding, NotConfiguredAction
|
from authentik.flows.models import Flow, FlowStageBinding, NotConfiguredAction
|
||||||
|
from authentik.flows.stage import StageView
|
||||||
from authentik.flows.tests import FlowTestCase
|
from authentik.flows.tests import FlowTestCase
|
||||||
|
from authentik.flows.views.executor import FlowExecutorView
|
||||||
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.lib.tests.utils import get_request
|
from authentik.lib.tests.utils import get_request
|
||||||
from authentik.stages.authenticator_validate.challenge import (
|
from authentik.stages.authenticator_validate.challenge import (
|
||||||
get_challenge_for_device,
|
get_challenge_for_device,
|
||||||
|
@ -29,7 +32,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
|
||||||
def test_last_auth_threshold(self):
|
def test_last_auth_threshold(self):
|
||||||
"""Test last_auth_threshold"""
|
"""Test last_auth_threshold"""
|
||||||
ident_stage = IdentificationStage.objects.create(
|
ident_stage = IdentificationStage.objects.create(
|
||||||
name="conf",
|
name=generate_id(),
|
||||||
user_fields=[
|
user_fields=[
|
||||||
UserFields.USERNAME,
|
UserFields.USERNAME,
|
||||||
],
|
],
|
||||||
|
@ -40,7 +43,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
|
||||||
)
|
)
|
||||||
device.set_sign_count(device.sign_count + 1)
|
device.set_sign_count(device.sign_count + 1)
|
||||||
stage = AuthenticatorValidateStage.objects.create(
|
stage = AuthenticatorValidateStage.objects.create(
|
||||||
name="foo",
|
name=generate_id(),
|
||||||
last_auth_threshold="milliseconds=0",
|
last_auth_threshold="milliseconds=0",
|
||||||
not_configured_action=NotConfiguredAction.CONFIGURE,
|
not_configured_action=NotConfiguredAction.CONFIGURE,
|
||||||
device_classes=[DeviceClasses.WEBAUTHN],
|
device_classes=[DeviceClasses.WEBAUTHN],
|
||||||
|
@ -76,7 +79,13 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
|
||||||
public_key=bytes_to_base64url(b"qwerqwerqre"),
|
public_key=bytes_to_base64url(b"qwerqwerqre"),
|
||||||
credential_id=bytes_to_base64url(b"foobarbaz"),
|
credential_id=bytes_to_base64url(b"foobarbaz"),
|
||||||
sign_count=0,
|
sign_count=0,
|
||||||
rp_id="foo",
|
rp_id=generate_id(),
|
||||||
|
)
|
||||||
|
stage = AuthenticatorValidateStage.objects.create(
|
||||||
|
name=generate_id(),
|
||||||
|
last_auth_threshold="milliseconds=0",
|
||||||
|
not_configured_action=NotConfiguredAction.CONFIGURE,
|
||||||
|
device_classes=[DeviceClasses.WEBAUTHN],
|
||||||
)
|
)
|
||||||
challenge = get_challenge_for_device(request, webauthn_device)
|
challenge = get_challenge_for_device(request, webauthn_device)
|
||||||
del challenge["challenge"]
|
del challenge["challenge"]
|
||||||
|
@ -95,5 +104,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(Http404):
|
||||||
validate_challenge_webauthn({}, request, self.user)
|
validate_challenge_webauthn(
|
||||||
|
{}, StageView(FlowExecutorView(current_stage=stage), request=request), self.user
|
||||||
|
)
|
||||||
|
|
|
@ -127,6 +127,7 @@ class IdentificationChallengeResponse(ChallengeResponse):
|
||||||
user = authenticate(
|
user = authenticate(
|
||||||
self.stage.request,
|
self.stage.request,
|
||||||
current_stage.password_stage.backends,
|
current_stage.password_stage.backends,
|
||||||
|
current_stage,
|
||||||
username=self.pre_user.username,
|
username=self.pre_user.username,
|
||||||
password=password,
|
password=password,
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,7 +3,6 @@ from typing import Any, Optional
|
||||||
|
|
||||||
from django.contrib.auth import _clean_credentials
|
from django.contrib.auth import _clean_credentials
|
||||||
from django.contrib.auth.backends import BaseBackend
|
from django.contrib.auth.backends import BaseBackend
|
||||||
from django.contrib.auth.signals import user_login_failed
|
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
@ -14,13 +13,14 @@ from sentry_sdk.hub import Hub
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
|
from authentik.core.signals import login_failed
|
||||||
from authentik.flows.challenge import (
|
from authentik.flows.challenge import (
|
||||||
Challenge,
|
Challenge,
|
||||||
ChallengeResponse,
|
ChallengeResponse,
|
||||||
ChallengeTypes,
|
ChallengeTypes,
|
||||||
WithUserInfoChallenge,
|
WithUserInfoChallenge,
|
||||||
)
|
)
|
||||||
from authentik.flows.models import Flow, FlowDesignation
|
from authentik.flows.models import Flow, FlowDesignation, Stage
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
from authentik.lib.utils.reflection import path_to_class
|
from authentik.lib.utils.reflection import path_to_class
|
||||||
|
@ -33,7 +33,9 @@ PLAN_CONTEXT_METHOD_ARGS = "auth_method_args"
|
||||||
SESSION_KEY_INVALID_TRIES = "authentik/stages/password/user_invalid_tries"
|
SESSION_KEY_INVALID_TRIES = "authentik/stages/password/user_invalid_tries"
|
||||||
|
|
||||||
|
|
||||||
def authenticate(request: HttpRequest, backends: list[str], **credentials: Any) -> Optional[User]:
|
def authenticate(
|
||||||
|
request: HttpRequest, backends: list[str], stage: Optional[Stage] = None, **credentials: Any
|
||||||
|
) -> Optional[User]:
|
||||||
"""If the given credentials are valid, return a User object.
|
"""If the given credentials are valid, return a User object.
|
||||||
|
|
||||||
Customized version of django's authenticate, which accepts a list of backends"""
|
Customized version of django's authenticate, which accepts a list of backends"""
|
||||||
|
@ -58,8 +60,11 @@ def authenticate(request: HttpRequest, backends: list[str], **credentials: Any)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
# The credentials supplied are invalid to all backends, fire signal
|
# The credentials supplied are invalid to all backends, fire signal
|
||||||
user_login_failed.send(
|
login_failed.send(
|
||||||
sender=__name__, credentials=_clean_credentials(credentials), request=request
|
sender=__name__,
|
||||||
|
credentials=_clean_credentials(credentials),
|
||||||
|
request=request,
|
||||||
|
stage=stage,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -130,7 +135,10 @@ class PasswordStageView(ChallengeStageView):
|
||||||
description="User authenticate call",
|
description="User authenticate call",
|
||||||
):
|
):
|
||||||
user = authenticate(
|
user = authenticate(
|
||||||
self.request, self.executor.current_stage.backends, **auth_kwargs
|
self.request,
|
||||||
|
self.executor.current_stage.backends,
|
||||||
|
self.executor.current_stage,
|
||||||
|
**auth_kwargs,
|
||||||
)
|
)
|
||||||
except PermissionDenied:
|
except PermissionDenied:
|
||||||
del auth_kwargs["password"]
|
del auth_kwargs["password"]
|
||||||
|
|
Reference in New Issue