From 4fc8e61f8c20db1819eebe89a1e55ed5aee9f817 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 1 Sep 2021 19:28:52 +0200 Subject: [PATCH] stages/authenticator_validate: show single button for multiple webauthn authenticators tested with browser + yubikey 5 closes #1096 The order of allowCredentials doesn't seem to matter, chrome seems to always choose the internal authenticator first. Signed-off-by: Jens Langhammer --- .../authenticator_validate/challenge.py | 33 ++++++++++++------- .../stages/authenticator_validate/stage.py | 8 ++--- .../AuthenticatorValidateStage.ts | 2 +- web/src/locales/en.po | 22 ++++++++++--- web/src/locales/pseudo-LOCALE.po | 20 ++++++++--- 5 files changed, 58 insertions(+), 27 deletions(-) diff --git a/authentik/stages/authenticator_validate/challenge.py b/authentik/stages/authenticator_validate/challenge.py index bb592133ca..49213e69e4 100644 --- a/authentik/stages/authenticator_validate/challenge.py +++ b/authentik/stages/authenticator_validate/challenge.py @@ -51,20 +51,29 @@ def get_webauthn_challenge(request: HttpRequest, device: WebAuthnDevice) -> dict # for the reasons outlined in the comment in webauthn_begin_activate. request.session["challenge"] = challenge.rstrip("=") - webauthn_user = WebAuthnUser( - device.user.uid, - device.user.username, - device.user.name, - device.user.avatar, - device.credential_id, - device.public_key, - device.sign_count, - device.rp_id, - ) + assertion = {} - webauthn_assertion_options = WebAuthnAssertionOptions(webauthn_user, challenge) + # We want all the user's WebAuthn devices and merge their challenges + for device in WebAuthnDevice.objects.filter(user=device.user).order_by("name"): + webauthn_user = WebAuthnUser( + device.user.uid, + device.user.username, + device.user.name, + device.user.avatar, + device.credential_id, + device.public_key, + device.sign_count, + device.rp_id, + ) + webauthn_assertion_options = WebAuthnAssertionOptions(webauthn_user, challenge) + if assertion == {}: + assertion = webauthn_assertion_options.assertion_dict + else: + assertion["allowCredentials"] += webauthn_assertion_options.assertion_dict.get( + "allowCredentials" + ) - return webauthn_assertion_options.assertion_dict + return assertion def validate_challenge_code(code: str, request: HttpRequest, user: User) -> str: diff --git a/authentik/stages/authenticator_validate/stage.py b/authentik/stages/authenticator_validate/stage.py index 775bd41fd5..e559c0c63a 100644 --- a/authentik/stages/authenticator_validate/stage.py +++ b/authentik/stages/authenticator_validate/stage.py @@ -20,8 +20,6 @@ from authentik.stages.authenticator_validate.models import AuthenticatorValidate LOGGER = get_logger() -PER_DEVICE_CLASSES = [DeviceClasses.WEBAUTHN] - class AuthenticatorValidationChallenge(WithUserInfoChallenge): """Authenticator challenge""" @@ -91,9 +89,9 @@ class AuthenticatorValidateStageView(ChallengeStageView): if device_class not in stage.device_classes: LOGGER.debug("device class not allowed", device_class=device_class) continue - # Ensure only classes in PER_DEVICE_CLASSES are returned per device - # otherwise only return a single challenge - if device_class in seen_classes and device_class not in PER_DEVICE_CLASSES: + # Ensure only one challenge per device class + # WebAuthn does another device loop to find all webuahtn devices + if device_class in seen_classes: continue if device_class not in seen_classes: seen_classes.append(device_class) diff --git a/web/src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts b/web/src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts index 76e26decf9..094a901760 100644 --- a/web/src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts +++ b/web/src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts @@ -180,7 +180,7 @@ export class AuthenticatorValidateStage ${this.selectedDeviceChallenge ? "" : html``} ${this.selectedDeviceChallenge diff --git a/web/src/locales/en.po b/web/src/locales/en.po index d3c1d4194a..de5b6cdb14 100644 --- a/web/src/locales/en.po +++ b/web/src/locales/en.po @@ -3370,6 +3370,10 @@ msgstr "Request token URL" msgid "Required" msgstr "Required" +#: src/flows/stages/prompt/PromptStage.ts +msgid "Required." +msgstr "Required." + #: src/pages/user-settings/UserSelfForm.ts #: src/pages/users/ServiceAccountForm.ts #: src/pages/users/UserForm.ts @@ -3524,13 +3528,17 @@ msgstr "Select a provider that this application should use. Alternatively, creat msgid "Select all rows" msgstr "Select all rows" +#: src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts +msgid "Select an authentication method." +msgstr "Select an authentication method." + #: src/pages/stages/invitation/InvitationListLink.ts msgid "Select an enrollment flow" msgstr "Select an enrollment flow" -#: src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts -msgid "Select an identification method." -msgstr "Select an identification method." +#: +#~ msgid "Select an identification method." +#~ msgstr "Select an identification method." #: src/pages/users/GroupSelectModal.ts msgid "Select groups to add user to" @@ -3757,9 +3765,13 @@ msgstr "Source(s)" msgid "Sources" msgstr "Sources" +#: +#~ msgid "Sources of identities, which can either be synced into authentik's database, like LDAP, or can be used by users to authenticate and enroll themselves, like OAuth and social logins" +#~ msgstr "Sources of identities, which can either be synced into authentik's database, like LDAP, or can be used by users to authenticate and enroll themselves, like OAuth and social logins" + #: src/pages/sources/SourcesListPage.ts -msgid "Sources of identities, which can either be synced into authentik's database, like LDAP, or can be used by users to authenticate and enroll themselves, like OAuth and social logins" -msgstr "Sources of identities, which can either be synced into authentik's database, like LDAP, or can be used by users to authenticate and enroll themselves, like OAuth and social logins" +msgid "Sources of identities, which can either be synced into authentik's database, or can be used by users to authenticate and enroll themselves." +msgstr "Sources of identities, which can either be synced into authentik's database, or can be used by users to authenticate and enroll themselves." #: src/pages/flows/BoundStagesList.ts #: src/pages/flows/StageBindingForm.ts diff --git a/web/src/locales/pseudo-LOCALE.po b/web/src/locales/pseudo-LOCALE.po index b7db7829d6..bbf0388ee1 100644 --- a/web/src/locales/pseudo-LOCALE.po +++ b/web/src/locales/pseudo-LOCALE.po @@ -3362,6 +3362,10 @@ msgstr "" msgid "Required" msgstr "" +#: src/flows/stages/prompt/PromptStage.ts +msgid "Required." +msgstr "" + #: src/pages/user-settings/UserSelfForm.ts #: src/pages/users/ServiceAccountForm.ts #: src/pages/users/UserForm.ts @@ -3516,13 +3520,17 @@ msgstr "" msgid "Select all rows" msgstr "" +#: src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts +msgid "Select an authentication method." +msgstr "" + #: src/pages/stages/invitation/InvitationListLink.ts msgid "Select an enrollment flow" msgstr "" -#: src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts -msgid "Select an identification method." -msgstr "" +#: +#~ msgid "Select an identification method." +#~ msgstr "" #: src/pages/users/GroupSelectModal.ts msgid "Select groups to add user to" @@ -3749,8 +3757,12 @@ msgstr "" msgid "Sources" msgstr "" +#: +#~ msgid "Sources of identities, which can either be synced into authentik's database, like LDAP, or can be used by users to authenticate and enroll themselves, like OAuth and social logins" +#~ msgstr "" + #: src/pages/sources/SourcesListPage.ts -msgid "Sources of identities, which can either be synced into authentik's database, like LDAP, or can be used by users to authenticate and enroll themselves, like OAuth and social logins" +msgid "Sources of identities, which can either be synced into authentik's database, or can be used by users to authenticate and enroll themselves." msgstr "" #: src/pages/flows/BoundStagesList.ts