diff --git a/authentik/stages/authenticator_webauthn/api.py b/authentik/stages/authenticator_webauthn/api.py index 57d04cbc8..0302988ab 100644 --- a/authentik/stages/authenticator_webauthn/api.py +++ b/authentik/stages/authenticator_webauthn/api.py @@ -18,7 +18,12 @@ class AuthenticateWebAuthnStageSerializer(StageSerializer): class Meta: model = AuthenticateWebAuthnStage - fields = StageSerializer.Meta.fields + ["configure_flow", "user_verification"] + fields = StageSerializer.Meta.fields + [ + "configure_flow", + "user_verification", + "authenticator_attachment", + "resident_key_requirement", + ] class AuthenticateWebAuthnStageViewSet(UsedByMixin, ModelViewSet): diff --git a/authentik/stages/authenticator_webauthn/migrations/0006_authenticatewebauthnstage_authenticator_attachment_and_more.py b/authentik/stages/authenticator_webauthn/migrations/0006_authenticatewebauthnstage_authenticator_attachment_and_more.py new file mode 100644 index 000000000..43d824c46 --- /dev/null +++ b/authentik/stages/authenticator_webauthn/migrations/0006_authenticatewebauthnstage_authenticator_attachment_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 4.0.1 on 2022-01-12 21:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "authentik_stages_authenticator_webauthn", + "0005_authenticatewebauthnstage_user_verification", + ), + ] + + operations = [ + migrations.AddField( + model_name="authenticatewebauthnstage", + name="authenticator_attachment", + field=models.TextField( + choices=[("platform", "Platform"), ("cross-platform", "Cross Platform")], + default=None, + null=True, + ), + ), + migrations.AddField( + model_name="authenticatewebauthnstage", + name="resident_key_requirement", + field=models.TextField( + choices=[ + ("discouraged", "Discouraged"), + ("preferred", "Preferred"), + ("required", "Required"), + ], + default="preferred", + ), + ), + ] diff --git a/authentik/stages/authenticator_webauthn/models.py b/authentik/stages/authenticator_webauthn/models.py index 6ba52e850..8dce7e04d 100644 --- a/authentik/stages/authenticator_webauthn/models.py +++ b/authentik/stages/authenticator_webauthn/models.py @@ -31,6 +31,40 @@ class UserVerification(models.TextChoices): DISCOURAGED = "discouraged" +class ResidentKeyRequirement(models.TextChoices): + """The Relying Party's preference for the authenticator to create a dedicated "client-side" + credential for it. Requiring an authenticator to store a dedicated credential should not be + done lightly due to the limited storage capacity of some types of authenticators. + + Members: + `DISCOURAGED`: The authenticator should not create a dedicated credential + `PREFERRED`: The authenticator can create and store a dedicated credential, but if it + doesn't that's alright too + `REQUIRED`: The authenticator MUST create a dedicated credential. If it cannot, the RP + is prepared for an error to occur. + + https://www.w3.org/TR/webauthn-2/#enum-residentKeyRequirement + """ + + DISCOURAGED = "discouraged" + PREFERRED = "preferred" + REQUIRED = "required" + + +class AuthenticatorAttachment(models.TextChoices): + """How an authenticator is connected to the client/browser. + + Members: + `PLATFORM`: A non-removable authenticator, like TouchID or Windows Hello + `CROSS_PLATFORM`: A "roaming" authenticator, like a YubiKey + + https://www.w3.org/TR/webauthn-2/#enumdef-authenticatorattachment + """ + + PLATFORM = "platform" + CROSS_PLATFORM = "cross-platform" + + class AuthenticateWebAuthnStage(ConfigurableStage, Stage): """WebAuthn stage""" @@ -38,6 +72,13 @@ class AuthenticateWebAuthnStage(ConfigurableStage, Stage): choices=UserVerification.choices, default=UserVerification.PREFERRED, ) + resident_key_requirement = models.TextField( + choices=ResidentKeyRequirement.choices, + default=ResidentKeyRequirement.PREFERRED, + ) + authenticator_attachment = models.TextField( + choices=AuthenticatorAttachment.choices, default=None, null=True + ) @property def serializer(self) -> BaseSerializer: diff --git a/authentik/stages/authenticator_webauthn/stage.py b/authentik/stages/authenticator_webauthn/stage.py index 071e0e52e..aa647b5b6 100644 --- a/authentik/stages/authenticator_webauthn/stage.py +++ b/authentik/stages/authenticator_webauthn/stage.py @@ -10,6 +10,7 @@ from webauthn import generate_registration_options, options_to_json, verify_regi from webauthn.helpers import bytes_to_base64url from webauthn.helpers.exceptions import InvalidRegistrationResponse from webauthn.helpers.structs import ( + AuthenticatorAttachment, AuthenticatorSelectionCriteria, PublicKeyCredentialCreationOptions, RegistrationCredential, @@ -85,6 +86,12 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView): stage: AuthenticateWebAuthnStage = self.executor.current_stage user = self.get_pending_user() + # library accepts none so we store null in the database, but if there is a value + # set, cast it to string to ensure it's not a django class + authenticator_attachment = stage.authenticator_attachment + if authenticator_attachment: + authenticator_attachment = str(authenticator_attachment) + registration_options: PublicKeyCredentialCreationOptions = generate_registration_options( rp_id=get_rp_id(self.request), rp_name=self.request.tenant.branding_title, @@ -92,8 +99,9 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView): user_name=user.username, user_display_name=user.name, authenticator_selection=AuthenticatorSelectionCriteria( - resident_key=ResidentKeyRequirement.PREFERRED, + resident_key=str(stage.resident_key_requirement), user_verification=str(stage.user_verification), + authenticator_attachment=authenticator_attachment, ), ) diff --git a/schema.yml b/schema.yml index 993bc5806..de04ff859 100644 --- a/schema.yml +++ b/schema.yml @@ -15270,6 +15270,14 @@ paths: operationId: stages_authenticator_webauthn_list description: AuthenticateWebAuthnStage Viewset parameters: + - in: query + name: authenticator_attachment + schema: + type: string + nullable: true + enum: + - cross-platform + - platform - in: query name: configure_flow schema: @@ -15297,6 +15305,14 @@ paths: description: Number of results to return per page. schema: type: integer + - in: query + name: resident_key_requirement + schema: + type: string + enum: + - discouraged + - preferred + - required - name: search required: false in: query @@ -19174,6 +19190,12 @@ components: If empty, user will not be able to configure this stage. user_verification: $ref: '#/components/schemas/UserVerificationEnum' + authenticator_attachment: + allOf: + - $ref: '#/components/schemas/AuthenticatorAttachmentEnum' + nullable: true + resident_key_requirement: + $ref: '#/components/schemas/ResidentKeyRequirementEnum' required: - component - meta_model_name @@ -19200,6 +19222,12 @@ components: If empty, user will not be able to configure this stage. user_verification: $ref: '#/components/schemas/UserVerificationEnum' + authenticator_attachment: + allOf: + - $ref: '#/components/schemas/AuthenticatorAttachmentEnum' + nullable: true + resident_key_requirement: + $ref: '#/components/schemas/ResidentKeyRequirementEnum' required: - name AuthenticatedSession: @@ -19288,6 +19316,11 @@ components: - last_used - user - user_agent + AuthenticatorAttachmentEnum: + enum: + - platform + - cross-platform + type: string AuthenticatorDuoChallenge: type: object description: Duo Challenge @@ -26519,6 +26552,12 @@ components: If empty, user will not be able to configure this stage. user_verification: $ref: '#/components/schemas/UserVerificationEnum' + authenticator_attachment: + allOf: + - $ref: '#/components/schemas/AuthenticatorAttachmentEnum' + nullable: true + resident_key_requirement: + $ref: '#/components/schemas/ResidentKeyRequirementEnum' PatchedAuthenticatorDuoStageRequest: type: object description: AuthenticatorDuoStage Serializer @@ -29376,6 +29415,12 @@ components: type: integer maximum: 2147483647 minimum: -2147483648 + ResidentKeyRequirementEnum: + enum: + - discouraged + - preferred + - required + type: string SAMLMetadata: type: object description: SAML Provider Metadata serializer diff --git a/web/src/locales/en.po b/web/src/locales/en.po index fb1e348f1..a6113f24a 100644 --- a/web/src/locales/en.po +++ b/web/src/locales/en.po @@ -62,6 +62,10 @@ msgstr "6 digits, widely compatible" msgid "8 digits, not compatible with apps like Google Authenticator" msgstr "8 digits, not compatible with apps like Google Authenticator" +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "A \"roaming\" authenticator, like a YubiKey" +msgstr "A \"roaming\" authenticator, like a YubiKey" + #: src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts msgid "A code has been sent to you via SMS." msgstr "A code has been sent to you via SMS." @@ -70,6 +74,10 @@ msgstr "A code has been sent to you via SMS." msgid "A newer version of the frontend is available." msgstr "A newer version of the frontend is available." +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "A non-removable authenticator, like TouchID or Windows Hello" +msgstr "A non-removable authenticator, like TouchID or Windows Hello" + #: src/pages/policies/dummy/DummyPolicyForm.ts msgid "A policy used for testing. Always returns the same result as specified below after waiting a random duration." msgstr "A policy used for testing. Always returns the same result as specified below after waiting a random duration." @@ -488,6 +496,10 @@ msgstr "Authentication flow" msgid "Authenticator" msgstr "Authenticator" +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "Authenticator Attachment" +msgstr "Authenticator Attachment" + #: src/pages/flows/utils.ts msgid "Authorization" msgstr "Authorization" @@ -3147,6 +3159,10 @@ msgstr "No objects found." msgid "No policies are currently bound to this object." msgstr "No policies are currently bound to this object." +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "No preference is sent" +msgstr "No preference is sent" + #: src/pages/users/UserListPage.ts msgid "No recovery flow is configured." msgstr "No recovery flow is configured." @@ -3940,6 +3956,10 @@ msgstr "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only." msgid "Reset Password" msgstr "Reset Password" +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "Resident key requirement" +msgstr "Resident key requirement" + #: src/interfaces/AdminInterface.ts #~ msgid "Resources" #~ msgstr "Resources" @@ -5045,6 +5065,18 @@ msgstr "The Host IP of the docker host" msgid "The URL \"{0}\" was not found." msgstr "The URL \"{0}\" was not found." +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "The authenticator MUST create a dedicated credential. If it cannot, the RP is prepared for an error to occur" +msgstr "The authenticator MUST create a dedicated credential. If it cannot, the RP is prepared for an error to occur" + +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "The authenticator can create and store a dedicated credential, but if it doesn't that's alright too" +msgstr "The authenticator can create and store a dedicated credential, but if it doesn't that's alright too" + +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "The authenticator should not create a dedicated credential" +msgstr "The authenticator should not create a dedicated credential" + #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts msgid "The external URL you'll access the application at. Include any non-standard port." diff --git a/web/src/locales/fr_FR.po b/web/src/locales/fr_FR.po index 2715eead5..53605e0e5 100644 --- a/web/src/locales/fr_FR.po +++ b/web/src/locales/fr_FR.po @@ -68,6 +68,10 @@ msgstr "6 chiffres, compatibilité large" msgid "8 digits, not compatible with apps like Google Authenticator" msgstr "8 chiffres, incompatible avec certaines applications telles que Google Authenticator" +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "A \"roaming\" authenticator, like a YubiKey" +msgstr "" + #: src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts msgid "A code has been sent to you via SMS." msgstr "" @@ -76,6 +80,10 @@ msgstr "" msgid "A newer version of the frontend is available." msgstr "Une nouvelle version de l'interface est disponible." +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "A non-removable authenticator, like TouchID or Windows Hello" +msgstr "" + #: src/pages/policies/dummy/DummyPolicyForm.ts msgid "A policy used for testing. Always returns the same result as specified below after waiting a random duration." msgstr "Une politique utilisée pour les tests. Retourne toujours la même valeur telle qu'indiquée ci-dessous après une attente aléatoire." @@ -493,6 +501,10 @@ msgstr "Flux d'authentification" msgid "Authenticator" msgstr "Authentificateur" +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "Authenticator Attachment" +msgstr "" + #: src/pages/flows/utils.ts msgid "Authorization" msgstr "Authorisation" @@ -3126,6 +3138,10 @@ msgstr "Aucun objet trouvé." msgid "No policies are currently bound to this object." msgstr "Aucune politique n'est actuellement lié à cet objet." +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "No preference is sent" +msgstr "" + #: src/pages/users/UserListPage.ts msgid "No recovery flow is configured." msgstr "Aucun flux de récupération n'est configuré." @@ -3912,6 +3928,10 @@ msgstr "Obligatoire. 150 caractères ou moins. Lettres, chiffres et @/./+/-/_ un msgid "Reset Password" msgstr "Réinitialiser le mot de passe" +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "Resident key requirement" +msgstr "" + #: src/interfaces/AdminInterface.ts #~ msgid "Resources" #~ msgstr "Ressources" @@ -5000,6 +5020,18 @@ msgstr "" msgid "The URL \"{0}\" was not found." msgstr "L'URL \"{0}\" est introuvable." +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "The authenticator MUST create a dedicated credential. If it cannot, the RP is prepared for an error to occur" +msgstr "" + +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "The authenticator can create and store a dedicated credential, but if it doesn't that's alright too" +msgstr "" + +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "The authenticator should not create a dedicated credential" +msgstr "" + #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts msgid "The external URL you'll access the application at. Include any non-standard port." diff --git a/web/src/locales/pseudo-LOCALE.po b/web/src/locales/pseudo-LOCALE.po index 983e6722b..7006c7cb6 100644 --- a/web/src/locales/pseudo-LOCALE.po +++ b/web/src/locales/pseudo-LOCALE.po @@ -62,6 +62,10 @@ msgstr "" msgid "8 digits, not compatible with apps like Google Authenticator" msgstr "" +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "A \"roaming\" authenticator, like a YubiKey" +msgstr "" + #: src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts msgid "A code has been sent to you via SMS." msgstr "" @@ -70,6 +74,10 @@ msgstr "" msgid "A newer version of the frontend is available." msgstr "" +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "A non-removable authenticator, like TouchID or Windows Hello" +msgstr "" + #: src/pages/policies/dummy/DummyPolicyForm.ts msgid "A policy used for testing. Always returns the same result as specified below after waiting a random duration." msgstr "" @@ -484,6 +492,10 @@ msgstr "" msgid "Authenticator" msgstr "" +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "Authenticator Attachment" +msgstr "" + #: src/pages/flows/utils.ts msgid "Authorization" msgstr "" @@ -3137,6 +3149,10 @@ msgstr "" msgid "No policies are currently bound to this object." msgstr "" +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "No preference is sent" +msgstr "" + #: src/pages/users/UserListPage.ts msgid "No recovery flow is configured." msgstr "" @@ -3930,6 +3946,10 @@ msgstr "" msgid "Reset Password" msgstr "" +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "Resident key requirement" +msgstr "" + #: src/interfaces/AdminInterface.ts #~ msgid "Resources" #~ msgstr "" @@ -5035,6 +5055,18 @@ msgstr "" msgid "The URL \"{0}\" was not found." msgstr "" +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "The authenticator MUST create a dedicated credential. If it cannot, the RP is prepared for an error to occur" +msgstr "" + +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "The authenticator can create and store a dedicated credential, but if it doesn't that's alright too" +msgstr "" + +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "The authenticator should not create a dedicated credential" +msgstr "" + #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts msgid "The external URL you'll access the application at. Include any non-standard port." diff --git a/web/src/locales/tr.po b/web/src/locales/tr.po index 7f9ddf2a1..ee6340237 100644 --- a/web/src/locales/tr.po +++ b/web/src/locales/tr.po @@ -65,6 +65,10 @@ msgstr "6 basamaklı, yaygın olarak uyumlu" msgid "8 digits, not compatible with apps like Google Authenticator" msgstr "Google Authenticator gibi uygulamalarla uyumlu olmayan 8 haneli" +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "A \"roaming\" authenticator, like a YubiKey" +msgstr "" + #: src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts msgid "A code has been sent to you via SMS." msgstr "SMS ile size bir kod gönderildi." @@ -73,6 +77,10 @@ msgstr "SMS ile size bir kod gönderildi." msgid "A newer version of the frontend is available." msgstr "Ön yüzün daha yeni bir sürümü mevcuttur." +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "A non-removable authenticator, like TouchID or Windows Hello" +msgstr "" + #: src/pages/policies/dummy/DummyPolicyForm.ts msgid "A policy used for testing. Always returns the same result as specified below after waiting a random duration." msgstr "Test için kullanılan bir ilke. Her zaman rastgele bir süre bekledikten sonra aşağıda belirtilen sonucu döndürür." @@ -487,6 +495,10 @@ msgstr "Kimlik doğrulama akışı" msgid "Authenticator" msgstr "Kimlik Doğrulayıcı" +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "Authenticator Attachment" +msgstr "" + #: src/pages/flows/utils.ts msgid "Authorization" msgstr "Yetkilendirme" @@ -3096,6 +3108,10 @@ msgstr "Nesne bulunamadı." msgid "No policies are currently bound to this object." msgstr "Hiçbir ilke şu anda bu nesneye bağlı değildir." +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "No preference is sent" +msgstr "" + #: src/pages/users/UserListPage.ts msgid "No recovery flow is configured." msgstr "Kurtarma akışı yapılandırılmamış." @@ -3867,6 +3883,10 @@ msgstr "Gerekli. 150 karakter veya daha az. Harfler, rakamlar ve yalnızca @/./+ msgid "Reset Password" msgstr "Parolayı Sıfırla" +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "Resident key requirement" +msgstr "" + #~ msgid "Resources" #~ msgstr "Kaynaklar" @@ -4938,6 +4958,18 @@ msgstr "Docker ana bilgisayarının Ana Bilgisayar IP'si" msgid "The URL \"{0}\" was not found." msgstr "“{0}” URL'si bulunamadı." +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "The authenticator MUST create a dedicated credential. If it cannot, the RP is prepared for an error to occur" +msgstr "" + +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "The authenticator can create and store a dedicated credential, but if it doesn't that's alright too" +msgstr "" + +#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +msgid "The authenticator should not create a dedicated credential" +msgstr "" + #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts msgid "The external URL you'll access the application at. Include any non-standard port." diff --git a/web/src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts b/web/src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts index 30e0c227a..a6df2d58d 100644 --- a/web/src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts +++ b/web/src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts @@ -9,8 +9,10 @@ import { until } from "lit/directives/until.js"; import { AuthenticateWebAuthnStage, + AuthenticatorAttachmentEnum, FlowsApi, FlowsInstancesListDesignationEnum, + ResidentKeyRequirementEnum, StagesApi, } from "@goauthentik/api"; @@ -35,6 +37,9 @@ export class AuthenticateWebAuthnStageForm extends ModelForm => { + if (data.authenticatorAttachment?.toString() === "") { + data.authenticatorAttachment = null; + } if (this.instance) { return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorWebauthnUpdate({ stageUuid: this.instance.pk || "", @@ -68,7 +73,7 @@ export class AuthenticateWebAuthnStageForm extends ModelForm -