authentik.stages.authenticator_validate.tests.test_webauthn
Test validator stage
1"""Test validator stage""" 2 3from time import sleep 4 5from django.urls.base import reverse 6from rest_framework.serializers import ValidationError 7from webauthn.helpers.base64url_to_bytes import base64url_to_bytes 8from webauthn.helpers.bytes_to_base64url import bytes_to_base64url 9 10from authentik.core.tests.utils import RequestFactory, create_test_admin_user, create_test_flow 11from authentik.flows.models import FlowStageBinding, NotConfiguredAction 12from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan 13from authentik.flows.stage import StageView 14from authentik.flows.tests import FlowTestCase 15from authentik.flows.views.executor import SESSION_KEY_PLAN, FlowExecutorView 16from authentik.lib.generators import generate_id 17from authentik.stages.authenticator_validate.challenge import ( 18 get_challenge_for_device, 19 get_webauthn_challenge_without_user, 20 validate_challenge_webauthn, 21) 22from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses 23from authentik.stages.authenticator_validate.stage import ( 24 PLAN_CONTEXT_DEVICE_CHALLENGES, 25 AuthenticatorValidateStageView, 26) 27from authentik.stages.authenticator_webauthn.models import ( 28 UserVerification, 29 WebAuthnDevice, 30 WebAuthnDeviceType, 31 WebAuthnHint, 32) 33from authentik.stages.authenticator_webauthn.stage import PLAN_CONTEXT_WEBAUTHN_CHALLENGE 34from authentik.stages.authenticator_webauthn.tasks import webauthn_mds_import 35from authentik.stages.identification.models import IdentificationStage, UserFields 36from authentik.stages.user_login.models import UserLoginStage 37 38 39class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): 40 """Test validator stage""" 41 42 def setUp(self) -> None: 43 self.user = create_test_admin_user() 44 self.request_factory = RequestFactory() 45 46 def test_last_auth_threshold(self): 47 """Test last_auth_threshold""" 48 ident_stage = IdentificationStage.objects.create( 49 name=generate_id(), 50 user_fields=[ 51 UserFields.USERNAME, 52 ], 53 ) 54 device: WebAuthnDevice = WebAuthnDevice.objects.create( 55 user=self.user, 56 confirmed=True, 57 ) 58 device.set_sign_count(device.sign_count + 1) 59 stage = AuthenticatorValidateStage.objects.create( 60 name=generate_id(), 61 last_auth_threshold="milliseconds=0", 62 not_configured_action=NotConfiguredAction.CONFIGURE, 63 device_classes=[DeviceClasses.WEBAUTHN], 64 ) 65 sleep(1) 66 stage.configuration_stages.set([ident_stage]) 67 flow = create_test_flow() 68 FlowStageBinding.objects.create(target=flow, stage=ident_stage, order=0) 69 FlowStageBinding.objects.create(target=flow, stage=stage, order=1) 70 71 response = self.client.post( 72 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 73 {"uid_field": self.user.username}, 74 ) 75 self.assertEqual(response.status_code, 302) 76 response = self.client.get( 77 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 78 follow=True, 79 ) 80 self.assertStageResponse( 81 response, 82 flow, 83 component="ak-stage-authenticator-validate", 84 ) 85 86 def test_device_challenge_webauthn(self): 87 """Test webauthn""" 88 request = self.request_factory.get("/") 89 request.user = self.user 90 91 webauthn_device = WebAuthnDevice.objects.create( 92 user=self.user, 93 public_key=bytes_to_base64url(b"qwerqwerqre"), 94 credential_id=bytes_to_base64url(b"foobarbaz"), 95 sign_count=0, 96 rp_id=generate_id(), 97 ) 98 stage = AuthenticatorValidateStage.objects.create( 99 name=generate_id(), 100 last_auth_threshold="milliseconds=0", 101 not_configured_action=NotConfiguredAction.CONFIGURE, 102 device_classes=[DeviceClasses.WEBAUTHN], 103 webauthn_user_verification=UserVerification.PREFERRED, 104 ) 105 plan = FlowPlan("") 106 stage_view = AuthenticatorValidateStageView( 107 FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request 108 ) 109 challenge = get_challenge_for_device(stage_view, stage, webauthn_device) 110 del challenge["challenge"] 111 self.assertEqual( 112 challenge, 113 { 114 "allowCredentials": [ 115 { 116 "id": "Zm9vYmFyYmF6", 117 "type": "public-key", 118 } 119 ], 120 "rpId": "testserver", 121 "timeout": 60000, 122 "userVerification": "preferred", 123 }, 124 ) 125 126 with self.assertRaises(ValidationError): 127 validate_challenge_webauthn( 128 {}, 129 StageView(FlowExecutorView(current_stage=stage, plan=plan), request=request), 130 self.user, 131 ) 132 133 def test_device_challenge_webauthn_restricted(self): 134 """Test webauthn (getting device challenges with a webauthn 135 device that is not allowed due to aaguid restrictions)""" 136 webauthn_mds_import.send(force=True).get_result() 137 request = self.request_factory.get("/") 138 request.user = self.user 139 140 WebAuthnDevice.objects.create( 141 user=self.user, 142 public_key=bytes_to_base64url(b"qwerqwerqre"), 143 credential_id=bytes_to_base64url(b"foobarbaz"), 144 sign_count=0, 145 rp_id=generate_id(), 146 device_type=WebAuthnDeviceType.objects.get( 147 aaguid="2fc0579f-8113-47ea-b116-bb5a8db9202a" 148 ), 149 ) 150 flow = create_test_flow() 151 stage = AuthenticatorValidateStage.objects.create( 152 name=generate_id(), 153 last_auth_threshold="milliseconds=0", 154 not_configured_action=NotConfiguredAction.DENY, 155 device_classes=[DeviceClasses.WEBAUTHN], 156 webauthn_user_verification=UserVerification.PREFERRED, 157 ) 158 stage.webauthn_allowed_device_types.set( 159 WebAuthnDeviceType.objects.filter(description="YubiKey 5 Series") 160 ) 161 session = self.client.session 162 plan = FlowPlan(flow_pk=flow.pk.hex) 163 plan.append_stage(stage) 164 plan.append_stage(UserLoginStage.objects.create(name=generate_id())) 165 plan.context[PLAN_CONTEXT_PENDING_USER] = self.user 166 session[SESSION_KEY_PLAN] = plan 167 session.save() 168 169 response = self.client.get( 170 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 171 ) 172 self.assertStageResponse( 173 response, 174 flow, 175 component="ak-stage-access-denied", 176 error_message="No (allowed) MFA authenticator configured.", 177 ) 178 179 def test_raw_get_challenge(self): 180 """Test webauthn""" 181 request = self.request_factory.get("/") 182 request.user = self.user 183 184 stage = AuthenticatorValidateStage.objects.create( 185 name=generate_id(), 186 last_auth_threshold="milliseconds=0", 187 not_configured_action=NotConfiguredAction.CONFIGURE, 188 device_classes=[DeviceClasses.WEBAUTHN], 189 webauthn_user_verification=UserVerification.PREFERRED, 190 ) 191 webauthn_device = WebAuthnDevice.objects.create( 192 user=self.user, 193 public_key=( 194 "pQECAyYgASFYIGsBLkklToCQkT7qJT_bJYN1sEc1oJdbnmoOc43i0J" 195 "H6IlggLTXytuhzFVYYAK4PQNj8_coGrbbzSfUxdiPAcZTQCyU" 196 ), 197 credential_id="QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 198 sign_count=0, 199 rp_id=generate_id(), 200 ) 201 plan = FlowPlan("") 202 plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes( 203 "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA" 204 ) 205 stage_view = AuthenticatorValidateStageView( 206 FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request 207 ) 208 challenge = get_challenge_for_device(stage_view, stage, webauthn_device) 209 self.assertEqual( 210 challenge["allowCredentials"], 211 [ 212 { 213 "id": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 214 "type": "public-key", 215 } 216 ], 217 ) 218 self.assertIsNotNone(challenge["challenge"]) 219 self.assertEqual( 220 challenge["rpId"], 221 "testserver", 222 ) 223 self.assertEqual( 224 challenge["timeout"], 225 60000, 226 ) 227 self.assertEqual( 228 challenge["userVerification"], 229 "preferred", 230 ) 231 232 def test_get_challenge_userless(self): 233 """Test webauthn (userless)""" 234 request = self.request_factory.get("/") 235 stage = AuthenticatorValidateStage.objects.create( 236 name=generate_id(), webauthn_user_verification=UserVerification.PREFERRED 237 ) 238 stage.refresh_from_db() 239 WebAuthnDevice.objects.create( 240 user=self.user, 241 public_key=( 242 "pQECAyYgASFYIGsBLkklToCQkT7qJT_bJYN1sEc1oJdbnmoOc43i0J" 243 "H6IlggLTXytuhzFVYYAK4PQNj8_coGrbbzSfUxdiPAcZTQCyU" 244 ), 245 credential_id="QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 246 sign_count=0, 247 rp_id=generate_id(), 248 ) 249 plan = FlowPlan("") 250 stage_view = AuthenticatorValidateStageView( 251 FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request 252 ) 253 challenge = get_webauthn_challenge_without_user(stage_view, stage) 254 self.assertEqual(challenge["allowCredentials"], []) 255 self.assertIsNotNone(challenge["challenge"]) 256 self.assertEqual(challenge["rpId"], "testserver") 257 self.assertEqual(challenge["timeout"], 60000) 258 self.assertEqual(challenge["userVerification"], "preferred") 259 260 def test_device_challenge_webauthn_with_hints(self): 261 """Test that webauthn hints are included in authentication challenge""" 262 request = self.request_factory.get("/") 263 request.user = self.user 264 265 webauthn_device = WebAuthnDevice.objects.create( 266 user=self.user, 267 public_key=bytes_to_base64url(b"qwerqwerqre"), 268 credential_id=bytes_to_base64url(b"foobarbaz"), 269 sign_count=0, 270 rp_id=generate_id(), 271 ) 272 stage = AuthenticatorValidateStage.objects.create( 273 name=generate_id(), 274 last_auth_threshold="milliseconds=0", 275 not_configured_action=NotConfiguredAction.CONFIGURE, 276 device_classes=[DeviceClasses.WEBAUTHN], 277 webauthn_user_verification=UserVerification.PREFERRED, 278 webauthn_hints=[WebAuthnHint.CLIENT_DEVICE, WebAuthnHint.HYBRID], 279 ) 280 plan = FlowPlan("") 281 stage_view = AuthenticatorValidateStageView( 282 FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request 283 ) 284 challenge = get_challenge_for_device(stage_view, stage, webauthn_device) 285 self.assertEqual(challenge["hints"], ["client-device", "hybrid"]) 286 287 def test_device_challenge_webauthn_no_hints(self): 288 """Test that hints key is absent when no hints configured""" 289 request = self.request_factory.get("/") 290 request.user = self.user 291 292 webauthn_device = WebAuthnDevice.objects.create( 293 user=self.user, 294 public_key=bytes_to_base64url(b"qwerqwerqre"), 295 credential_id=bytes_to_base64url(b"foobarbaz"), 296 sign_count=0, 297 rp_id=generate_id(), 298 ) 299 stage = AuthenticatorValidateStage.objects.create( 300 name=generate_id(), 301 last_auth_threshold="milliseconds=0", 302 not_configured_action=NotConfiguredAction.CONFIGURE, 303 device_classes=[DeviceClasses.WEBAUTHN], 304 webauthn_user_verification=UserVerification.PREFERRED, 305 ) 306 plan = FlowPlan("") 307 stage_view = AuthenticatorValidateStageView( 308 FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request 309 ) 310 challenge = get_challenge_for_device(stage_view, stage, webauthn_device) 311 self.assertNotIn("hints", challenge) 312 313 def test_get_challenge_userless_with_hints(self): 314 """Test that hints are included in userless/passwordless challenge""" 315 request = self.request_factory.get("/") 316 stage = AuthenticatorValidateStage.objects.create( 317 name=generate_id(), 318 webauthn_user_verification=UserVerification.PREFERRED, 319 webauthn_hints=[WebAuthnHint.SECURITY_KEY, WebAuthnHint.CLIENT_DEVICE], 320 ) 321 plan = FlowPlan("") 322 stage_view = AuthenticatorValidateStageView( 323 FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request 324 ) 325 challenge = get_webauthn_challenge_without_user(stage_view, stage) 326 self.assertEqual(challenge["hints"], ["security-key", "client-device"]) 327 328 def test_device_challenge_webauthn_hints_order_preserved(self): 329 """Test that hint order is preserved in authentication challenge""" 330 request = self.request_factory.get("/") 331 request.user = self.user 332 333 webauthn_device = WebAuthnDevice.objects.create( 334 user=self.user, 335 public_key=bytes_to_base64url(b"qwerqwerqre"), 336 credential_id=bytes_to_base64url(b"foobarbaz"), 337 sign_count=0, 338 rp_id=generate_id(), 339 ) 340 stage = AuthenticatorValidateStage.objects.create( 341 name=generate_id(), 342 last_auth_threshold="milliseconds=0", 343 not_configured_action=NotConfiguredAction.CONFIGURE, 344 device_classes=[DeviceClasses.WEBAUTHN], 345 webauthn_user_verification=UserVerification.PREFERRED, 346 webauthn_hints=[ 347 WebAuthnHint.HYBRID, 348 WebAuthnHint.SECURITY_KEY, 349 WebAuthnHint.CLIENT_DEVICE, 350 ], 351 ) 352 plan = FlowPlan("") 353 stage_view = AuthenticatorValidateStageView( 354 FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request 355 ) 356 challenge = get_challenge_for_device(stage_view, stage, webauthn_device) 357 self.assertEqual(challenge["hints"], ["hybrid", "security-key", "client-device"]) 358 359 def test_validate_challenge_unrestricted(self): 360 """Test webauthn authentication (unrestricted webauthn device)""" 361 webauthn_mds_import.send(force=True).get_result() 362 device = WebAuthnDevice.objects.create( 363 user=self.user, 364 public_key=( 365 "pQECAyYgASFYIF-N4GvQJdTJMAmTOxFX9_boL00zBiSrP0DY9xvJl_FFIlggnyZloVSVofdJNTLMeMdjQHgW2Rzmd5_Xt5AWtNztcdo" 366 ), 367 credential_id="X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm", 368 sign_count=2, 369 rp_id=generate_id(), 370 device_type=WebAuthnDeviceType.objects.get( 371 aaguid="2fc0579f-8113-47ea-b116-bb5a8db9202a" 372 ), 373 ) 374 flow = create_test_flow() 375 stage = AuthenticatorValidateStage.objects.create( 376 name=generate_id(), 377 not_configured_action=NotConfiguredAction.CONFIGURE, 378 device_classes=[DeviceClasses.WEBAUTHN], 379 ) 380 session = self.client.session 381 plan = FlowPlan(flow_pk=flow.pk.hex) 382 plan.append_stage(stage) 383 plan.append_stage(UserLoginStage.objects.create(name=generate_id())) 384 plan.context[PLAN_CONTEXT_PENDING_USER] = self.user 385 plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [ 386 { 387 "device_class": device.__class__.__name__.lower().replace("device", ""), 388 "device_uid": device.pk, 389 "challenge": {}, 390 "last_used": None, 391 } 392 ] 393 plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes( 394 "aCC6ak_DP45xMH1qyxzUM5iC2xc4QthQb09v7m4qDBmY8FvWvhxFzSuFlDYQmclrh5fWS5q0TPxgJGF4vimcFQ" 395 ) 396 session[SESSION_KEY_PLAN] = plan 397 session.save() 398 399 response = self.client.post( 400 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 401 data={ 402 "webauthn": { 403 "id": "X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm", 404 "rawId": "X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm", 405 "type": "public-key", 406 "assertionClientExtensions": "{}", 407 "response": { 408 "clientDataJSON": ( 409 "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiYUNDN" 410 "mFrX0RQNDV4TUgxcXl4elVNNWlDMnhjNFF0aFFiMDl2N200cURCbV" 411 "k4RnZXdmh4RnpTdUZsRFlRbWNscmg1ZldTNXEwVFB4Z0pHRjR2aW1" 412 "jRlEiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjkwMDAiLCJj" 413 "cm9zc09yaWdpbiI6ZmFsc2V9" 414 ), 415 "signature": ( 416 "MEQCIAHQCGfE_PX1z6mBDaXUNqK_NrllhXylNOmETUD3Khv9AiBTl" 417 "rX3GDRj5OaOfTToOwUwAhtd74tu0T6DZAVHPb_hlQ==" 418 ), 419 "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABg==", 420 "userHandle": None, 421 }, 422 }, 423 }, 424 SERVER_NAME="localhost", 425 SERVER_PORT="9000", 426 ) 427 self.assertEqual(response.status_code, 302) 428 response = self.client.get( 429 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 430 ) 431 self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) 432 433 def test_validate_challenge_restricted(self): 434 """Test webauthn authentication (restricted device type, failure)""" 435 webauthn_mds_import.send(force=True).get_result() 436 device = WebAuthnDevice.objects.create( 437 user=self.user, 438 public_key=( 439 "pQECAyYgASFYIF-N4GvQJdTJMAmTOxFX9_boL00zBiSrP0DY9xvJl_FFIlggnyZloVSVofdJNTLMeMdjQHgW2Rzmd5_Xt5AWtNztcdo" 440 ), 441 credential_id="X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm", 442 sign_count=2, 443 rp_id=generate_id(), 444 device_type=WebAuthnDeviceType.objects.get( 445 aaguid="2fc0579f-8113-47ea-b116-bb5a8db9202a" 446 ), 447 ) 448 flow = create_test_flow() 449 stage = AuthenticatorValidateStage.objects.create( 450 name=generate_id(), 451 not_configured_action=NotConfiguredAction.CONFIGURE, 452 device_classes=[DeviceClasses.WEBAUTHN], 453 ) 454 stage.webauthn_allowed_device_types.set( 455 WebAuthnDeviceType.objects.filter(description="YubiKey 5 Series") 456 ) 457 session = self.client.session 458 plan = FlowPlan(flow_pk=flow.pk.hex) 459 plan.append_stage(stage) 460 plan.append_stage(UserLoginStage.objects.create(name=generate_id())) 461 plan.context[PLAN_CONTEXT_PENDING_USER] = self.user 462 plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [ 463 { 464 "device_class": device.__class__.__name__.lower().replace("device", ""), 465 "device_uid": device.pk, 466 "challenge": {}, 467 "last_used": None, 468 } 469 ] 470 plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes( 471 "aCC6ak_DP45xMH1qyxzUM5iC2xc4QthQb09v7m4qDBmY8FvWvhxFzSuFlDYQmclrh5fWS5q0TPxgJGF4vimcFQ" 472 ) 473 session[SESSION_KEY_PLAN] = plan 474 session.save() 475 476 response = self.client.post( 477 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 478 data={ 479 "webauthn": { 480 "id": "X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm", 481 "rawId": "X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm", 482 "type": "public-key", 483 "assertionClientExtensions": "{}", 484 "response": { 485 "clientDataJSON": ( 486 "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiYUNDN" 487 "mFrX0RQNDV4TUgxcXl4elVNNWlDMnhjNFF0aFFiMDl2N200cURCbV" 488 "k4RnZXdmh4RnpTdUZsRFlRbWNscmg1ZldTNXEwVFB4Z0pHRjR2aW1" 489 "jRlEiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjkwMDAiLCJj" 490 "cm9zc09yaWdpbiI6ZmFsc2V9" 491 ), 492 "signature": ( 493 "MEQCIAHQCGfE_PX1z6mBDaXUNqK_NrllhXylNOmETUD3Khv9AiBTl" 494 "rX3GDRj5OaOfTToOwUwAhtd74tu0T6DZAVHPb_hlQ==" 495 ), 496 "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABg==", 497 "userHandle": None, 498 }, 499 } 500 }, 501 SERVER_NAME="localhost", 502 SERVER_PORT="9000", 503 ) 504 self.assertEqual(response.status_code, 200) 505 self.assertStageResponse( 506 response, 507 flow, 508 component="ak-stage-authenticator-validate", 509 response_errors={ 510 "webauthn": [ 511 { 512 "string": ( 513 "Invalid device type. Contact your authentik administrator for help." 514 ), 515 "code": "invalid", 516 } 517 ] 518 }, 519 ) 520 521 def test_validate_challenge_userless(self): 522 """Test webauthn""" 523 device = WebAuthnDevice.objects.create( 524 user=self.user, 525 public_key=( 526 "pQECAyYgASFYIGsBLkklToCQkT7qJT_bJYN1sEc1oJdbnmoOc43i0J" 527 "H6IlggLTXytuhzFVYYAK4PQNj8_coGrbbzSfUxdiPAcZTQCyU" 528 ), 529 credential_id="QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 530 sign_count=4, 531 rp_id=generate_id(), 532 ) 533 flow = create_test_flow() 534 stage = AuthenticatorValidateStage.objects.create( 535 name=generate_id(), 536 not_configured_action=NotConfiguredAction.CONFIGURE, 537 device_classes=[DeviceClasses.WEBAUTHN], 538 ) 539 session = self.client.session 540 plan = FlowPlan(flow_pk=flow.pk.hex) 541 plan.append_stage(stage) 542 plan.append_stage(UserLoginStage.objects.create(name=generate_id())) 543 plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [ 544 { 545 "device_class": device.__class__.__name__.lower().replace("device", ""), 546 "device_uid": device.pk, 547 "challenge": {}, 548 "last_used": None, 549 } 550 ] 551 plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes( 552 "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA" 553 ) 554 session[SESSION_KEY_PLAN] = plan 555 session.save() 556 557 response = self.client.post( 558 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 559 data={ 560 "webauthn": { 561 "id": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 562 "rawId": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 563 "type": "public-key", 564 "assertionClientExtensions": "{}", 565 "response": { 566 "clientDataJSON": ( 567 "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZzk4STUxbVF2Wlhv" 568 "NWx4TGZockQyemZvbGhaYkxSeUNncWtrWWFwMWp3U2FKMTNCZ3VvSldDRjlfTGcz" 569 "QWdPNFdoLUJxYTU1NkpFMjBvS3NZYmw2UkEiLCJvcmlnaW4iOiJodHRwOi8vbG9j" 570 "YWxob3N0OjkwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2UsIm90aGVyX2tleXNfY2Fu" 571 "X2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBh" 572 "Z2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgifQ==" 573 ), 574 "signature": ( 575 "MEQCIFNlrHf9ablJAalXLWkrqvHB8oIu8kwvRpH3X3rbJVpI" 576 "AiAqtOK6mIZPk62kZN0OzFsHfuvu_RlOl7zlqSNzDdz_Ag==" 577 ), 578 "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABQ==", 579 "userHandle": None, 580 }, 581 }, 582 }, 583 SERVER_NAME="localhost", 584 SERVER_PORT="9000", 585 ) 586 self.assertEqual(response.status_code, 302) 587 response = self.client.get( 588 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 589 ) 590 self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) 591 592 def test_validate_challenge_invalid(self): 593 """Test webauthn""" 594 request = self.request_factory.get("/") 595 request.user = self.user 596 597 WebAuthnDevice.objects.create( 598 user=self.user, 599 public_key=( 600 "pQECAyYgASFYIGsBLkklToCQkT7qJT_bJYN1sEc1oJdbnmoOc4" 601 "3i0JH6IlggLTXytuhzFVYYAK4PQNj8_coGrbbzSfUxdiPAcZTQCyU" 602 ), 603 credential_id="QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 604 # One more sign count than above, make it invalid 605 sign_count=5, 606 rp_id=generate_id(), 607 ) 608 flow = create_test_flow() 609 stage = AuthenticatorValidateStage.objects.create( 610 name=generate_id(), 611 not_configured_action=NotConfiguredAction.CONFIGURE, 612 device_classes=[DeviceClasses.WEBAUTHN], 613 ) 614 plan = FlowPlan(flow.pk.hex) 615 plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes( 616 "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA" 617 ) 618 request = self.request_factory.get("/") 619 620 stage_view = AuthenticatorValidateStageView( 621 FlowExecutorView(flow=flow, current_stage=stage, plan=plan), request=request 622 ) 623 request.META["SERVER_NAME"] = "localhost" 624 request.META["SERVER_PORT"] = "9000" 625 with self.assertRaises(ValidationError): 626 validate_challenge_webauthn( 627 { 628 "id": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 629 "rawId": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 630 "type": "public-key", 631 "assertionClientExtensions": "{}", 632 "response": { 633 "clientDataJSON": ( 634 "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZzk4STUxbVF2WlhvNWx4" 635 "TGZockQyemZvbGhaYkxSeUNncWtrWWFwMWp3U2FKMTNCZ3VvSldDRjlfTGczQWdPNFdo" 636 "LUJxYTU1NkpFMjBvS3NZYmw2UkEiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0Ojkw" 637 "MDAiLCJjcm9zc09yaWdpbiI6ZmFsc2UsIm90aGVyX2tleXNfY2FuX2JlX2FkZGVkX2hl" 638 "cmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2FpbnN0IGEgdGVtcGxh" 639 "dGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgifQ==" 640 ), 641 "signature": ( 642 "MEQCIFNlrHf9ablJAalXLWkrqvHB8oIu8kwvRpH3X3rbJVpI" 643 "AiAqtOK6mIZPk62kZN0OzFsHfuvu_RlOl7zlqSNzDdz_Ag==" 644 ), 645 "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABQ==", 646 "userHandle": None, 647 }, 648 }, 649 stage_view, 650 self.user, 651 )
40class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): 41 """Test validator stage""" 42 43 def setUp(self) -> None: 44 self.user = create_test_admin_user() 45 self.request_factory = RequestFactory() 46 47 def test_last_auth_threshold(self): 48 """Test last_auth_threshold""" 49 ident_stage = IdentificationStage.objects.create( 50 name=generate_id(), 51 user_fields=[ 52 UserFields.USERNAME, 53 ], 54 ) 55 device: WebAuthnDevice = WebAuthnDevice.objects.create( 56 user=self.user, 57 confirmed=True, 58 ) 59 device.set_sign_count(device.sign_count + 1) 60 stage = AuthenticatorValidateStage.objects.create( 61 name=generate_id(), 62 last_auth_threshold="milliseconds=0", 63 not_configured_action=NotConfiguredAction.CONFIGURE, 64 device_classes=[DeviceClasses.WEBAUTHN], 65 ) 66 sleep(1) 67 stage.configuration_stages.set([ident_stage]) 68 flow = create_test_flow() 69 FlowStageBinding.objects.create(target=flow, stage=ident_stage, order=0) 70 FlowStageBinding.objects.create(target=flow, stage=stage, order=1) 71 72 response = self.client.post( 73 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 74 {"uid_field": self.user.username}, 75 ) 76 self.assertEqual(response.status_code, 302) 77 response = self.client.get( 78 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 79 follow=True, 80 ) 81 self.assertStageResponse( 82 response, 83 flow, 84 component="ak-stage-authenticator-validate", 85 ) 86 87 def test_device_challenge_webauthn(self): 88 """Test webauthn""" 89 request = self.request_factory.get("/") 90 request.user = self.user 91 92 webauthn_device = WebAuthnDevice.objects.create( 93 user=self.user, 94 public_key=bytes_to_base64url(b"qwerqwerqre"), 95 credential_id=bytes_to_base64url(b"foobarbaz"), 96 sign_count=0, 97 rp_id=generate_id(), 98 ) 99 stage = AuthenticatorValidateStage.objects.create( 100 name=generate_id(), 101 last_auth_threshold="milliseconds=0", 102 not_configured_action=NotConfiguredAction.CONFIGURE, 103 device_classes=[DeviceClasses.WEBAUTHN], 104 webauthn_user_verification=UserVerification.PREFERRED, 105 ) 106 plan = FlowPlan("") 107 stage_view = AuthenticatorValidateStageView( 108 FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request 109 ) 110 challenge = get_challenge_for_device(stage_view, stage, webauthn_device) 111 del challenge["challenge"] 112 self.assertEqual( 113 challenge, 114 { 115 "allowCredentials": [ 116 { 117 "id": "Zm9vYmFyYmF6", 118 "type": "public-key", 119 } 120 ], 121 "rpId": "testserver", 122 "timeout": 60000, 123 "userVerification": "preferred", 124 }, 125 ) 126 127 with self.assertRaises(ValidationError): 128 validate_challenge_webauthn( 129 {}, 130 StageView(FlowExecutorView(current_stage=stage, plan=plan), request=request), 131 self.user, 132 ) 133 134 def test_device_challenge_webauthn_restricted(self): 135 """Test webauthn (getting device challenges with a webauthn 136 device that is not allowed due to aaguid restrictions)""" 137 webauthn_mds_import.send(force=True).get_result() 138 request = self.request_factory.get("/") 139 request.user = self.user 140 141 WebAuthnDevice.objects.create( 142 user=self.user, 143 public_key=bytes_to_base64url(b"qwerqwerqre"), 144 credential_id=bytes_to_base64url(b"foobarbaz"), 145 sign_count=0, 146 rp_id=generate_id(), 147 device_type=WebAuthnDeviceType.objects.get( 148 aaguid="2fc0579f-8113-47ea-b116-bb5a8db9202a" 149 ), 150 ) 151 flow = create_test_flow() 152 stage = AuthenticatorValidateStage.objects.create( 153 name=generate_id(), 154 last_auth_threshold="milliseconds=0", 155 not_configured_action=NotConfiguredAction.DENY, 156 device_classes=[DeviceClasses.WEBAUTHN], 157 webauthn_user_verification=UserVerification.PREFERRED, 158 ) 159 stage.webauthn_allowed_device_types.set( 160 WebAuthnDeviceType.objects.filter(description="YubiKey 5 Series") 161 ) 162 session = self.client.session 163 plan = FlowPlan(flow_pk=flow.pk.hex) 164 plan.append_stage(stage) 165 plan.append_stage(UserLoginStage.objects.create(name=generate_id())) 166 plan.context[PLAN_CONTEXT_PENDING_USER] = self.user 167 session[SESSION_KEY_PLAN] = plan 168 session.save() 169 170 response = self.client.get( 171 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 172 ) 173 self.assertStageResponse( 174 response, 175 flow, 176 component="ak-stage-access-denied", 177 error_message="No (allowed) MFA authenticator configured.", 178 ) 179 180 def test_raw_get_challenge(self): 181 """Test webauthn""" 182 request = self.request_factory.get("/") 183 request.user = self.user 184 185 stage = AuthenticatorValidateStage.objects.create( 186 name=generate_id(), 187 last_auth_threshold="milliseconds=0", 188 not_configured_action=NotConfiguredAction.CONFIGURE, 189 device_classes=[DeviceClasses.WEBAUTHN], 190 webauthn_user_verification=UserVerification.PREFERRED, 191 ) 192 webauthn_device = WebAuthnDevice.objects.create( 193 user=self.user, 194 public_key=( 195 "pQECAyYgASFYIGsBLkklToCQkT7qJT_bJYN1sEc1oJdbnmoOc43i0J" 196 "H6IlggLTXytuhzFVYYAK4PQNj8_coGrbbzSfUxdiPAcZTQCyU" 197 ), 198 credential_id="QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 199 sign_count=0, 200 rp_id=generate_id(), 201 ) 202 plan = FlowPlan("") 203 plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes( 204 "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA" 205 ) 206 stage_view = AuthenticatorValidateStageView( 207 FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request 208 ) 209 challenge = get_challenge_for_device(stage_view, stage, webauthn_device) 210 self.assertEqual( 211 challenge["allowCredentials"], 212 [ 213 { 214 "id": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 215 "type": "public-key", 216 } 217 ], 218 ) 219 self.assertIsNotNone(challenge["challenge"]) 220 self.assertEqual( 221 challenge["rpId"], 222 "testserver", 223 ) 224 self.assertEqual( 225 challenge["timeout"], 226 60000, 227 ) 228 self.assertEqual( 229 challenge["userVerification"], 230 "preferred", 231 ) 232 233 def test_get_challenge_userless(self): 234 """Test webauthn (userless)""" 235 request = self.request_factory.get("/") 236 stage = AuthenticatorValidateStage.objects.create( 237 name=generate_id(), webauthn_user_verification=UserVerification.PREFERRED 238 ) 239 stage.refresh_from_db() 240 WebAuthnDevice.objects.create( 241 user=self.user, 242 public_key=( 243 "pQECAyYgASFYIGsBLkklToCQkT7qJT_bJYN1sEc1oJdbnmoOc43i0J" 244 "H6IlggLTXytuhzFVYYAK4PQNj8_coGrbbzSfUxdiPAcZTQCyU" 245 ), 246 credential_id="QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 247 sign_count=0, 248 rp_id=generate_id(), 249 ) 250 plan = FlowPlan("") 251 stage_view = AuthenticatorValidateStageView( 252 FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request 253 ) 254 challenge = get_webauthn_challenge_without_user(stage_view, stage) 255 self.assertEqual(challenge["allowCredentials"], []) 256 self.assertIsNotNone(challenge["challenge"]) 257 self.assertEqual(challenge["rpId"], "testserver") 258 self.assertEqual(challenge["timeout"], 60000) 259 self.assertEqual(challenge["userVerification"], "preferred") 260 261 def test_device_challenge_webauthn_with_hints(self): 262 """Test that webauthn hints are included in authentication challenge""" 263 request = self.request_factory.get("/") 264 request.user = self.user 265 266 webauthn_device = WebAuthnDevice.objects.create( 267 user=self.user, 268 public_key=bytes_to_base64url(b"qwerqwerqre"), 269 credential_id=bytes_to_base64url(b"foobarbaz"), 270 sign_count=0, 271 rp_id=generate_id(), 272 ) 273 stage = AuthenticatorValidateStage.objects.create( 274 name=generate_id(), 275 last_auth_threshold="milliseconds=0", 276 not_configured_action=NotConfiguredAction.CONFIGURE, 277 device_classes=[DeviceClasses.WEBAUTHN], 278 webauthn_user_verification=UserVerification.PREFERRED, 279 webauthn_hints=[WebAuthnHint.CLIENT_DEVICE, WebAuthnHint.HYBRID], 280 ) 281 plan = FlowPlan("") 282 stage_view = AuthenticatorValidateStageView( 283 FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request 284 ) 285 challenge = get_challenge_for_device(stage_view, stage, webauthn_device) 286 self.assertEqual(challenge["hints"], ["client-device", "hybrid"]) 287 288 def test_device_challenge_webauthn_no_hints(self): 289 """Test that hints key is absent when no hints configured""" 290 request = self.request_factory.get("/") 291 request.user = self.user 292 293 webauthn_device = WebAuthnDevice.objects.create( 294 user=self.user, 295 public_key=bytes_to_base64url(b"qwerqwerqre"), 296 credential_id=bytes_to_base64url(b"foobarbaz"), 297 sign_count=0, 298 rp_id=generate_id(), 299 ) 300 stage = AuthenticatorValidateStage.objects.create( 301 name=generate_id(), 302 last_auth_threshold="milliseconds=0", 303 not_configured_action=NotConfiguredAction.CONFIGURE, 304 device_classes=[DeviceClasses.WEBAUTHN], 305 webauthn_user_verification=UserVerification.PREFERRED, 306 ) 307 plan = FlowPlan("") 308 stage_view = AuthenticatorValidateStageView( 309 FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request 310 ) 311 challenge = get_challenge_for_device(stage_view, stage, webauthn_device) 312 self.assertNotIn("hints", challenge) 313 314 def test_get_challenge_userless_with_hints(self): 315 """Test that hints are included in userless/passwordless challenge""" 316 request = self.request_factory.get("/") 317 stage = AuthenticatorValidateStage.objects.create( 318 name=generate_id(), 319 webauthn_user_verification=UserVerification.PREFERRED, 320 webauthn_hints=[WebAuthnHint.SECURITY_KEY, WebAuthnHint.CLIENT_DEVICE], 321 ) 322 plan = FlowPlan("") 323 stage_view = AuthenticatorValidateStageView( 324 FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request 325 ) 326 challenge = get_webauthn_challenge_without_user(stage_view, stage) 327 self.assertEqual(challenge["hints"], ["security-key", "client-device"]) 328 329 def test_device_challenge_webauthn_hints_order_preserved(self): 330 """Test that hint order is preserved in authentication challenge""" 331 request = self.request_factory.get("/") 332 request.user = self.user 333 334 webauthn_device = WebAuthnDevice.objects.create( 335 user=self.user, 336 public_key=bytes_to_base64url(b"qwerqwerqre"), 337 credential_id=bytes_to_base64url(b"foobarbaz"), 338 sign_count=0, 339 rp_id=generate_id(), 340 ) 341 stage = AuthenticatorValidateStage.objects.create( 342 name=generate_id(), 343 last_auth_threshold="milliseconds=0", 344 not_configured_action=NotConfiguredAction.CONFIGURE, 345 device_classes=[DeviceClasses.WEBAUTHN], 346 webauthn_user_verification=UserVerification.PREFERRED, 347 webauthn_hints=[ 348 WebAuthnHint.HYBRID, 349 WebAuthnHint.SECURITY_KEY, 350 WebAuthnHint.CLIENT_DEVICE, 351 ], 352 ) 353 plan = FlowPlan("") 354 stage_view = AuthenticatorValidateStageView( 355 FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request 356 ) 357 challenge = get_challenge_for_device(stage_view, stage, webauthn_device) 358 self.assertEqual(challenge["hints"], ["hybrid", "security-key", "client-device"]) 359 360 def test_validate_challenge_unrestricted(self): 361 """Test webauthn authentication (unrestricted webauthn device)""" 362 webauthn_mds_import.send(force=True).get_result() 363 device = WebAuthnDevice.objects.create( 364 user=self.user, 365 public_key=( 366 "pQECAyYgASFYIF-N4GvQJdTJMAmTOxFX9_boL00zBiSrP0DY9xvJl_FFIlggnyZloVSVofdJNTLMeMdjQHgW2Rzmd5_Xt5AWtNztcdo" 367 ), 368 credential_id="X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm", 369 sign_count=2, 370 rp_id=generate_id(), 371 device_type=WebAuthnDeviceType.objects.get( 372 aaguid="2fc0579f-8113-47ea-b116-bb5a8db9202a" 373 ), 374 ) 375 flow = create_test_flow() 376 stage = AuthenticatorValidateStage.objects.create( 377 name=generate_id(), 378 not_configured_action=NotConfiguredAction.CONFIGURE, 379 device_classes=[DeviceClasses.WEBAUTHN], 380 ) 381 session = self.client.session 382 plan = FlowPlan(flow_pk=flow.pk.hex) 383 plan.append_stage(stage) 384 plan.append_stage(UserLoginStage.objects.create(name=generate_id())) 385 plan.context[PLAN_CONTEXT_PENDING_USER] = self.user 386 plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [ 387 { 388 "device_class": device.__class__.__name__.lower().replace("device", ""), 389 "device_uid": device.pk, 390 "challenge": {}, 391 "last_used": None, 392 } 393 ] 394 plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes( 395 "aCC6ak_DP45xMH1qyxzUM5iC2xc4QthQb09v7m4qDBmY8FvWvhxFzSuFlDYQmclrh5fWS5q0TPxgJGF4vimcFQ" 396 ) 397 session[SESSION_KEY_PLAN] = plan 398 session.save() 399 400 response = self.client.post( 401 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 402 data={ 403 "webauthn": { 404 "id": "X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm", 405 "rawId": "X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm", 406 "type": "public-key", 407 "assertionClientExtensions": "{}", 408 "response": { 409 "clientDataJSON": ( 410 "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiYUNDN" 411 "mFrX0RQNDV4TUgxcXl4elVNNWlDMnhjNFF0aFFiMDl2N200cURCbV" 412 "k4RnZXdmh4RnpTdUZsRFlRbWNscmg1ZldTNXEwVFB4Z0pHRjR2aW1" 413 "jRlEiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjkwMDAiLCJj" 414 "cm9zc09yaWdpbiI6ZmFsc2V9" 415 ), 416 "signature": ( 417 "MEQCIAHQCGfE_PX1z6mBDaXUNqK_NrllhXylNOmETUD3Khv9AiBTl" 418 "rX3GDRj5OaOfTToOwUwAhtd74tu0T6DZAVHPb_hlQ==" 419 ), 420 "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABg==", 421 "userHandle": None, 422 }, 423 }, 424 }, 425 SERVER_NAME="localhost", 426 SERVER_PORT="9000", 427 ) 428 self.assertEqual(response.status_code, 302) 429 response = self.client.get( 430 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 431 ) 432 self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) 433 434 def test_validate_challenge_restricted(self): 435 """Test webauthn authentication (restricted device type, failure)""" 436 webauthn_mds_import.send(force=True).get_result() 437 device = WebAuthnDevice.objects.create( 438 user=self.user, 439 public_key=( 440 "pQECAyYgASFYIF-N4GvQJdTJMAmTOxFX9_boL00zBiSrP0DY9xvJl_FFIlggnyZloVSVofdJNTLMeMdjQHgW2Rzmd5_Xt5AWtNztcdo" 441 ), 442 credential_id="X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm", 443 sign_count=2, 444 rp_id=generate_id(), 445 device_type=WebAuthnDeviceType.objects.get( 446 aaguid="2fc0579f-8113-47ea-b116-bb5a8db9202a" 447 ), 448 ) 449 flow = create_test_flow() 450 stage = AuthenticatorValidateStage.objects.create( 451 name=generate_id(), 452 not_configured_action=NotConfiguredAction.CONFIGURE, 453 device_classes=[DeviceClasses.WEBAUTHN], 454 ) 455 stage.webauthn_allowed_device_types.set( 456 WebAuthnDeviceType.objects.filter(description="YubiKey 5 Series") 457 ) 458 session = self.client.session 459 plan = FlowPlan(flow_pk=flow.pk.hex) 460 plan.append_stage(stage) 461 plan.append_stage(UserLoginStage.objects.create(name=generate_id())) 462 plan.context[PLAN_CONTEXT_PENDING_USER] = self.user 463 plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [ 464 { 465 "device_class": device.__class__.__name__.lower().replace("device", ""), 466 "device_uid": device.pk, 467 "challenge": {}, 468 "last_used": None, 469 } 470 ] 471 plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes( 472 "aCC6ak_DP45xMH1qyxzUM5iC2xc4QthQb09v7m4qDBmY8FvWvhxFzSuFlDYQmclrh5fWS5q0TPxgJGF4vimcFQ" 473 ) 474 session[SESSION_KEY_PLAN] = plan 475 session.save() 476 477 response = self.client.post( 478 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 479 data={ 480 "webauthn": { 481 "id": "X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm", 482 "rawId": "X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm", 483 "type": "public-key", 484 "assertionClientExtensions": "{}", 485 "response": { 486 "clientDataJSON": ( 487 "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiYUNDN" 488 "mFrX0RQNDV4TUgxcXl4elVNNWlDMnhjNFF0aFFiMDl2N200cURCbV" 489 "k4RnZXdmh4RnpTdUZsRFlRbWNscmg1ZldTNXEwVFB4Z0pHRjR2aW1" 490 "jRlEiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjkwMDAiLCJj" 491 "cm9zc09yaWdpbiI6ZmFsc2V9" 492 ), 493 "signature": ( 494 "MEQCIAHQCGfE_PX1z6mBDaXUNqK_NrllhXylNOmETUD3Khv9AiBTl" 495 "rX3GDRj5OaOfTToOwUwAhtd74tu0T6DZAVHPb_hlQ==" 496 ), 497 "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABg==", 498 "userHandle": None, 499 }, 500 } 501 }, 502 SERVER_NAME="localhost", 503 SERVER_PORT="9000", 504 ) 505 self.assertEqual(response.status_code, 200) 506 self.assertStageResponse( 507 response, 508 flow, 509 component="ak-stage-authenticator-validate", 510 response_errors={ 511 "webauthn": [ 512 { 513 "string": ( 514 "Invalid device type. Contact your authentik administrator for help." 515 ), 516 "code": "invalid", 517 } 518 ] 519 }, 520 ) 521 522 def test_validate_challenge_userless(self): 523 """Test webauthn""" 524 device = WebAuthnDevice.objects.create( 525 user=self.user, 526 public_key=( 527 "pQECAyYgASFYIGsBLkklToCQkT7qJT_bJYN1sEc1oJdbnmoOc43i0J" 528 "H6IlggLTXytuhzFVYYAK4PQNj8_coGrbbzSfUxdiPAcZTQCyU" 529 ), 530 credential_id="QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 531 sign_count=4, 532 rp_id=generate_id(), 533 ) 534 flow = create_test_flow() 535 stage = AuthenticatorValidateStage.objects.create( 536 name=generate_id(), 537 not_configured_action=NotConfiguredAction.CONFIGURE, 538 device_classes=[DeviceClasses.WEBAUTHN], 539 ) 540 session = self.client.session 541 plan = FlowPlan(flow_pk=flow.pk.hex) 542 plan.append_stage(stage) 543 plan.append_stage(UserLoginStage.objects.create(name=generate_id())) 544 plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [ 545 { 546 "device_class": device.__class__.__name__.lower().replace("device", ""), 547 "device_uid": device.pk, 548 "challenge": {}, 549 "last_used": None, 550 } 551 ] 552 plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes( 553 "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA" 554 ) 555 session[SESSION_KEY_PLAN] = plan 556 session.save() 557 558 response = self.client.post( 559 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 560 data={ 561 "webauthn": { 562 "id": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 563 "rawId": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 564 "type": "public-key", 565 "assertionClientExtensions": "{}", 566 "response": { 567 "clientDataJSON": ( 568 "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZzk4STUxbVF2Wlhv" 569 "NWx4TGZockQyemZvbGhaYkxSeUNncWtrWWFwMWp3U2FKMTNCZ3VvSldDRjlfTGcz" 570 "QWdPNFdoLUJxYTU1NkpFMjBvS3NZYmw2UkEiLCJvcmlnaW4iOiJodHRwOi8vbG9j" 571 "YWxob3N0OjkwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2UsIm90aGVyX2tleXNfY2Fu" 572 "X2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBh" 573 "Z2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgifQ==" 574 ), 575 "signature": ( 576 "MEQCIFNlrHf9ablJAalXLWkrqvHB8oIu8kwvRpH3X3rbJVpI" 577 "AiAqtOK6mIZPk62kZN0OzFsHfuvu_RlOl7zlqSNzDdz_Ag==" 578 ), 579 "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABQ==", 580 "userHandle": None, 581 }, 582 }, 583 }, 584 SERVER_NAME="localhost", 585 SERVER_PORT="9000", 586 ) 587 self.assertEqual(response.status_code, 302) 588 response = self.client.get( 589 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 590 ) 591 self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) 592 593 def test_validate_challenge_invalid(self): 594 """Test webauthn""" 595 request = self.request_factory.get("/") 596 request.user = self.user 597 598 WebAuthnDevice.objects.create( 599 user=self.user, 600 public_key=( 601 "pQECAyYgASFYIGsBLkklToCQkT7qJT_bJYN1sEc1oJdbnmoOc4" 602 "3i0JH6IlggLTXytuhzFVYYAK4PQNj8_coGrbbzSfUxdiPAcZTQCyU" 603 ), 604 credential_id="QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 605 # One more sign count than above, make it invalid 606 sign_count=5, 607 rp_id=generate_id(), 608 ) 609 flow = create_test_flow() 610 stage = AuthenticatorValidateStage.objects.create( 611 name=generate_id(), 612 not_configured_action=NotConfiguredAction.CONFIGURE, 613 device_classes=[DeviceClasses.WEBAUTHN], 614 ) 615 plan = FlowPlan(flow.pk.hex) 616 plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes( 617 "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA" 618 ) 619 request = self.request_factory.get("/") 620 621 stage_view = AuthenticatorValidateStageView( 622 FlowExecutorView(flow=flow, current_stage=stage, plan=plan), request=request 623 ) 624 request.META["SERVER_NAME"] = "localhost" 625 request.META["SERVER_PORT"] = "9000" 626 with self.assertRaises(ValidationError): 627 validate_challenge_webauthn( 628 { 629 "id": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 630 "rawId": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 631 "type": "public-key", 632 "assertionClientExtensions": "{}", 633 "response": { 634 "clientDataJSON": ( 635 "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZzk4STUxbVF2WlhvNWx4" 636 "TGZockQyemZvbGhaYkxSeUNncWtrWWFwMWp3U2FKMTNCZ3VvSldDRjlfTGczQWdPNFdo" 637 "LUJxYTU1NkpFMjBvS3NZYmw2UkEiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0Ojkw" 638 "MDAiLCJjcm9zc09yaWdpbiI6ZmFsc2UsIm90aGVyX2tleXNfY2FuX2JlX2FkZGVkX2hl" 639 "cmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2FpbnN0IGEgdGVtcGxh" 640 "dGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgifQ==" 641 ), 642 "signature": ( 643 "MEQCIFNlrHf9ablJAalXLWkrqvHB8oIu8kwvRpH3X3rbJVpI" 644 "AiAqtOK6mIZPk62kZN0OzFsHfuvu_RlOl7zlqSNzDdz_Ag==" 645 ), 646 "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABQ==", 647 "userHandle": None, 648 }, 649 }, 650 stage_view, 651 self.user, 652 )
Test validator stage
def
setUp(self) -> None:
43 def setUp(self) -> None: 44 self.user = create_test_admin_user() 45 self.request_factory = RequestFactory()
Hook method for setting up the test fixture before exercising it.
def
test_last_auth_threshold(self):
47 def test_last_auth_threshold(self): 48 """Test last_auth_threshold""" 49 ident_stage = IdentificationStage.objects.create( 50 name=generate_id(), 51 user_fields=[ 52 UserFields.USERNAME, 53 ], 54 ) 55 device: WebAuthnDevice = WebAuthnDevice.objects.create( 56 user=self.user, 57 confirmed=True, 58 ) 59 device.set_sign_count(device.sign_count + 1) 60 stage = AuthenticatorValidateStage.objects.create( 61 name=generate_id(), 62 last_auth_threshold="milliseconds=0", 63 not_configured_action=NotConfiguredAction.CONFIGURE, 64 device_classes=[DeviceClasses.WEBAUTHN], 65 ) 66 sleep(1) 67 stage.configuration_stages.set([ident_stage]) 68 flow = create_test_flow() 69 FlowStageBinding.objects.create(target=flow, stage=ident_stage, order=0) 70 FlowStageBinding.objects.create(target=flow, stage=stage, order=1) 71 72 response = self.client.post( 73 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 74 {"uid_field": self.user.username}, 75 ) 76 self.assertEqual(response.status_code, 302) 77 response = self.client.get( 78 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 79 follow=True, 80 ) 81 self.assertStageResponse( 82 response, 83 flow, 84 component="ak-stage-authenticator-validate", 85 )
Test last_auth_threshold
def
test_device_challenge_webauthn(self):
87 def test_device_challenge_webauthn(self): 88 """Test webauthn""" 89 request = self.request_factory.get("/") 90 request.user = self.user 91 92 webauthn_device = WebAuthnDevice.objects.create( 93 user=self.user, 94 public_key=bytes_to_base64url(b"qwerqwerqre"), 95 credential_id=bytes_to_base64url(b"foobarbaz"), 96 sign_count=0, 97 rp_id=generate_id(), 98 ) 99 stage = AuthenticatorValidateStage.objects.create( 100 name=generate_id(), 101 last_auth_threshold="milliseconds=0", 102 not_configured_action=NotConfiguredAction.CONFIGURE, 103 device_classes=[DeviceClasses.WEBAUTHN], 104 webauthn_user_verification=UserVerification.PREFERRED, 105 ) 106 plan = FlowPlan("") 107 stage_view = AuthenticatorValidateStageView( 108 FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request 109 ) 110 challenge = get_challenge_for_device(stage_view, stage, webauthn_device) 111 del challenge["challenge"] 112 self.assertEqual( 113 challenge, 114 { 115 "allowCredentials": [ 116 { 117 "id": "Zm9vYmFyYmF6", 118 "type": "public-key", 119 } 120 ], 121 "rpId": "testserver", 122 "timeout": 60000, 123 "userVerification": "preferred", 124 }, 125 ) 126 127 with self.assertRaises(ValidationError): 128 validate_challenge_webauthn( 129 {}, 130 StageView(FlowExecutorView(current_stage=stage, plan=plan), request=request), 131 self.user, 132 )
Test webauthn
def
test_device_challenge_webauthn_restricted(self):
134 def test_device_challenge_webauthn_restricted(self): 135 """Test webauthn (getting device challenges with a webauthn 136 device that is not allowed due to aaguid restrictions)""" 137 webauthn_mds_import.send(force=True).get_result() 138 request = self.request_factory.get("/") 139 request.user = self.user 140 141 WebAuthnDevice.objects.create( 142 user=self.user, 143 public_key=bytes_to_base64url(b"qwerqwerqre"), 144 credential_id=bytes_to_base64url(b"foobarbaz"), 145 sign_count=0, 146 rp_id=generate_id(), 147 device_type=WebAuthnDeviceType.objects.get( 148 aaguid="2fc0579f-8113-47ea-b116-bb5a8db9202a" 149 ), 150 ) 151 flow = create_test_flow() 152 stage = AuthenticatorValidateStage.objects.create( 153 name=generate_id(), 154 last_auth_threshold="milliseconds=0", 155 not_configured_action=NotConfiguredAction.DENY, 156 device_classes=[DeviceClasses.WEBAUTHN], 157 webauthn_user_verification=UserVerification.PREFERRED, 158 ) 159 stage.webauthn_allowed_device_types.set( 160 WebAuthnDeviceType.objects.filter(description="YubiKey 5 Series") 161 ) 162 session = self.client.session 163 plan = FlowPlan(flow_pk=flow.pk.hex) 164 plan.append_stage(stage) 165 plan.append_stage(UserLoginStage.objects.create(name=generate_id())) 166 plan.context[PLAN_CONTEXT_PENDING_USER] = self.user 167 session[SESSION_KEY_PLAN] = plan 168 session.save() 169 170 response = self.client.get( 171 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 172 ) 173 self.assertStageResponse( 174 response, 175 flow, 176 component="ak-stage-access-denied", 177 error_message="No (allowed) MFA authenticator configured.", 178 )
Test webauthn (getting device challenges with a webauthn device that is not allowed due to aaguid restrictions)
def
test_raw_get_challenge(self):
180 def test_raw_get_challenge(self): 181 """Test webauthn""" 182 request = self.request_factory.get("/") 183 request.user = self.user 184 185 stage = AuthenticatorValidateStage.objects.create( 186 name=generate_id(), 187 last_auth_threshold="milliseconds=0", 188 not_configured_action=NotConfiguredAction.CONFIGURE, 189 device_classes=[DeviceClasses.WEBAUTHN], 190 webauthn_user_verification=UserVerification.PREFERRED, 191 ) 192 webauthn_device = WebAuthnDevice.objects.create( 193 user=self.user, 194 public_key=( 195 "pQECAyYgASFYIGsBLkklToCQkT7qJT_bJYN1sEc1oJdbnmoOc43i0J" 196 "H6IlggLTXytuhzFVYYAK4PQNj8_coGrbbzSfUxdiPAcZTQCyU" 197 ), 198 credential_id="QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 199 sign_count=0, 200 rp_id=generate_id(), 201 ) 202 plan = FlowPlan("") 203 plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes( 204 "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA" 205 ) 206 stage_view = AuthenticatorValidateStageView( 207 FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request 208 ) 209 challenge = get_challenge_for_device(stage_view, stage, webauthn_device) 210 self.assertEqual( 211 challenge["allowCredentials"], 212 [ 213 { 214 "id": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 215 "type": "public-key", 216 } 217 ], 218 ) 219 self.assertIsNotNone(challenge["challenge"]) 220 self.assertEqual( 221 challenge["rpId"], 222 "testserver", 223 ) 224 self.assertEqual( 225 challenge["timeout"], 226 60000, 227 ) 228 self.assertEqual( 229 challenge["userVerification"], 230 "preferred", 231 )
Test webauthn
def
test_get_challenge_userless(self):
233 def test_get_challenge_userless(self): 234 """Test webauthn (userless)""" 235 request = self.request_factory.get("/") 236 stage = AuthenticatorValidateStage.objects.create( 237 name=generate_id(), webauthn_user_verification=UserVerification.PREFERRED 238 ) 239 stage.refresh_from_db() 240 WebAuthnDevice.objects.create( 241 user=self.user, 242 public_key=( 243 "pQECAyYgASFYIGsBLkklToCQkT7qJT_bJYN1sEc1oJdbnmoOc43i0J" 244 "H6IlggLTXytuhzFVYYAK4PQNj8_coGrbbzSfUxdiPAcZTQCyU" 245 ), 246 credential_id="QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 247 sign_count=0, 248 rp_id=generate_id(), 249 ) 250 plan = FlowPlan("") 251 stage_view = AuthenticatorValidateStageView( 252 FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request 253 ) 254 challenge = get_webauthn_challenge_without_user(stage_view, stage) 255 self.assertEqual(challenge["allowCredentials"], []) 256 self.assertIsNotNone(challenge["challenge"]) 257 self.assertEqual(challenge["rpId"], "testserver") 258 self.assertEqual(challenge["timeout"], 60000) 259 self.assertEqual(challenge["userVerification"], "preferred")
Test webauthn (userless)
def
test_device_challenge_webauthn_with_hints(self):
261 def test_device_challenge_webauthn_with_hints(self): 262 """Test that webauthn hints are included in authentication challenge""" 263 request = self.request_factory.get("/") 264 request.user = self.user 265 266 webauthn_device = WebAuthnDevice.objects.create( 267 user=self.user, 268 public_key=bytes_to_base64url(b"qwerqwerqre"), 269 credential_id=bytes_to_base64url(b"foobarbaz"), 270 sign_count=0, 271 rp_id=generate_id(), 272 ) 273 stage = AuthenticatorValidateStage.objects.create( 274 name=generate_id(), 275 last_auth_threshold="milliseconds=0", 276 not_configured_action=NotConfiguredAction.CONFIGURE, 277 device_classes=[DeviceClasses.WEBAUTHN], 278 webauthn_user_verification=UserVerification.PREFERRED, 279 webauthn_hints=[WebAuthnHint.CLIENT_DEVICE, WebAuthnHint.HYBRID], 280 ) 281 plan = FlowPlan("") 282 stage_view = AuthenticatorValidateStageView( 283 FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request 284 ) 285 challenge = get_challenge_for_device(stage_view, stage, webauthn_device) 286 self.assertEqual(challenge["hints"], ["client-device", "hybrid"])
Test that webauthn hints are included in authentication challenge
def
test_device_challenge_webauthn_no_hints(self):
288 def test_device_challenge_webauthn_no_hints(self): 289 """Test that hints key is absent when no hints configured""" 290 request = self.request_factory.get("/") 291 request.user = self.user 292 293 webauthn_device = WebAuthnDevice.objects.create( 294 user=self.user, 295 public_key=bytes_to_base64url(b"qwerqwerqre"), 296 credential_id=bytes_to_base64url(b"foobarbaz"), 297 sign_count=0, 298 rp_id=generate_id(), 299 ) 300 stage = AuthenticatorValidateStage.objects.create( 301 name=generate_id(), 302 last_auth_threshold="milliseconds=0", 303 not_configured_action=NotConfiguredAction.CONFIGURE, 304 device_classes=[DeviceClasses.WEBAUTHN], 305 webauthn_user_verification=UserVerification.PREFERRED, 306 ) 307 plan = FlowPlan("") 308 stage_view = AuthenticatorValidateStageView( 309 FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request 310 ) 311 challenge = get_challenge_for_device(stage_view, stage, webauthn_device) 312 self.assertNotIn("hints", challenge)
Test that hints key is absent when no hints configured
def
test_get_challenge_userless_with_hints(self):
314 def test_get_challenge_userless_with_hints(self): 315 """Test that hints are included in userless/passwordless challenge""" 316 request = self.request_factory.get("/") 317 stage = AuthenticatorValidateStage.objects.create( 318 name=generate_id(), 319 webauthn_user_verification=UserVerification.PREFERRED, 320 webauthn_hints=[WebAuthnHint.SECURITY_KEY, WebAuthnHint.CLIENT_DEVICE], 321 ) 322 plan = FlowPlan("") 323 stage_view = AuthenticatorValidateStageView( 324 FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request 325 ) 326 challenge = get_webauthn_challenge_without_user(stage_view, stage) 327 self.assertEqual(challenge["hints"], ["security-key", "client-device"])
Test that hints are included in userless/passwordless challenge
def
test_device_challenge_webauthn_hints_order_preserved(self):
329 def test_device_challenge_webauthn_hints_order_preserved(self): 330 """Test that hint order is preserved in authentication challenge""" 331 request = self.request_factory.get("/") 332 request.user = self.user 333 334 webauthn_device = WebAuthnDevice.objects.create( 335 user=self.user, 336 public_key=bytes_to_base64url(b"qwerqwerqre"), 337 credential_id=bytes_to_base64url(b"foobarbaz"), 338 sign_count=0, 339 rp_id=generate_id(), 340 ) 341 stage = AuthenticatorValidateStage.objects.create( 342 name=generate_id(), 343 last_auth_threshold="milliseconds=0", 344 not_configured_action=NotConfiguredAction.CONFIGURE, 345 device_classes=[DeviceClasses.WEBAUTHN], 346 webauthn_user_verification=UserVerification.PREFERRED, 347 webauthn_hints=[ 348 WebAuthnHint.HYBRID, 349 WebAuthnHint.SECURITY_KEY, 350 WebAuthnHint.CLIENT_DEVICE, 351 ], 352 ) 353 plan = FlowPlan("") 354 stage_view = AuthenticatorValidateStageView( 355 FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request 356 ) 357 challenge = get_challenge_for_device(stage_view, stage, webauthn_device) 358 self.assertEqual(challenge["hints"], ["hybrid", "security-key", "client-device"])
Test that hint order is preserved in authentication challenge
def
test_validate_challenge_unrestricted(self):
360 def test_validate_challenge_unrestricted(self): 361 """Test webauthn authentication (unrestricted webauthn device)""" 362 webauthn_mds_import.send(force=True).get_result() 363 device = WebAuthnDevice.objects.create( 364 user=self.user, 365 public_key=( 366 "pQECAyYgASFYIF-N4GvQJdTJMAmTOxFX9_boL00zBiSrP0DY9xvJl_FFIlggnyZloVSVofdJNTLMeMdjQHgW2Rzmd5_Xt5AWtNztcdo" 367 ), 368 credential_id="X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm", 369 sign_count=2, 370 rp_id=generate_id(), 371 device_type=WebAuthnDeviceType.objects.get( 372 aaguid="2fc0579f-8113-47ea-b116-bb5a8db9202a" 373 ), 374 ) 375 flow = create_test_flow() 376 stage = AuthenticatorValidateStage.objects.create( 377 name=generate_id(), 378 not_configured_action=NotConfiguredAction.CONFIGURE, 379 device_classes=[DeviceClasses.WEBAUTHN], 380 ) 381 session = self.client.session 382 plan = FlowPlan(flow_pk=flow.pk.hex) 383 plan.append_stage(stage) 384 plan.append_stage(UserLoginStage.objects.create(name=generate_id())) 385 plan.context[PLAN_CONTEXT_PENDING_USER] = self.user 386 plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [ 387 { 388 "device_class": device.__class__.__name__.lower().replace("device", ""), 389 "device_uid": device.pk, 390 "challenge": {}, 391 "last_used": None, 392 } 393 ] 394 plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes( 395 "aCC6ak_DP45xMH1qyxzUM5iC2xc4QthQb09v7m4qDBmY8FvWvhxFzSuFlDYQmclrh5fWS5q0TPxgJGF4vimcFQ" 396 ) 397 session[SESSION_KEY_PLAN] = plan 398 session.save() 399 400 response = self.client.post( 401 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 402 data={ 403 "webauthn": { 404 "id": "X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm", 405 "rawId": "X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm", 406 "type": "public-key", 407 "assertionClientExtensions": "{}", 408 "response": { 409 "clientDataJSON": ( 410 "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiYUNDN" 411 "mFrX0RQNDV4TUgxcXl4elVNNWlDMnhjNFF0aFFiMDl2N200cURCbV" 412 "k4RnZXdmh4RnpTdUZsRFlRbWNscmg1ZldTNXEwVFB4Z0pHRjR2aW1" 413 "jRlEiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjkwMDAiLCJj" 414 "cm9zc09yaWdpbiI6ZmFsc2V9" 415 ), 416 "signature": ( 417 "MEQCIAHQCGfE_PX1z6mBDaXUNqK_NrllhXylNOmETUD3Khv9AiBTl" 418 "rX3GDRj5OaOfTToOwUwAhtd74tu0T6DZAVHPb_hlQ==" 419 ), 420 "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABg==", 421 "userHandle": None, 422 }, 423 }, 424 }, 425 SERVER_NAME="localhost", 426 SERVER_PORT="9000", 427 ) 428 self.assertEqual(response.status_code, 302) 429 response = self.client.get( 430 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 431 ) 432 self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
Test webauthn authentication (unrestricted webauthn device)
def
test_validate_challenge_restricted(self):
434 def test_validate_challenge_restricted(self): 435 """Test webauthn authentication (restricted device type, failure)""" 436 webauthn_mds_import.send(force=True).get_result() 437 device = WebAuthnDevice.objects.create( 438 user=self.user, 439 public_key=( 440 "pQECAyYgASFYIF-N4GvQJdTJMAmTOxFX9_boL00zBiSrP0DY9xvJl_FFIlggnyZloVSVofdJNTLMeMdjQHgW2Rzmd5_Xt5AWtNztcdo" 441 ), 442 credential_id="X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm", 443 sign_count=2, 444 rp_id=generate_id(), 445 device_type=WebAuthnDeviceType.objects.get( 446 aaguid="2fc0579f-8113-47ea-b116-bb5a8db9202a" 447 ), 448 ) 449 flow = create_test_flow() 450 stage = AuthenticatorValidateStage.objects.create( 451 name=generate_id(), 452 not_configured_action=NotConfiguredAction.CONFIGURE, 453 device_classes=[DeviceClasses.WEBAUTHN], 454 ) 455 stage.webauthn_allowed_device_types.set( 456 WebAuthnDeviceType.objects.filter(description="YubiKey 5 Series") 457 ) 458 session = self.client.session 459 plan = FlowPlan(flow_pk=flow.pk.hex) 460 plan.append_stage(stage) 461 plan.append_stage(UserLoginStage.objects.create(name=generate_id())) 462 plan.context[PLAN_CONTEXT_PENDING_USER] = self.user 463 plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [ 464 { 465 "device_class": device.__class__.__name__.lower().replace("device", ""), 466 "device_uid": device.pk, 467 "challenge": {}, 468 "last_used": None, 469 } 470 ] 471 plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes( 472 "aCC6ak_DP45xMH1qyxzUM5iC2xc4QthQb09v7m4qDBmY8FvWvhxFzSuFlDYQmclrh5fWS5q0TPxgJGF4vimcFQ" 473 ) 474 session[SESSION_KEY_PLAN] = plan 475 session.save() 476 477 response = self.client.post( 478 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 479 data={ 480 "webauthn": { 481 "id": "X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm", 482 "rawId": "X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm", 483 "type": "public-key", 484 "assertionClientExtensions": "{}", 485 "response": { 486 "clientDataJSON": ( 487 "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiYUNDN" 488 "mFrX0RQNDV4TUgxcXl4elVNNWlDMnhjNFF0aFFiMDl2N200cURCbV" 489 "k4RnZXdmh4RnpTdUZsRFlRbWNscmg1ZldTNXEwVFB4Z0pHRjR2aW1" 490 "jRlEiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjkwMDAiLCJj" 491 "cm9zc09yaWdpbiI6ZmFsc2V9" 492 ), 493 "signature": ( 494 "MEQCIAHQCGfE_PX1z6mBDaXUNqK_NrllhXylNOmETUD3Khv9AiBTl" 495 "rX3GDRj5OaOfTToOwUwAhtd74tu0T6DZAVHPb_hlQ==" 496 ), 497 "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABg==", 498 "userHandle": None, 499 }, 500 } 501 }, 502 SERVER_NAME="localhost", 503 SERVER_PORT="9000", 504 ) 505 self.assertEqual(response.status_code, 200) 506 self.assertStageResponse( 507 response, 508 flow, 509 component="ak-stage-authenticator-validate", 510 response_errors={ 511 "webauthn": [ 512 { 513 "string": ( 514 "Invalid device type. Contact your authentik administrator for help." 515 ), 516 "code": "invalid", 517 } 518 ] 519 }, 520 )
Test webauthn authentication (restricted device type, failure)
def
test_validate_challenge_userless(self):
522 def test_validate_challenge_userless(self): 523 """Test webauthn""" 524 device = WebAuthnDevice.objects.create( 525 user=self.user, 526 public_key=( 527 "pQECAyYgASFYIGsBLkklToCQkT7qJT_bJYN1sEc1oJdbnmoOc43i0J" 528 "H6IlggLTXytuhzFVYYAK4PQNj8_coGrbbzSfUxdiPAcZTQCyU" 529 ), 530 credential_id="QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 531 sign_count=4, 532 rp_id=generate_id(), 533 ) 534 flow = create_test_flow() 535 stage = AuthenticatorValidateStage.objects.create( 536 name=generate_id(), 537 not_configured_action=NotConfiguredAction.CONFIGURE, 538 device_classes=[DeviceClasses.WEBAUTHN], 539 ) 540 session = self.client.session 541 plan = FlowPlan(flow_pk=flow.pk.hex) 542 plan.append_stage(stage) 543 plan.append_stage(UserLoginStage.objects.create(name=generate_id())) 544 plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [ 545 { 546 "device_class": device.__class__.__name__.lower().replace("device", ""), 547 "device_uid": device.pk, 548 "challenge": {}, 549 "last_used": None, 550 } 551 ] 552 plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes( 553 "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA" 554 ) 555 session[SESSION_KEY_PLAN] = plan 556 session.save() 557 558 response = self.client.post( 559 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 560 data={ 561 "webauthn": { 562 "id": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 563 "rawId": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 564 "type": "public-key", 565 "assertionClientExtensions": "{}", 566 "response": { 567 "clientDataJSON": ( 568 "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZzk4STUxbVF2Wlhv" 569 "NWx4TGZockQyemZvbGhaYkxSeUNncWtrWWFwMWp3U2FKMTNCZ3VvSldDRjlfTGcz" 570 "QWdPNFdoLUJxYTU1NkpFMjBvS3NZYmw2UkEiLCJvcmlnaW4iOiJodHRwOi8vbG9j" 571 "YWxob3N0OjkwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2UsIm90aGVyX2tleXNfY2Fu" 572 "X2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBh" 573 "Z2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgifQ==" 574 ), 575 "signature": ( 576 "MEQCIFNlrHf9ablJAalXLWkrqvHB8oIu8kwvRpH3X3rbJVpI" 577 "AiAqtOK6mIZPk62kZN0OzFsHfuvu_RlOl7zlqSNzDdz_Ag==" 578 ), 579 "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABQ==", 580 "userHandle": None, 581 }, 582 }, 583 }, 584 SERVER_NAME="localhost", 585 SERVER_PORT="9000", 586 ) 587 self.assertEqual(response.status_code, 302) 588 response = self.client.get( 589 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 590 ) 591 self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
Test webauthn
def
test_validate_challenge_invalid(self):
593 def test_validate_challenge_invalid(self): 594 """Test webauthn""" 595 request = self.request_factory.get("/") 596 request.user = self.user 597 598 WebAuthnDevice.objects.create( 599 user=self.user, 600 public_key=( 601 "pQECAyYgASFYIGsBLkklToCQkT7qJT_bJYN1sEc1oJdbnmoOc4" 602 "3i0JH6IlggLTXytuhzFVYYAK4PQNj8_coGrbbzSfUxdiPAcZTQCyU" 603 ), 604 credential_id="QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 605 # One more sign count than above, make it invalid 606 sign_count=5, 607 rp_id=generate_id(), 608 ) 609 flow = create_test_flow() 610 stage = AuthenticatorValidateStage.objects.create( 611 name=generate_id(), 612 not_configured_action=NotConfiguredAction.CONFIGURE, 613 device_classes=[DeviceClasses.WEBAUTHN], 614 ) 615 plan = FlowPlan(flow.pk.hex) 616 plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes( 617 "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA" 618 ) 619 request = self.request_factory.get("/") 620 621 stage_view = AuthenticatorValidateStageView( 622 FlowExecutorView(flow=flow, current_stage=stage, plan=plan), request=request 623 ) 624 request.META["SERVER_NAME"] = "localhost" 625 request.META["SERVER_PORT"] = "9000" 626 with self.assertRaises(ValidationError): 627 validate_challenge_webauthn( 628 { 629 "id": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 630 "rawId": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", 631 "type": "public-key", 632 "assertionClientExtensions": "{}", 633 "response": { 634 "clientDataJSON": ( 635 "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZzk4STUxbVF2WlhvNWx4" 636 "TGZockQyemZvbGhaYkxSeUNncWtrWWFwMWp3U2FKMTNCZ3VvSldDRjlfTGczQWdPNFdo" 637 "LUJxYTU1NkpFMjBvS3NZYmw2UkEiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0Ojkw" 638 "MDAiLCJjcm9zc09yaWdpbiI6ZmFsc2UsIm90aGVyX2tleXNfY2FuX2JlX2FkZGVkX2hl" 639 "cmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2FpbnN0IGEgdGVtcGxh" 640 "dGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgifQ==" 641 ), 642 "signature": ( 643 "MEQCIFNlrHf9ablJAalXLWkrqvHB8oIu8kwvRpH3X3rbJVpI" 644 "AiAqtOK6mIZPk62kZN0OzFsHfuvu_RlOl7zlqSNzDdz_Ag==" 645 ), 646 "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABQ==", 647 "userHandle": None, 648 }, 649 }, 650 stage_view, 651 self.user, 652 )
Test webauthn