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:
Jens L 2022-06-04 15:30:56 +02:00 committed by GitHub
parent 6739ded5a9
commit fa04883ac1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 148 additions and 61 deletions

View File

@ -12,6 +12,8 @@ from django.http.request import HttpRequest
# Arguments: user: User, password: str
password_changed = Signal()
# Arguments: credentials: dict[str, any], request: HttpRequest, stage: Stage
login_failed = Signal()
if TYPE_CHECKING:
from authentik.core.models import AuthenticatedSession, User

View File

@ -2,15 +2,16 @@
from threading import Thread
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.dispatch import receiver
from django.http import HttpRequest
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.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.views.executor import SESSION_KEY_PLAN
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()
@receiver(user_login_failed)
@receiver(login_failed)
# pylint: disable=unused-argument
def on_user_login_failed(sender, credentials: dict[str, str], request: HttpRequest, **_):
"""Failed Login"""
thread = EventNewThread(EventAction.LOGIN_FAILED, request, **credentials)
def on_login_failed(
signal,
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()

View File

@ -23,6 +23,7 @@ from authentik.flows.challenge import (
)
from authentik.flows.models import InvalidResponseAction
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_PENDING_USER
from authentik.lib.utils.reflection import class_to_path
if TYPE_CHECKING:
from authentik.flows.views.executor import FlowExecutorView
@ -44,7 +45,7 @@ class StageView(View):
current_stage = getattr(self.executor, "current_stage", None)
self.logger = get_logger().bind(
stage=getattr(current_stage, "name", None),
stage_view=self,
stage_view=class_to_path(type(self)),
)
super().__init__(**kwargs)

View File

@ -1,10 +1,11 @@
"""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.dispatch import receiver
from django.http import HttpRequest
from structlog.stdlib import get_logger
from authentik.core.signals import login_failed
from authentik.lib.config import CONFIG
from authentik.lib.utils.http import get_client_ip
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()
@receiver(user_login_failed)
@receiver(login_failed)
# pylint: disable=unused-argument
def handle_failed_login(sender, request, credentials, **_):
"""Lower Score for failed login attempts"""

View File

@ -1,5 +1,4 @@
"""test reputation signals and policy"""
from django.contrib.auth import authenticate
from django.core.cache import cache
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.tasks import save_reputation
from authentik.policies.types import PolicyRequest
from authentik.stages.password import BACKEND_INBUILT
from authentik.stages.password.stage import authenticate
class TestReputationPolicy(TestCase):
@ -21,11 +22,14 @@ class TestReputationPolicy(TestCase):
cache.delete_many(keys)
# We need a user for the one-to-one in userreputation
self.user = User.objects.create(username=self.test_username)
self.backends = [BACKEND_INBUILT]
def test_ip_reputation(self):
"""test IP 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
self.assertEqual(
cache.get(CACHE_KEY_PREFIX + self.test_ip + self.test_username),
@ -38,7 +42,9 @@ class TestReputationPolicy(TestCase):
def test_user_reputation(self):
"""test User 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
self.assertEqual(
cache.get(CACHE_KEY_PREFIX + self.test_ip + self.test_username),

View File

@ -18,9 +18,12 @@ from webauthn.helpers.structs import AuthenticationCredential
from authentik.core.api.utils import PassiveSerializer
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.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
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.stage import SESSION_KEY_WEBAUTHN_CHALLENGE
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)
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
the user mustn't choose between totp and static devices."""
device = match_token(user, code)
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"))
return device
# 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"""
request = stage_view.request
challenge = request.session.get(SESSION_KEY_WEBAUTHN_CHALLENGE)
credential_id = data.get("id")
device = WebAuthnDevice.objects.filter(credential_id=credential_id).first()
if not device:
raise ValidationError("Device does not exist.")
raise Http404()
try:
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,
require_user_verification=False,
)
except InvalidAuthenticationResponse as 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
device.set_sign_count(authentication_verification.new_sign_count)
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"""
device = get_object_or_404(DuoDevice, pk=device_pk)
if device.user != user:
@ -140,13 +158,20 @@ def validate_challenge_duo(device_pk: int, request: HttpRequest, user: User) ->
response = stage.client.auth(
"auto",
user_id=device.duo_user_id,
ipaddr=get_client_ip(request),
ipaddr=get_client_ip(stage_view.request),
type="authentik Login request",
display_username=user.username,
device="auto",
)
# {'result': 'allow', 'status': 'allow', 'status_msg': 'Success. Logging you in...'}
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")
device.save()
return device

View File

@ -14,7 +14,7 @@ class DeviceClasses(models.TextChoices):
"""Device classes this stage can validate"""
# device class must match Device's class name so StaticDevice -> static
STATIC = "static"
STATIC = "static", _("Static")
TOTP = "totp", _("TOTP")
WEBAUTHN = "webauthn", _("WebAuthn")
DUO = "duo", _("Duo")

View File

@ -85,9 +85,7 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
def validate_code(self, code: str) -> str:
"""Validate code-based response, raise error if code isn't allowed"""
self._challenge_allowed([DeviceClasses.TOTP, DeviceClasses.STATIC, DeviceClasses.SMS])
self.device = validate_challenge_code(
code, self.stage.request, self.stage.get_pending_user()
)
self.device = validate_challenge_code(code, self.stage, self.stage.get_pending_user())
return code
def validate_webauthn(self, webauthn: dict) -> dict:
@ -95,14 +93,14 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
or response is invalid"""
self._challenge_allowed([DeviceClasses.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
def validate_duo(self, duo: int) -> int:
"""Initiate Duo authentication"""
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
def validate_selected_challenge(self, challenge: dict) -> dict:

View File

@ -5,7 +5,9 @@ from django.test.client import RequestFactory
from rest_framework.exceptions import ValidationError
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.views.executor import FlowExecutorView
from authentik.lib.generators import generate_id, generate_key
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
from authentik.stages.authenticator_validate.challenge import validate_challenge_duo
@ -22,7 +24,7 @@ class AuthenticatorValidateStageDuoTests(FlowTestCase):
"""Test duo"""
request = self.request_factory.get("/")
stage = AuthenticatorDuoStage.objects.create(
name="test",
name=generate_id(),
client_id=generate_id(),
client_secret=generate_key(),
api_hostname="",
@ -45,10 +47,21 @@ class AuthenticatorValidateStageDuoTests(FlowTestCase):
"authentik.stages.authenticator_duo.models.AuthenticatorDuoStage.client",
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(
"authentik.stages.authenticator_duo.models.AuthenticatorDuoStage.client",
failed_duo_mock,
):
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,
)

View File

@ -7,6 +7,7 @@ from django.urls.base import reverse
from authentik.core.tests.utils import create_test_admin_user
from authentik.flows.models import Flow, FlowStageBinding, NotConfiguredAction
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_validate.models import AuthenticatorValidateStage, DeviceClasses
from authentik.stages.authenticator_validate.stage import COOKIE_NAME_MFA
@ -28,7 +29,7 @@ class AuthenticatorValidateStageSMSTests(FlowTestCase):
def test_last_auth_threshold(self):
"""Test last_auth_threshold"""
ident_stage = IdentificationStage.objects.create(
name="conf",
name=generate_id(),
user_fields=[
UserFields.USERNAME,
],
@ -40,7 +41,7 @@ class AuthenticatorValidateStageSMSTests(FlowTestCase):
)
stage = AuthenticatorValidateStage.objects.create(
name="foo",
name=generate_id(),
last_auth_threshold="milliseconds=0",
not_configured_action=NotConfiguredAction.CONFIGURE,
device_classes=[DeviceClasses.SMS],
@ -65,7 +66,7 @@ class AuthenticatorValidateStageSMSTests(FlowTestCase):
def test_last_auth_threshold_valid(self):
"""Test last_auth_threshold"""
ident_stage = IdentificationStage.objects.create(
name="conf",
name=generate_id(),
user_fields=[
UserFields.USERNAME,
],
@ -77,7 +78,7 @@ class AuthenticatorValidateStageSMSTests(FlowTestCase):
)
stage = AuthenticatorValidateStage.objects.create(
name="foo",
name=generate_id(),
last_auth_threshold="hours=1",
not_configured_action=NotConfiguredAction.CONFIGURE,
device_classes=[DeviceClasses.SMS],
@ -120,7 +121,7 @@ class AuthenticatorValidateStageSMSTests(FlowTestCase):
def test_sms_hashed(self):
"""Test hashed SMS device"""
ident_stage = IdentificationStage.objects.create(
name="conf",
name=generate_id(),
user_fields=[
UserFields.USERNAME,
],
@ -133,7 +134,7 @@ class AuthenticatorValidateStageSMSTests(FlowTestCase):
)
stage = AuthenticatorValidateStage.objects.create(
name="foo",
name=generate_id(),
last_auth_threshold="hours=1",
not_configured_action=NotConfiguredAction.DENY,
device_classes=[DeviceClasses.SMS],

View File

@ -9,6 +9,7 @@ from authentik.flows.models import Flow, FlowStageBinding, NotConfiguredAction
from authentik.flows.stage import StageView
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 dummy_get_response
from authentik.stages.authenticator_validate.api import AuthenticatorValidateStageSerializer
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
@ -29,13 +30,13 @@ class AuthenticatorValidateStageTests(FlowTestCase):
def test_not_configured_action(self):
"""Test not_configured_action"""
conf_stage = IdentificationStage.objects.create(
name="conf",
name=generate_id(),
user_fields=[
UserFields.USERNAME,
],
)
stage = AuthenticatorValidateStage.objects.create(
name="foo",
name=generate_id(),
not_configured_action=NotConfiguredAction.CONFIGURE,
)
stage.configuration_stages.set([conf_stage])
@ -67,12 +68,12 @@ class AuthenticatorValidateStageTests(FlowTestCase):
"""Test serializer validation"""
self.client.force_login(self.user)
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.assertIn("not_configured_action", serializer.errors)
serializer = AuthenticatorValidateStageSerializer(
data={"name": "foo", "not_configured_action": NotConfiguredAction.DENY}
data={"name": generate_id(), "not_configured_action": NotConfiguredAction.DENY}
)
self.assertTrue(serializer.is_valid())

View File

@ -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.flows.models import FlowDesignation, FlowStageBinding, NotConfiguredAction
from authentik.flows.stage import StageView
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 (
get_challenge_for_device,
validate_challenge_code,
@ -35,7 +38,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
def test_last_auth_threshold(self):
"""Test last_auth_threshold"""
ident_stage = IdentificationStage.objects.create(
name="conf",
name=generate_id(),
user_fields=[
UserFields.USERNAME,
],
@ -49,7 +52,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
sleep(1)
self.assertTrue(device.verify_token(totp.token()))
stage = AuthenticatorValidateStage.objects.create(
name="foo",
name=generate_id(),
last_auth_threshold="milliseconds=0",
not_configured_action=NotConfiguredAction.CONFIGURE,
device_classes=[DeviceClasses.TOTP],
@ -76,7 +79,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
def test_last_auth_threshold_valid(self) -> SimpleCookie:
"""Test last_auth_threshold"""
ident_stage = IdentificationStage.objects.create(
name="conf",
name=generate_id(),
user_fields=[
UserFields.USERNAME,
],
@ -86,7 +89,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
confirmed=True,
)
stage = AuthenticatorValidateStage.objects.create(
name="foo",
name=generate_id(),
last_auth_threshold="hours=1",
not_configured_action=NotConfiguredAction.CONFIGURE,
device_classes=[DeviceClasses.TOTP],
@ -133,7 +136,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
def test_last_auth_stage_pk(self):
"""Test MFA cookie with wrong stage PK"""
ident_stage = IdentificationStage.objects.create(
name="conf",
name=generate_id(),
user_fields=[
UserFields.USERNAME,
],
@ -143,7 +146,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
confirmed=True,
)
stage = AuthenticatorValidateStage.objects.create(
name="foo",
name=generate_id(),
last_auth_threshold="hours=1",
not_configured_action=NotConfiguredAction.CONFIGURE,
device_classes=[DeviceClasses.TOTP],
@ -154,7 +157,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
self.client.cookies[COOKIE_NAME_MFA] = encode(
payload={
"device": device.pk,
"stage": stage.pk.hex + "foo",
"stage": stage.pk.hex + generate_id(),
"exp": (datetime.now() + timedelta(days=3)).timestamp(),
},
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):
"""Test MFA cookie with wrong device PK"""
ident_stage = IdentificationStage.objects.create(
name="conf",
name=generate_id(),
user_fields=[
UserFields.USERNAME,
],
@ -182,7 +185,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
confirmed=True,
)
stage = AuthenticatorValidateStage.objects.create(
name="foo",
name=generate_id(),
last_auth_threshold="hours=1",
not_configured_action=NotConfiguredAction.CONFIGURE,
device_classes=[DeviceClasses.TOTP],
@ -211,7 +214,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
def test_last_auth_stage_expired(self):
"""Test MFA cookie with expired cookie"""
ident_stage = IdentificationStage.objects.create(
name="conf",
name=generate_id(),
user_fields=[
UserFields.USERNAME,
],
@ -221,7 +224,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
confirmed=True,
)
stage = AuthenticatorValidateStage.objects.create(
name="foo",
name=generate_id(),
last_auth_threshold="hours=1",
not_configured_action=NotConfiguredAction.CONFIGURE,
device_classes=[DeviceClasses.TOTP],
@ -251,6 +254,14 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
"""Test device challenge"""
request = self.request_factory.get("/")
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), {})
with self.assertRaises(ValidationError):
validate_challenge_code("1234", request, self.user)
validate_challenge_code(
"1234", StageView(FlowExecutorView(current_stage=stage), request=request), self.user
)

View File

@ -1,14 +1,17 @@
"""Test validator stage"""
from time import sleep
from django.http import Http404
from django.test.client import RequestFactory
from django.urls.base import reverse
from rest_framework.exceptions import ValidationError
from webauthn.helpers import bytes_to_base64url
from authentik.core.tests.utils import create_test_admin_user
from authentik.flows.models import Flow, FlowStageBinding, NotConfiguredAction
from authentik.flows.stage import StageView
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.stages.authenticator_validate.challenge import (
get_challenge_for_device,
@ -29,7 +32,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
def test_last_auth_threshold(self):
"""Test last_auth_threshold"""
ident_stage = IdentificationStage.objects.create(
name="conf",
name=generate_id(),
user_fields=[
UserFields.USERNAME,
],
@ -40,7 +43,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
)
device.set_sign_count(device.sign_count + 1)
stage = AuthenticatorValidateStage.objects.create(
name="foo",
name=generate_id(),
last_auth_threshold="milliseconds=0",
not_configured_action=NotConfiguredAction.CONFIGURE,
device_classes=[DeviceClasses.WEBAUTHN],
@ -76,7 +79,13 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
public_key=bytes_to_base64url(b"qwerqwerqre"),
credential_id=bytes_to_base64url(b"foobarbaz"),
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)
del challenge["challenge"]
@ -95,5 +104,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
},
)
with self.assertRaises(ValidationError):
validate_challenge_webauthn({}, request, self.user)
with self.assertRaises(Http404):
validate_challenge_webauthn(
{}, StageView(FlowExecutorView(current_stage=stage), request=request), self.user
)

View File

@ -127,6 +127,7 @@ class IdentificationChallengeResponse(ChallengeResponse):
user = authenticate(
self.stage.request,
current_stage.password_stage.backends,
current_stage,
username=self.pre_user.username,
password=password,
)

View File

@ -3,7 +3,6 @@ from typing import Any, Optional
from django.contrib.auth import _clean_credentials
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth.signals import user_login_failed
from django.core.exceptions import PermissionDenied
from django.http import HttpRequest, HttpResponse
from django.urls import reverse
@ -14,13 +13,14 @@ from sentry_sdk.hub import Hub
from structlog.stdlib import get_logger
from authentik.core.models import User
from authentik.core.signals import login_failed
from authentik.flows.challenge import (
Challenge,
ChallengeResponse,
ChallengeTypes,
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.stage import ChallengeStageView
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"
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.
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
# The credentials supplied are invalid to all backends, fire signal
user_login_failed.send(
sender=__name__, credentials=_clean_credentials(credentials), request=request
login_failed.send(
sender=__name__,
credentials=_clean_credentials(credentials),
request=request,
stage=stage,
)
@ -130,7 +135,10 @@ class PasswordStageView(ChallengeStageView):
description="User authenticate call",
):
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:
del auth_kwargs["password"]