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,
|
request=stage_view.request,
|
||||||
stage=stage_view.executor.current_stage,
|
stage=stage_view.executor.current_stage,
|
||||||
device_class=DeviceClasses.DUO.value,
|
device_class=DeviceClasses.DUO.value,
|
||||||
|
duo_response=response,
|
||||||
)
|
)
|
||||||
raise ValidationError("Duo denied access", code="denied")
|
raise ValidationError("Duo denied access", code="denied")
|
||||||
return device
|
return device
|
||||||
|
|
|
@ -134,6 +134,12 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
|
||||||
# Here we only check if the any data was sent at all
|
# 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:
|
if "code" not in attrs and "webauthn" not in attrs and "duo" not in attrs:
|
||||||
raise ValidationError("Empty response")
|
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
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,17 +3,22 @@ from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from django.contrib.sessions.middleware import SessionMiddleware
|
from django.contrib.sessions.middleware import SessionMiddleware
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
|
from django.urls import reverse
|
||||||
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, create_test_flow
|
||||||
from authentik.flows.planner import FlowPlan
|
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.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 SESSION_KEY_PLAN, FlowExecutorView
|
||||||
from authentik.lib.generators import generate_id, generate_key
|
from authentik.lib.generators import generate_id, generate_key
|
||||||
from authentik.lib.tests.utils import dummy_get_response
|
from authentik.lib.tests.utils import dummy_get_response
|
||||||
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
|
||||||
|
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
|
from authentik.tenants.utils import get_tenant_for_request
|
||||||
|
|
||||||
|
|
||||||
|
@ -97,3 +102,88 @@ class AuthenticatorValidateStageDuoTests(FlowTestCase):
|
||||||
),
|
),
|
||||||
self.user,
|
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 import FlowTestCase
|
||||||
from authentik.flows.tests.test_executor import TO_STAGE_RESPONSE_MOCK
|
from authentik.flows.tests.test_executor import TO_STAGE_RESPONSE_MOCK
|
||||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
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 import BACKEND_INBUILT
|
||||||
from authentik.stages.password.models import PasswordStage
|
from authentik.stages.password.models import PasswordStage
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ class TestPasswordStage(FlowTestCase):
|
||||||
self.user = create_test_admin_user()
|
self.user = create_test_admin_user()
|
||||||
|
|
||||||
self.flow = create_test_flow(FlowDesignation.AUTHENTICATION)
|
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)
|
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
||||||
|
|
||||||
@patch(
|
@patch(
|
||||||
|
|
Reference in New Issue