stages/authenticator_validate: save used mfa devices in login event
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
1e328436d8
commit
f22f1ebcde
|
@ -207,6 +207,7 @@ def validate_challenge_duo(device_pk: int, stage_view: StageView, user: User) ->
|
|||
request=stage_view.request,
|
||||
stage=stage_view.executor.current_stage,
|
||||
device_class=DeviceClasses.DUO.value,
|
||||
duo_response=response,
|
||||
)
|
||||
raise ValidationError("Duo denied access", code="denied")
|
||||
return device
|
||||
|
|
|
@ -134,6 +134,12 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
|
|||
# Here we only check if the any data was sent at all
|
||||
if "code" not in attrs and "webauthn" not in attrs and "duo" not in attrs:
|
||||
raise ValidationError("Empty response")
|
||||
self.stage.executor.plan.context.setdefault(PLAN_CONTEXT_METHOD, "mfa")
|
||||
self.stage.executor.plan.context.setdefault(PLAN_CONTEXT_METHOD_ARGS, {})
|
||||
self.stage.executor.plan.context[PLAN_CONTEXT_METHOD_ARGS].setdefault("mfa_devices", [])
|
||||
self.stage.executor.plan.context[PLAN_CONTEXT_METHOD_ARGS]["mfa_devices"].append(
|
||||
self.device
|
||||
)
|
||||
return attrs
|
||||
|
||||
|
||||
|
|
|
@ -3,17 +3,22 @@ from unittest.mock import MagicMock, patch
|
|||
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.test.client import RequestFactory
|
||||
from django.urls import reverse
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.flows.planner import FlowPlan
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.flows.tests import FlowTestCase
|
||||
from authentik.flows.views.executor import FlowExecutorView
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN, FlowExecutorView
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.lib.tests.utils import dummy_get_response
|
||||
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
|
||||
from authentik.stages.authenticator_validate.challenge import validate_challenge_duo
|
||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
||||
from authentik.stages.user_login.models import UserLoginStage
|
||||
from authentik.tenants.utils import get_tenant_for_request
|
||||
|
||||
|
||||
|
@ -97,3 +102,88 @@ class AuthenticatorValidateStageDuoTests(FlowTestCase):
|
|||
),
|
||||
self.user,
|
||||
)
|
||||
|
||||
@patch(
|
||||
"authentik.stages.authenticator_duo.models.AuthenticatorDuoStage.auth_client",
|
||||
MagicMock(
|
||||
return_value=MagicMock(
|
||||
auth=MagicMock(
|
||||
return_value={
|
||||
"result": "allow",
|
||||
"status": "allow",
|
||||
"status_msg": "Success. Logging you in...",
|
||||
}
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
def test_full(self):
|
||||
"""Test full within a flow executor"""
|
||||
duo_stage = AuthenticatorDuoStage.objects.create(
|
||||
name=generate_id(),
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
api_hostname="",
|
||||
)
|
||||
duo_device = DuoDevice.objects.create(
|
||||
user=self.user,
|
||||
stage=duo_stage,
|
||||
)
|
||||
|
||||
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
|
||||
stage = AuthenticatorValidateStage.objects.create(
|
||||
name=generate_id(),
|
||||
device_classes=[DeviceClasses.DUO],
|
||||
)
|
||||
|
||||
plan = FlowPlan(flow_pk=flow.pk.hex)
|
||||
plan.append(FlowStageBinding.objects.create(target=flow, stage=stage, order=2))
|
||||
plan.append(
|
||||
FlowStageBinding.objects.create(
|
||||
target=flow, stage=UserLoginStage.objects.create(name=generate_id()), order=3
|
||||
)
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
{"duo": duo_device.pk},
|
||||
follow=True,
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
|
||||
event = Event.objects.filter(
|
||||
action=EventAction.LOGIN,
|
||||
user__pk=self.user.pk,
|
||||
).first()
|
||||
self.assertIsNotNone(event)
|
||||
self.assertEqual(
|
||||
event.context,
|
||||
{
|
||||
"auth_method": "mfa",
|
||||
"auth_method_args": {
|
||||
"mfa_devices": [
|
||||
{
|
||||
"app": "authentik_stages_authenticator_duo",
|
||||
"model_name": "duodevice",
|
||||
"name": "",
|
||||
"pk": duo_device.pk,
|
||||
}
|
||||
]
|
||||
},
|
||||
"http_request": {
|
||||
"args": {},
|
||||
"method": "GET",
|
||||
"path": f"/api/v3/flows/executor/{flow.slug}/",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
|
@ -11,6 +11,7 @@ from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
|||
from authentik.flows.tests import FlowTestCase
|
||||
from authentik.flows.tests.test_executor import TO_STAGE_RESPONSE_MOCK
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
from authentik.stages.password.models import PasswordStage
|
||||
|
||||
|
@ -25,7 +26,7 @@ class TestPasswordStage(FlowTestCase):
|
|||
self.user = create_test_admin_user()
|
||||
|
||||
self.flow = create_test_flow(FlowDesignation.AUTHENTICATION)
|
||||
self.stage = PasswordStage.objects.create(name="password", backends=[BACKEND_INBUILT])
|
||||
self.stage = PasswordStage.objects.create(name=generate_id(), backends=[BACKEND_INBUILT])
|
||||
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
||||
|
||||
@patch(
|
||||
|
|
Reference in New Issue