stages/authenticator_validate: save used mfa devices in login event

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2022-11-24 23:47:25 +01:00
parent 1e328436d8
commit f22f1ebcde
4 changed files with 102 additions and 4 deletions

View file

@ -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

View file

@ -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

View file

@ -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}/",
},
},
)

View file

@ -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(