stages/authenticator_validate: fix error when using pretend_user (#8447)
This commit is contained in:
@ -43,7 +43,9 @@ class TokenBackend(InbuiltBackend):
|
|||||||
self, request: HttpRequest, username: Optional[str], password: Optional[str], **kwargs: Any
|
self, request: HttpRequest, username: Optional[str], password: Optional[str], **kwargs: Any
|
||||||
) -> Optional[User]:
|
) -> Optional[User]:
|
||||||
try:
|
try:
|
||||||
|
# pylint: disable=no-member
|
||||||
user = User._default_manager.get_by_natural_key(username)
|
user = User._default_manager.get_by_natural_key(username)
|
||||||
|
# pylint: disable=no-member
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
# Run the default password hasher once to reduce the timing
|
# Run the default password hasher once to reduce the timing
|
||||||
# difference between an existing and a nonexistent user (#20760).
|
# difference between an existing and a nonexistent user (#20760).
|
||||||
|
|||||||
@ -37,6 +37,7 @@ def clean_expired_models(self: SystemTask):
|
|||||||
messages.append(f"Expired {amount} {cls._meta.verbose_name_plural}")
|
messages.append(f"Expired {amount} {cls._meta.verbose_name_plural}")
|
||||||
# Special case
|
# Special case
|
||||||
amount = 0
|
amount = 0
|
||||||
|
# pylint: disable=no-member
|
||||||
for session in AuthenticatedSession.objects.all():
|
for session in AuthenticatedSession.objects.all():
|
||||||
cache_key = f"{KEY_PREFIX}{session.session_key}"
|
cache_key = f"{KEY_PREFIX}{session.session_key}"
|
||||||
value = None
|
value = None
|
||||||
@ -49,6 +50,7 @@ def clean_expired_models(self: SystemTask):
|
|||||||
session.delete()
|
session.delete()
|
||||||
amount += 1
|
amount += 1
|
||||||
LOGGER.debug("Expired sessions", model=AuthenticatedSession, amount=amount)
|
LOGGER.debug("Expired sessions", model=AuthenticatedSession, amount=amount)
|
||||||
|
# pylint: disable=no-member
|
||||||
messages.append(f"Expired {amount} {AuthenticatedSession._meta.verbose_name_plural}")
|
messages.append(f"Expired {amount} {AuthenticatedSession._meta.verbose_name_plural}")
|
||||||
self.set_status(TaskStatus.SUCCESSFUL, *messages)
|
self.set_status(TaskStatus.SUCCESSFUL, *messages)
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@ from authentik.core.api.utils import JSONDictField, PassiveSerializer
|
|||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.flows.challenge import ChallengeResponse, ChallengeTypes, WithUserInfoChallenge
|
from authentik.flows.challenge import ChallengeResponse, ChallengeTypes, WithUserInfoChallenge
|
||||||
from authentik.flows.exceptions import FlowSkipStageException
|
from authentik.flows.exceptions import FlowSkipStageException, StageInvalidException
|
||||||
from authentik.flows.models import FlowDesignation, NotConfiguredAction, Stage
|
from authentik.flows.models import FlowDesignation, NotConfiguredAction, 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
|
||||||
@ -154,6 +154,16 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||||||
def get_device_challenges(self) -> list[dict]:
|
def get_device_challenges(self) -> list[dict]:
|
||||||
"""Get a list of all device challenges applicable for the current stage"""
|
"""Get a list of all device challenges applicable for the current stage"""
|
||||||
challenges = []
|
challenges = []
|
||||||
|
pending_user = self.get_pending_user()
|
||||||
|
if pending_user.is_anonymous:
|
||||||
|
# We shouldn't get here without any kind of authentication data
|
||||||
|
raise StageInvalidException()
|
||||||
|
# When `pretend_user_exists` is enabled in the identification stage,
|
||||||
|
# `pending_user` will be a user model that isn't save to the DB
|
||||||
|
# hence it doesn't have a PK. In that case we just return an empty list of
|
||||||
|
# authenticators
|
||||||
|
if not pending_user.pk:
|
||||||
|
return []
|
||||||
# Convert to a list to have usable log output instead of just <generator ...>
|
# Convert to a list to have usable log output instead of just <generator ...>
|
||||||
user_devices = list(devices_for_user(self.get_pending_user()))
|
user_devices = list(devices_for_user(self.get_pending_user()))
|
||||||
self.logger.debug("Got devices for user", devices=user_devices)
|
self.logger.debug("Got devices for user", devices=user_devices)
|
||||||
|
|||||||
@ -123,7 +123,7 @@ class IdentificationChallengeResponse(ChallengeResponse):
|
|||||||
if not current_stage.show_matched_user:
|
if not current_stage.show_matched_user:
|
||||||
self.stage.executor.plan.context[PLAN_CONTEXT_PENDING_USER_IDENTIFIER] = uid_field
|
self.stage.executor.plan.context[PLAN_CONTEXT_PENDING_USER_IDENTIFIER] = uid_field
|
||||||
# when `pretend` is enabled, continue regardless
|
# when `pretend` is enabled, continue regardless
|
||||||
if current_stage.pretend_user_exists:
|
if current_stage.pretend_user_exists and not current_stage.password_stage:
|
||||||
return attrs
|
return attrs
|
||||||
raise ValidationError("Failed to authenticate.")
|
raise ValidationError("Failed to authenticate.")
|
||||||
self.pre_user = pre_user
|
self.pre_user = pre_user
|
||||||
|
|||||||
@ -100,6 +100,42 @@ class TestIdentificationStage(FlowTestCase):
|
|||||||
user_fields=["email"],
|
user_fields=["email"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_invalid_with_password_pretend(self):
|
||||||
|
"""Test with invalid email and invalid password in single step (with pretend_user_exists)"""
|
||||||
|
self.stage.pretend_user_exists = True
|
||||||
|
pw_stage = PasswordStage.objects.create(name="password", backends=[BACKEND_INBUILT])
|
||||||
|
self.stage.password_stage = pw_stage
|
||||||
|
self.stage.save()
|
||||||
|
form_data = {
|
||||||
|
"uid_field": self.user.email + "test",
|
||||||
|
"password": self.user.username + "test",
|
||||||
|
}
|
||||||
|
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||||
|
response = self.client.post(url, form_data)
|
||||||
|
self.assertStageResponse(
|
||||||
|
response,
|
||||||
|
self.flow,
|
||||||
|
component="ak-stage-identification",
|
||||||
|
password_fields=True,
|
||||||
|
primary_action="Log in",
|
||||||
|
response_errors={
|
||||||
|
"non_field_errors": [{"code": "invalid", "string": "Failed to authenticate."}]
|
||||||
|
},
|
||||||
|
sources=[
|
||||||
|
{
|
||||||
|
"challenge": {
|
||||||
|
"component": "xak-flow-redirect",
|
||||||
|
"to": "/source/oauth/login/test/",
|
||||||
|
"type": ChallengeTypes.REDIRECT.value,
|
||||||
|
},
|
||||||
|
"icon_url": "/static/authentik/sources/default.svg",
|
||||||
|
"name": "test",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
show_source_labels=False,
|
||||||
|
user_fields=["email"],
|
||||||
|
)
|
||||||
|
|
||||||
def test_invalid_with_username(self):
|
def test_invalid_with_username(self):
|
||||||
"""Test invalid with username (user exists but stage only allows email)"""
|
"""Test invalid with username (user exists but stage only allows email)"""
|
||||||
form_data = {"uid_field": self.user.username}
|
form_data = {"uid_field": self.user.username}
|
||||||
|
|||||||
Reference in New Issue
Block a user