From 9b64db70769ff7fcc0fcd24e6bb1a6d49948cab4 Mon Sep 17 00:00:00 2001 From: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com> Date: Wed, 23 Oct 2024 14:38:56 -0700 Subject: [PATCH 01/43] web/admin: Add InvalidationFlow to Radius Provider dialogues (#11786) web: Add InvalidationFlow to Radius Provider dialogues ## What - Bugfix: adds the InvalidationFlow to the Radius Provider dialogues - Repairs: `{"invalidation_flow":["This field is required."]}` message, which was *not* propagated to the Notification. - Nitpick: Pretties `?foo=${true}` expressions: `s/\?([^=]+)=\$\{true\}/\1/` ## Note Yes, I know I'm going to have to do more magic when we harmonize the forms, and no, I didn't add the Property Mappings to the wizard, and yes, I know I'm going to have pain with the *new* version of the wizard. But this is a serious bug; you can't make Radius servers with *either* of the current dialogues at the moment. --- ...ication-wizard-authentication-by-radius.ts | 20 +++++++++++ .../providers/radius/RadiusProviderForm.ts | 36 +++++++++++++++---- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/web/src/admin/applications/wizard/methods/radius/ak-application-wizard-authentication-by-radius.ts b/web/src/admin/applications/wizard/methods/radius/ak-application-wizard-authentication-by-radius.ts index c7d6bd0f0e..fca1666d81 100644 --- a/web/src/admin/applications/wizard/methods/radius/ak-application-wizard-authentication-by-radius.ts +++ b/web/src/admin/applications/wizard/methods/radius/ak-application-wizard-authentication-by-radius.ts @@ -75,6 +75,26 @@ export class ApplicationWizardAuthenticationByRadius extends WithBrandConfig(Bas > + + ${msg("Advanced flow settings")} +
+ + +

+ ${msg("Flow used when logging out of this provider.")} +

+
+
`; } } diff --git a/web/src/admin/providers/radius/RadiusProviderForm.ts b/web/src/admin/providers/radius/RadiusProviderForm.ts index 9beb0e115f..d3280d8013 100644 --- a/web/src/admin/providers/radius/RadiusProviderForm.ts +++ b/web/src/admin/providers/radius/RadiusProviderForm.ts @@ -1,3 +1,5 @@ +import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search"; +import "@goauthentik/admin/common/ak-flow-search/ak-flow-search"; import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils"; @@ -70,7 +72,8 @@ export class RadiusProviderFormPage extends WithBrandConfig(BaseProviderForm + return html` + - + ${msg("Protocol settings")}
-
`; +
+ + ${msg("Advanced flow settings")} +
+ + +

+ ${msg("Flow used when logging out of this provider.")} +

+
+
+ `; } } From 76390dc47b004237df27d36acfd61c55af1fb1aa Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 01:19:54 +0200 Subject: [PATCH 02/43] translate: Updates for file locale/en/LC_MESSAGES/django.po in fr (#11784) Translate locale/en/LC_MESSAGES/django.po in fr 100% translated source file: 'locale/en/LC_MESSAGES/django.po' on 'fr'. Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com> --- locale/fr/LC_MESSAGES/django.po | 149 +++++++++++++++++++++++++++++++- 1 file changed, 148 insertions(+), 1 deletion(-) diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 243070be90..7662522a26 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -19,7 +19,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-10-18 00:09+0000\n" +"POT-Creation-Date: 2024-10-23 16:39+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n" "Last-Translator: Marc Schmitt, 2024\n" "Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n" @@ -587,6 +587,30 @@ msgstr "Limite maximum de connection atteinte." msgid "(You are already connected in another tab/window)" msgstr "(Vous êtes déjà connecté dans un autre onglet/une autre fenêtre)" +#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py +msgid "Endpoint Authenticator Google Device Trust Connector Stage" +msgstr "" +"Étape d'authentificateur d'appareil du connecteur de confiance des appareils" +" Google" + +#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py +msgid "Endpoint Authenticator Google Device Trust Connector Stages" +msgstr "" +"Étapes d'authentificateur d'appareil du connecteur de confiance des " +"appareils Google" + +#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py +msgid "Endpoint Device" +msgstr "Appareil point de terminaison" + +#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py +msgid "Endpoint Devices" +msgstr "Appareils point de terminaison" + +#: authentik/enterprise/stages/authenticator_endpoint_gdtc/stage.py +msgid "Verifying your browser..." +msgstr "Vérification de votre navigateur..." + #: authentik/enterprise/stages/source/models.py msgid "" "Amount of time a user can take to return from the source to continue the " @@ -2029,6 +2053,125 @@ msgstr "" msgid "Used recovery-link to authenticate." msgstr "Utiliser un lien de récupération pour se connecter." +#: authentik/sources/kerberos/models.py +msgid "Kerberos realm" +msgstr "Realm Kerberos" + +#: authentik/sources/kerberos/models.py +msgid "Custom krb5.conf to use. Uses the system one by default" +msgstr "" +"krb5.conf personnalisé à utiliser. Utilise celui du système par défault" + +#: authentik/sources/kerberos/models.py +msgid "Sync users from Kerberos into authentik" +msgstr "Synchroniser les utilisateurs Kerberos dans authentik" + +#: authentik/sources/kerberos/models.py +msgid "When a user changes their password, sync it back to Kerberos" +msgstr "" +"Lorsqu'un utilisateur change son mot de passe, le synchroniser à nouveau " +"vers Kerberos." + +#: authentik/sources/kerberos/models.py +msgid "Principal to authenticate to kadmin for sync." +msgstr "Principal pour s'authentifier à kadmin pour la synchronisation." + +#: authentik/sources/kerberos/models.py +msgid "Password to authenticate to kadmin for sync" +msgstr "Mot de passe pour s'authentifier à kadmin pour la synchronisation." + +#: authentik/sources/kerberos/models.py +msgid "" +"Keytab to authenticate to kadmin for sync. Must be base64-encoded or in the " +"form TYPE:residual" +msgstr "" +"Keytab pour s'authentifier à kadmin pour la synchronisation. Doit être " +"encodé en base64 ou de la forme TYPE:residual" + +#: authentik/sources/kerberos/models.py +msgid "" +"Credentials cache to authenticate to kadmin for sync. Must be in the form " +"TYPE:residual" +msgstr "" +"Credentials cache pour s'authentifier à kadmin pour la synchronisation. Doit" +" être de la forme TYPE:residual" + +#: authentik/sources/kerberos/models.py +msgid "" +"Force the use of a specific server name for SPNEGO. Must be in the form " +"HTTP@hostname" +msgstr "" +"Force l'utilisation d'un nom de serveur spécifique pour SPNEGO. Doit être de" +" la forme HTTP@hostname" + +#: authentik/sources/kerberos/models.py +msgid "SPNEGO keytab base64-encoded or path to keytab in the form FILE:path" +msgstr "" +"Keytab SPNEGO encodée en base64 ou chemin vers la keytab de la forme " +"FILE:path" + +#: authentik/sources/kerberos/models.py +msgid "Credential cache to use for SPNEGO in form type:residual" +msgstr "Credentials cache pour SPNEGO de la forme TYPE:residual" + +#: authentik/sources/kerberos/models.py +msgid "" +"If enabled, the authentik-stored password will be updated upon login with " +"the Kerberos password backend" +msgstr "" +"Si activé, le mot de passe stocké par authentik sera mis à jour à la " +"connexion avec le backend de mot de passe Kerberos" + +#: authentik/sources/kerberos/models.py +msgid "Kerberos Source" +msgstr "Source Kerberos" + +#: authentik/sources/kerberos/models.py +msgid "Kerberos Sources" +msgstr "Sources Kerberos" + +#: authentik/sources/kerberos/models.py +msgid "Kerberos Source Property Mapping" +msgstr "Mappage de propriété source Kerberos" + +#: authentik/sources/kerberos/models.py +msgid "Kerberos Source Property Mappings" +msgstr "Mappages de propriété source Kerberos" + +#: authentik/sources/kerberos/models.py +msgid "User Kerberos Source Connection" +msgstr "Connexion de l'utilisateur à la source Kerberos" + +#: authentik/sources/kerberos/models.py +msgid "User Kerberos Source Connections" +msgstr "Connexions de l'utilisateur à la source Kerberos" + +#: authentik/sources/kerberos/models.py +msgid "Group Kerberos Source Connection" +msgstr "Connexion du groupe à la source Kerberos" + +#: authentik/sources/kerberos/models.py +msgid "Group Kerberos Source Connections" +msgstr "Connexions du groupe à la source Kerberos" + +#: authentik/sources/kerberos/views.py +msgid "SPNEGO authentication required" +msgstr "Authentification SPNEGO requise" + +#: authentik/sources/kerberos/views.py +msgid "" +"\n" +" Make sure you have valid tickets (obtainable via kinit)\n" +" and configured the browser correctly.\n" +" Please contact your administrator.\n" +" " +msgstr "" +"\n" +" Vérifiez que vous avez des tickets valides (qu'on peut obtenir via kinit)\n" +" et que le navigateur est configuré correctement.\n" +" Veuillez contacter votre administrateur.\n" +" " + #: authentik/sources/ldap/api.py msgid "Only a single LDAP Source with password synchronization is allowed" msgstr "" @@ -3121,6 +3264,10 @@ msgstr "Base de données utilisateurs + mots de passes applicatifs" msgid "User database + LDAP password" msgstr "Base de données utilisateurs + mot de passe LDAP" +#: authentik/stages/password/models.py +msgid "User database + Kerberos password" +msgstr "Base de données utilisateurs + mot de passe Kerberos" + #: authentik/stages/password/models.py msgid "Selection of backends to test the password against." msgstr "Sélection de backends pour tester le mot de passe." From dc670da27f7c61d95f73fe5336584f54176103fe Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 01:20:21 +0200 Subject: [PATCH 03/43] translate: Updates for file web/xliff/en.xlf in fr (#11785) Translate web/xliff/en.xlf in fr 100% translated source file: 'web/xliff/en.xlf' on 'fr'. Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com> --- web/xliff/fr.xlf | 87 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 58 insertions(+), 29 deletions(-) diff --git a/web/xliff/fr.xlf b/web/xliff/fr.xlf index 92015eaf59..48afe0c4d9 100644 --- a/web/xliff/fr.xlf +++ b/web/xliff/fr.xlf @@ -1,4 +1,4 @@ - + @@ -596,9 +596,9 @@ - The URL "" was not found. - L'URL " - " n'a pas été trouvée. + The URL "" was not found. + L'URL " + " n'a pas été trouvée. @@ -1030,8 +1030,8 @@ - To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have. - Pour permettre n'importe quelle URI de redirection, définissez cette valeur sur ".*". Soyez conscient des possibles implications de sécurité que cela peut avoir. + To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have. + Pour permettre n'importe quelle URI de redirection, définissez cette valeur sur ".*". Soyez conscient des possibles implications de sécurité que cela peut avoir. @@ -1583,7 +1583,7 @@ Token to authenticate with. Currently only bearer authentication is supported. - Jeton d'authentification à utiliser. Actuellement, seule l'authentification "bearer authentication" est prise en charge. + Jeton d'authentification à utiliser. Actuellement, seule l'authentification "bearer authentication" est prise en charge. @@ -1751,8 +1751,8 @@ - Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test". - Entrez une URL complète, un chemin relatif ou utilisez 'fa://fa-test' pour utiliser l'icône Font Awesome "fa-test". + Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test". + Entrez une URL complète, un chemin relatif ou utilisez 'fa://fa-test' pour utiliser l'icône Font Awesome "fa-test". @@ -2830,7 +2830,7 @@ doesn't pass when either or both of the selected options are equal or above the To use SSL instead, use 'ldaps://' and disable this option. - Pour utiliser SSL à la base, utilisez "ldaps://" et désactviez cette option. + Pour utiliser SSL à la base, utilisez "ldaps://" et désactviez cette option. @@ -2914,8 +2914,8 @@ doesn't pass when either or both of the selected options are equal or above the - Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...' - Champ qui contient les membres d'un groupe. Si vous utilisez le champ "memberUid", la valeur est censée contenir un nom distinctif relatif, par exemple 'memberUid=un-utilisateur' au lieu de 'memberUid=cn=un-utilisateur,ou=groups,...' + Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...' + Champ qui contient les membres d'un groupe. Si vous utilisez le champ "memberUid", la valeur est censée contenir un nom distinctif relatif, par exemple 'memberUid=un-utilisateur' au lieu de 'memberUid=cn=un-utilisateur,ou=groups,...' @@ -3210,7 +3210,7 @@ doesn't pass when either or both of the selected options are equal or above the Time offset when temporary users should be deleted. This only applies if your IDP uses the NameID Format 'transient', and the user doesn't log out manually. - Moment où les utilisateurs temporaires doivent être supprimés. Cela ne s'applique que si votre IDP utilise le format NameID "transient" et que l'utilisateur ne se déconnecte pas manuellement. + Moment où les utilisateurs temporaires doivent être supprimés. Cela ne s'applique que si votre IDP utilise le format NameID "transient" et que l'utilisateur ne se déconnecte pas manuellement. @@ -3352,7 +3352,7 @@ doesn't pass when either or both of the selected options are equal or above the Optionally set the 'FriendlyName' value of the Assertion attribute. - Indiquer la valeur "FriendlyName" de l'attribut d'assertion (optionnel) + Indiquer la valeur "FriendlyName" de l'attribut d'assertion (optionnel) @@ -3661,8 +3661,8 @@ doesn't pass when either or both of the selected options are equal or above the - When using an external logging solution for archiving, this can be set to "minutes=5". - En cas d'utilisation d'une solution de journalisation externe pour l'archivage, cette valeur peut être fixée à "minutes=5". + When using an external logging solution for archiving, this can be set to "minutes=5". + En cas d'utilisation d'une solution de journalisation externe pour l'archivage, cette valeur peut être fixée à "minutes=5". @@ -3838,10 +3838,10 @@ doesn't pass when either or both of the selected options are equal or above the - Are you sure you want to update ""? + Are you sure you want to update ""? Êtes-vous sûr de vouloir mettre à jour - " - "? + " + "? @@ -4917,8 +4917,8 @@ doesn't pass when either or both of the selected options are equal or above the - A "roaming" authenticator, like a YubiKey - Un authentificateur "itinérant", comme une YubiKey + A "roaming" authenticator, like a YubiKey + Un authentificateur "itinérant", comme une YubiKey @@ -5243,7 +5243,7 @@ doesn't pass when either or both of the selected options are equal or above the Show arbitrary input fields to the user, for example during enrollment. Data is saved in the flow context under the 'prompt_data' variable. - Afficher des champs de saisie arbitraires à l'utilisateur, par exemple pendant l'inscription. Les données sont enregistrées dans le contexte du flux sous la variable "prompt_data". + Afficher des champs de saisie arbitraires à l'utilisateur, par exemple pendant l'inscription. Les données sont enregistrées dans le contexte du flux sous la variable "prompt_data". @@ -5296,8 +5296,8 @@ doesn't pass when either or both of the selected options are equal or above the - If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here. - Si défini à une durée supérieure à 0, l'utilisateur aura la possibilité de choisir de "rester connecté", ce qui prolongera sa session jusqu'à la durée spécifiée ici. + If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here. + Si défini à une durée supérieure à 0, l'utilisateur aura la possibilité de choisir de "rester connecté", ce qui prolongera sa session jusqu'à la durée spécifiée ici. @@ -6065,7 +6065,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti Can be in the format of 'unix://' when connecting to a local docker daemon, using 'ssh://' to connect via SSH, or 'https://:2376' when connecting to a remote system. - Peut être au format "unix://" pour une connexion à un service docker local, "ssh://" pour une connexion via SSH, ou "https://:2376" pour une connexion à un système distant. + Peut être au format "unix://" pour une connexion à un service docker local, "ssh://" pour une connexion via SSH, ou "https://:2376" pour une connexion à un système distant. @@ -7322,7 +7322,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a managed outpost, this is done for you). - Utilisez ce fournisseur avec l'option "auth_request" de Nginx ou "forwardAuth" de Traefik. Chaque application/domaine a besoin de son propre fournisseur. De plus, sur chaque domaine, "/outpost.goauthentik.io" doit être routé vers le poste avancé (lorsque vous utilisez un poste avancé géré, cela est fait pour vous). + Utilisez ce fournisseur avec l'option "auth_request" de Nginx ou "forwardAuth" de Traefik. Chaque application/domaine a besoin de son propre fournisseur. De plus, sur chaque domaine, "/outpost.goauthentik.io" doit être routé vers le poste avancé (lorsque vous utilisez un poste avancé géré, cela est fait pour vous). Default relay state @@ -7720,7 +7720,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti Utilisateur créé et ajouté au groupe avec succès - This user will be added to the group "". + This user will be added to the group "". Cet utilisateur sera ajouté au groupe &quot;&quot;. @@ -9078,7 +9078,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti Synchroniser le groupe - ("", of type ) + ("", of type ) (&quot;&quot;, de type ) @@ -9131,91 +9131,120 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti Device type cannot be deleted + Le type d'appareil ne peut pas être supprimé Stage used to verify users' browsers using Google Chrome Device Trust. This stage can be used in authentication/authorization flows. + Étape utilisée pour vérifier le navigateur des utilisateurs avec le connecteur de confiance des appareils Google Chrome Enterprise. Cette étape peut être utilisée dans les flux d'authentification et d'autorisation. Google Verified Access API + API Google Verified Access Device type cannot be edited + Le type d'appareil ne peut pas être édité Advanced flow settings + Paramètres avancés des flux Enable this option to write password changes made in authentik back to Kerberos. Ignored if sync is disabled. + Activer cette option pour écrire les changements de mot de passe fait dans authentik dans Kerberos. Ignoré si la synchronisation est désactivée. Realm settings + Paramètres du realm Realm + Realm Kerberos 5 configuration + Configuration Kerberos 5 Kerberos 5 configuration. See man krb5.conf(5) for configuration format. If left empty, a default krb5.conf will be used. + Configuration Kerbers 5. Cf. man krb5.conf(5) pour le format de configuration. Si laissé vide, un krb5.conf par défaut sera utilisé. Sync connection settings + Paramètres de synchronisation Sync principal + Principal de synchronisation Principal used to authenticate to the KDC for syncing. + Principal utilisé pour s'authentifier au KDC pour synchroniser. Sync password + Mot de passe de synchronisation Password used to authenticate to the KDC for syncing. Optional if Sync keytab or Sync credentials cache is provided. + Mot de passe utilisé pour s'authentifier au KDC pour synchroniser. Optional si une keytab de synchronisation ou un credentials cache de synchronisation est fourni. Sync keytab + Keytab de synchronisation Keytab used to authenticate to the KDC for syncing. Optional if Sync password or Sync credentials cache is provided. Must be base64 encoded or in the form TYPE:residual. + Keytab utilisée pour s'authentifier au KDC pour synchroniser. Optional si un mot de passe de synchronisation ou un credentials cache de synchronisation est fourni. Doit être encodé en base64 ou de la forme TYPE:residual. Sync credentials cache + Credentials cache de synchronisation Credentials cache used to authenticate to the KDC for syncing. Optional if Sync password or Sync keytab is provided. Must be in the form TYPE:residual. + Credentials cache utilisé pour s'authentifier au KDC pour synchroniser. Optional si un mot de passe de synchronisation ou une keytab de synchronisation est fourni. Doit être de la forme TYPE:residual. SPNEGO settings + Paramètres SPNEGO SPNEGO server name + Nom de serveur SPNEGO Force the use of a specific server name for SPNEGO. Must be in the form HTTP@domain + Force l'utilisation d'un nom de serveur spécifique pour SPNEGO. Doit être de la forme HTTP@hostname SPNEGO keytab + Keytab SPNEGO Keytab used for SPNEGO. Optional if SPNEGO credentials cache is provided. Must be base64 encoded or in the form TYPE:residual. + Keytab utilisée pour SPNEGO. Optional si un credentials cache SPNEGO est fourni. Doit être encodé en base64 ou de la forme TYPE:residual. SPNEGO credentials cache + Credentials cache SPNEGO Credentials cache used for SPNEGO. Optional if SPNEGO keytab is provided. Must be in the form TYPE:residual. + Credentials cache utilisé pour SPNEGO. Optional si une keytab SPNEGO est fournie. Doit être de la forme TYPE:residual. Kerberos Attribute mapping + Mappage d'attributs Kerberos Update Kerberos Source + Mettre à jour la source Kerberos User database + Kerberos password + Base de données utilisateurs + mot de passe Kerberos - + \ No newline at end of file From 70075e6f0a47bf81f21888500527e2fdaf93fb94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simonyi=20Gerg=C5=91?= <28359278+gergosimonyi@users.noreply.github.com> Date: Thu, 24 Oct 2024 09:04:40 +0200 Subject: [PATCH 04/43] stages/authenticator_validate: autoselect last used 2fa device (#11087) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * authenticator_validate: autoselect last used device class * improve usability of `AuthenticatorValidationStage` * don't automatically offer the recovery key authenticator validation I believe this could confuse users more than help them * web: move mutator block into the `willUpdate` override Removed the section of code from the renderer that updates the state of the component; Mutating in the middle of a render is strongly discouraged. This block contains an algorithm for determining if the selectedDeviceChallenge should be set and how; since `selectedDeviceChallenge` is a state, we don't want to be changing it outside of those lifecycle methods that do not trigger a rerender. * web: move styles() to top of class, extract custom CSS to a named block. * lint: collapse multiple early returns, missing curly brace. * autoselect device only once even if the user only has 1 device * make `DeviceChallenge.last_used` nullable instead of optional * clarify button text * fix typo * add docs for automatic device selection * update docs Co-authored-by: Tana M Berry Signed-off-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com> * fix punctuation --------- Signed-off-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com> Co-authored-by: Ken Sternberg Co-authored-by: Tana M Berry --- .../authenticator_validate/challenge.py | 3 +- .../stages/authenticator_validate/stage.py | 2 + .../authenticator_validate/tests/test_sms.py | 1 + .../tests/test_stage.py | 2 + .../tests/test_webauthn.py | 3 + schema.yml | 10 ++ .../AuthenticatorValidateStage.ts | 170 ++++++++++-------- .../AuthenticatorValidateStageCode.ts | 43 +++-- .../stages/authenticator_validate/base.ts | 2 +- .../stages/authenticator_validate/index.md | 10 +- 10 files changed, 155 insertions(+), 91 deletions(-) diff --git a/authentik/stages/authenticator_validate/challenge.py b/authentik/stages/authenticator_validate/challenge.py index c11439684f..1f9a656a38 100644 --- a/authentik/stages/authenticator_validate/challenge.py +++ b/authentik/stages/authenticator_validate/challenge.py @@ -8,7 +8,7 @@ from django.http.response import Http404 from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as __ from django.utils.translation import gettext_lazy as _ -from rest_framework.fields import CharField +from rest_framework.fields import CharField, DateTimeField from rest_framework.serializers import ValidationError from structlog.stdlib import get_logger from webauthn import options_to_json @@ -45,6 +45,7 @@ class DeviceChallenge(PassiveSerializer): device_class = CharField() device_uid = CharField() challenge = JSONDictField() + last_used = DateTimeField(allow_null=True) def get_challenge_for_device( diff --git a/authentik/stages/authenticator_validate/stage.py b/authentik/stages/authenticator_validate/stage.py index 96ae7e6215..bde76d37db 100644 --- a/authentik/stages/authenticator_validate/stage.py +++ b/authentik/stages/authenticator_validate/stage.py @@ -217,6 +217,7 @@ class AuthenticatorValidateStageView(ChallengeStageView): "device_class": device_class, "device_uid": device.pk, "challenge": get_challenge_for_device(self.request, stage, device), + "last_used": device.last_used, } ) challenge.is_valid() @@ -237,6 +238,7 @@ class AuthenticatorValidateStageView(ChallengeStageView): self.request, self.executor.current_stage, ), + "last_used": None, } ) challenge.is_valid() diff --git a/authentik/stages/authenticator_validate/tests/test_sms.py b/authentik/stages/authenticator_validate/tests/test_sms.py index 5cce796207..850854a892 100644 --- a/authentik/stages/authenticator_validate/tests/test_sms.py +++ b/authentik/stages/authenticator_validate/tests/test_sms.py @@ -107,6 +107,7 @@ class AuthenticatorValidateStageSMSTests(FlowTestCase): "device_class": "sms", "device_uid": str(device.pk), "challenge": {}, + "last_used": None, }, }, ) diff --git a/authentik/stages/authenticator_validate/tests/test_stage.py b/authentik/stages/authenticator_validate/tests/test_stage.py index 98fe5d2fe4..82a2f51322 100644 --- a/authentik/stages/authenticator_validate/tests/test_stage.py +++ b/authentik/stages/authenticator_validate/tests/test_stage.py @@ -169,6 +169,7 @@ class AuthenticatorValidateStageTests(FlowTestCase): "device_class": "baz", "device_uid": "quox", "challenge": {}, + "last_used": None, } }, ) @@ -188,6 +189,7 @@ class AuthenticatorValidateStageTests(FlowTestCase): "device_class": "static", "device_uid": "1", "challenge": {}, + "last_used": None, }, }, ) diff --git a/authentik/stages/authenticator_validate/tests/test_webauthn.py b/authentik/stages/authenticator_validate/tests/test_webauthn.py index e4247b221a..05fe216081 100644 --- a/authentik/stages/authenticator_validate/tests/test_webauthn.py +++ b/authentik/stages/authenticator_validate/tests/test_webauthn.py @@ -274,6 +274,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): "device_class": device.__class__.__name__.lower().replace("device", ""), "device_uid": device.pk, "challenge": {}, + "last_used": None, } ] session[SESSION_KEY_PLAN] = plan @@ -352,6 +353,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): "device_class": device.__class__.__name__.lower().replace("device", ""), "device_uid": device.pk, "challenge": {}, + "last_used": None, } ] session[SESSION_KEY_PLAN] = plan @@ -432,6 +434,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): "device_class": device.__class__.__name__.lower().replace("device", ""), "device_uid": device.pk, "challenge": {}, + "last_used": None, } ] session[SESSION_KEY_PLAN] = plan diff --git a/schema.yml b/schema.yml index d4f3eb78ac..f0c8447abb 100644 --- a/schema.yml +++ b/schema.yml @@ -40204,10 +40204,15 @@ components: challenge: type: object additionalProperties: {} + last_used: + type: string + format: date-time + nullable: true required: - challenge - device_class - device_uid + - last_used DeviceChallengeRequest: type: object description: Single device challenge @@ -40221,10 +40226,15 @@ components: challenge: type: object additionalProperties: {} + last_used: + type: string + format: date-time + nullable: true required: - challenge - device_class - device_uid + - last_used DeviceClassesEnum: enum: - static diff --git a/web/src/flow/stages/authenticator_validate/AuthenticatorValidateStage.ts b/web/src/flow/stages/authenticator_validate/AuthenticatorValidateStage.ts index a80ec94022..3bfad7def0 100644 --- a/web/src/flow/stages/authenticator_validate/AuthenticatorValidateStage.ts +++ b/web/src/flow/stages/authenticator_validate/AuthenticatorValidateStage.ts @@ -6,7 +6,7 @@ import { BaseStage, StageHost, SubmitOptions } from "@goauthentik/flow/stages/ba import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/IdentificationStage"; import { msg } from "@lit/localize"; -import { CSSResult, TemplateResult, css, html, nothing } from "lit"; +import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit"; import { customElement, state } from "lit/decorators.js"; import PFButton from "@patternfly/patternfly/components/Button/button.css"; @@ -25,6 +25,37 @@ import { FlowsApi, } from "@goauthentik/api"; +const customCSS = css` + ul { + padding-top: 1rem; + } + ul > li:not(:last-child) { + padding-bottom: 1rem; + } + .authenticator-button { + display: flex; + align-items: center; + } + :host([theme="dark"]) .authenticator-button { + color: var(--ak-dark-foreground) !important; + } + i { + font-size: 1.5rem; + padding: 1rem 0; + width: 3rem; + } + .right { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + text-align: left; + } + .right > * { + height: 50%; + } +`; + @customElement("ak-stage-authenticator-validate") export class AuthenticatorValidateStage extends BaseStage< @@ -33,6 +64,10 @@ export class AuthenticatorValidateStage > implements StageHost { + static get styles(): CSSResult[] { + return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, customCSS]; + } + flowSlug = ""; set loading(value: boolean) { @@ -47,14 +82,18 @@ export class AuthenticatorValidateStage return this.host.brand; } + @state() + _firstInitialized: boolean = false; + @state() _selectedDeviceChallenge?: DeviceChallenge; set selectedDeviceChallenge(value: DeviceChallenge | undefined) { const previousChallenge = this._selectedDeviceChallenge; this._selectedDeviceChallenge = value; - if (!value) return; - if (value === previousChallenge) return; + if (value === undefined || value === previousChallenge) { + return; + } // We don't use this.submit here, as we don't want to advance the flow. // We just want to notify the backend which challenge has been selected. new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({ @@ -79,37 +118,39 @@ export class AuthenticatorValidateStage return this.host?.submit(payload, options) || Promise.resolve(); } - static get styles(): CSSResult[] { - return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton].concat(css` - ul { - padding-top: 1rem; - } - ul > li:not(:last-child) { - padding-bottom: 1rem; - } - .authenticator-button { - display: flex; - align-items: center; - } - :host([theme="dark"]) .authenticator-button { - color: var(--ak-dark-foreground) !important; - } - i { - font-size: 1.5rem; - padding: 1rem 0; - width: 3rem; - } - .right { - display: flex; - flex-direction: column; - justify-content: space-between; - height: 100%; - text-align: left; - } - .right > * { - height: 50%; - } - `); + willUpdate(_changed: PropertyValues) { + if (this._firstInitialized || !this.challenge) { + return; + } + + this._firstInitialized = true; + + // If user only has a single device, autoselect that device. + if (this.challenge.deviceChallenges.length === 1) { + this.selectedDeviceChallenge = this.challenge.deviceChallenges[0]; + return; + } + + // If TOTP is allowed from the backend and we have a pre-filled value + // from the password manager, autoselect TOTP. + const totpChallenge = this.challenge.deviceChallenges.find( + (challenge) => challenge.deviceClass === DeviceClassesEnum.Totp, + ); + if (PasswordManagerPrefill.totp && totpChallenge) { + console.debug( + "authentik/stages/authenticator_validate: found prefill totp code, selecting totp challenge", + ); + this.selectedDeviceChallenge = totpChallenge; + return; + } + + // If the last used device is not Static, autoselect that device. + const lastUsedChallenge = this.challenge.deviceChallenges + .filter((deviceChallenge) => deviceChallenge.lastUsed) + .sort((a, b) => b.lastUsed!.valueOf() - a.lastUsed!.valueOf())[0]; + if (lastUsedChallenge && lastUsedChallenge.deviceClass !== DeviceClassesEnum.Static) { + this.selectedDeviceChallenge = lastUsedChallenge; + } } renderDevicePickerSingle(deviceChallenge: DeviceChallenge) { @@ -228,45 +269,28 @@ export class AuthenticatorValidateStage } render(): TemplateResult { - if (!this.challenge) { - return html` `; - } - // User only has a single device class, so we don't show a picker - if (this.challenge?.deviceChallenges.length === 1) { - this.selectedDeviceChallenge = this.challenge.deviceChallenges[0]; - } - // TOTP is a bit special, assuming that TOTP is allowed from the backend, - // and we have a pre-filled value from the password manager, - // directly set the the TOTP device Challenge as active. - const totpChallenge = this.challenge.deviceChallenges.find( - (challenge) => challenge.deviceClass === DeviceClassesEnum.Totp, - ); - if (PasswordManagerPrefill.totp && totpChallenge) { - console.debug( - "authentik/stages/authenticator_validate: found prefill totp code, selecting totp challenge", - ); - this.selectedDeviceChallenge = totpChallenge; - } - return html` - ${this.selectedDeviceChallenge - ? this.renderDeviceChallenge() - : html` -
- -
`}`; + return this.challenge + ? html` + ${this.selectedDeviceChallenge + ? this.renderDeviceChallenge() + : html` +
+ +
`}` + : html` `; } } diff --git a/web/src/flow/stages/authenticator_validate/AuthenticatorValidateStageCode.ts b/web/src/flow/stages/authenticator_validate/AuthenticatorValidateStageCode.ts index 9065f2e4d8..01fcd4ef68 100644 --- a/web/src/flow/stages/authenticator_validate/AuthenticatorValidateStageCode.ts +++ b/web/src/flow/stages/authenticator_validate/AuthenticatorValidateStageCode.ts @@ -31,6 +31,34 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage< `); } + deviceMessage(): string { + switch (this.deviceChallenge?.deviceClass) { + case DeviceClassesEnum.Sms: + return msg("A code has been sent to you via SMS."); + case DeviceClassesEnum.Totp: + return msg( + "Open your two-factor authenticator app to view your authentication code.", + ); + case DeviceClassesEnum.Static: + return msg("Enter a one-time recovery code for this user."); + } + + return msg("Enter the code from your authenticator device."); + } + + deviceIcon(): string { + switch (this.deviceChallenge?.deviceClass) { + case DeviceClassesEnum.Sms: + return "fa-key"; + case DeviceClassesEnum.Totp: + return "fa-mobile-alt"; + case DeviceClassesEnum.Static: + return "fa-sticky-note"; + } + + return "fa-mobile-alt"; + } + render(): TemplateResult { if (!this.challenge) { return html` `; @@ -44,19 +72,8 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage< > ${this.renderUserInfo()}
- - ${this.deviceChallenge?.deviceClass == DeviceClassesEnum.Sms - ? html`

${msg("A code has been sent to you via SMS.")}

` - : html`

- ${msg( - "Open your two-factor authenticator app to view your authentication code.", - )} -

`} + +

${this.deviceMessage()}

`; } } diff --git a/website/docs/add-secure-apps/flows-stages/stages/authenticator_validate/index.md b/website/docs/add-secure-apps/flows-stages/stages/authenticator_validate/index.md index 907a967e19..bbe7a1e7bf 100644 --- a/website/docs/add-secure-apps/flows-stages/stages/authenticator_validate/index.md +++ b/website/docs/add-secure-apps/flows-stages/stages/authenticator_validate/index.md @@ -5,10 +5,10 @@ title: Authenticator validation stage This stage validates an already configured Authenticator Device. This device has to be configured using any of the other authenticator stages: - [Duo authenticator stage](../authenticator_duo/index.md) -- [SMS authenticator stage](../authenticator_sms/index.md). -- [Static authenticator stage](../authenticator_static/index.md). +- [SMS authenticator stage](../authenticator_sms/index.md) +- [Static authenticator stage](../authenticator_static/index.md) - [TOTP authenticator stage](../authenticator_totp/index.md) -- [WebAuth authenticator stage](../authenticator_webauthn/index.md). +- [WebAuthn authenticator stage](../authenticator_webauthn/index.md) You can select which type of device classes are allowed. @@ -75,3 +75,7 @@ Optionally restrict which WebAuthn device types can be used to authenticate. When no restriction is set, all WebAuthn devices a user has registered are allowed. These restrictions only apply to WebAuthn devices created with authentik 2024.4 or later. + +#### Automatic device selection + +If the user has more than one device, the user is prompted to select which device they want to use for validation. After the user successfully authenticates with a certain device, that device is marked as "last used". In subsequent prompts by the Authenticator validation stage, the last used device is automatically selected for the user. Should they wish to use another device, the user can return to the device selection screen. From b6bdcd6c05c5494d1d41bf3ee9efdc13a4bf39d0 Mon Sep 17 00:00:00 2001 From: "authentik-automation[bot]" <135050075+authentik-automation[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 13:03:50 +0200 Subject: [PATCH 05/43] web: bump API Client version (#11792) Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com> --- web/package-lock.json | 8 ++++---- web/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index cd7081bf4b..9256230934 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -23,7 +23,7 @@ "@floating-ui/dom": "^1.6.11", "@formatjs/intl-listformat": "^7.5.7", "@fortawesome/fontawesome-free": "^6.6.0", - "@goauthentik/api": "^2024.8.3-1729699127", + "@goauthentik/api": "^2024.8.3-1729753499", "@lit-labs/ssr": "^3.2.2", "@lit/context": "^1.1.2", "@lit/localize": "^0.12.2", @@ -1775,9 +1775,9 @@ } }, "node_modules/@goauthentik/api": { - "version": "2024.8.3-1729699127", - "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.8.3-1729699127.tgz", - "integrity": "sha512-luo0SAASR6BTTtLszDgfdwofBejv4F3hCHgPxeSoTSFgE8/A2+zJD8EtWPZaa1udDkwPa9lbIeJSSmbgFke3jA==" + "version": "2024.8.3-1729753499", + "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.8.3-1729753499.tgz", + "integrity": "sha512-D+EaKSR4Dx9Izsw00SrQ9SW1jzpuC+Pc221ULb0miBCbiHUeY9YJ8wkoRtxDKce4ka+hSLa3cmngNACo1zGyDQ==" }, "node_modules/@goauthentik/web": { "resolved": "", diff --git a/web/package.json b/web/package.json index 26a2b15b2b..8fd7a72241 100644 --- a/web/package.json +++ b/web/package.json @@ -11,7 +11,7 @@ "@floating-ui/dom": "^1.6.11", "@formatjs/intl-listformat": "^7.5.7", "@fortawesome/fontawesome-free": "^6.6.0", - "@goauthentik/api": "^2024.8.3-1729699127", + "@goauthentik/api": "^2024.8.3-1729753499", "@lit-labs/ssr": "^3.2.2", "@lit/context": "^1.1.2", "@lit/localize": "^0.12.2", From a541e4fc9d352357a7431e7053adaf3035e9c9c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 13:04:07 +0200 Subject: [PATCH 06/43] core: bump gssapi from 1.8.3 to 1.9.0 (#11791) Bumps [gssapi](https://github.com/pythongssapi/python-gssapi) from 1.8.3 to 1.9.0. - [Release notes](https://github.com/pythongssapi/python-gssapi/releases) - [Commits](https://github.com/pythongssapi/python-gssapi/compare/v1.8.3...v1.9.0) --- updated-dependencies: - dependency-name: gssapi dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 53 +++++++++++++++++++++++++++-------------------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/poetry.lock b/poetry.lock index 477f4034d8..a9ad2fe4c3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1849,35 +1849,36 @@ grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] [[package]] name = "gssapi" -version = "1.8.3" +version = "1.9.0" description = "Python GSSAPI Wrapper" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "gssapi-1.8.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4e4a83e9b275fe69b5d40be6d5479889866b80333a12c51a9243f2712d4f0554"}, - {file = "gssapi-1.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8d57d67547e18f4e44a688bfb20abbf176d1b8df547da2b31c3f2df03cfdc269"}, - {file = "gssapi-1.8.3-cp310-cp310-win32.whl", hash = "sha256:3a3f63105f39c4af29ffc8f7b6542053d87fe9d63010c689dd9a9f5571facb8e"}, - {file = "gssapi-1.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:b031c0f186ab4275186da385b2c7470dd47c9b27522cb3b753757c9ac4bebf11"}, - {file = "gssapi-1.8.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b03d6b30f1fcd66d9a688b45a97e302e4dd3f1386d5c333442731aec73cdb409"}, - {file = "gssapi-1.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ca6ceb17fc15eda2a69f2e8c6cf10d11e2edb32832255e5d4c65b21b6db4680a"}, - {file = "gssapi-1.8.3-cp311-cp311-win32.whl", hash = "sha256:edc8ef3a9e397dbe18bb6016f8e2209969677b534316d20bb139da2865a38efe"}, - {file = "gssapi-1.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:8fdb1ff130cee49bc865ec1624dee8cf445cd6c6e93b04bffef2c6f363a60cb9"}, - {file = "gssapi-1.8.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:19c373b3ba63ce19cd3163aa1495635e3d01b0de6cc4ff1126095eded1df6e01"}, - {file = "gssapi-1.8.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37f1a8046d695f2c9b8d640a6e385780d3945c0741571ed6fee6f94c31e431dc"}, - {file = "gssapi-1.8.3-cp312-cp312-win32.whl", hash = "sha256:338db18612e3e6ed64e92b6d849242a535fdc98b365f21122992fb8cae737617"}, - {file = "gssapi-1.8.3-cp312-cp312-win_amd64.whl", hash = "sha256:5731c5b40ecc3116cfe7fb7e1d1e128583ec8b3df1e68bf8cd12073160793acd"}, - {file = "gssapi-1.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e556878da197ad115a566d36e46a8082d0079731d9c24d1ace795132d725ff2a"}, - {file = "gssapi-1.8.3-cp37-cp37m-win32.whl", hash = "sha256:e2bb081f2db2111377effe7d40ba23f9a87359b9d2f4881552b731e9da88b36b"}, - {file = "gssapi-1.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4d9ed83f2064cda60aad90e6840ae282096801b2c814b8cbd390bf0df4635aab"}, - {file = "gssapi-1.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7d91fe6e2a5c89b32102ea8e374b8ae13b9031d43d7b55f3abc1f194ddce820d"}, - {file = "gssapi-1.8.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d5b28237afc0668046934792756dd4b6b7e957b0d95a608d02f296734a2819ad"}, - {file = "gssapi-1.8.3-cp38-cp38-win32.whl", hash = "sha256:791e44f7bea602b8e3da1ec56fbdb383b8ee3326fdeb736f904c2aa9af13a67d"}, - {file = "gssapi-1.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:5b4bf84d0a6d7779a4bf11dacfd3db57ae02dd53562e2aeadac4219a68eaee07"}, - {file = "gssapi-1.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e40efc88ccefefd6142f8c47b8af498731938958b808bad49990442a91f45160"}, - {file = "gssapi-1.8.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee74b9211c977b9181ff4652d886d7712c9a221560752a35393b58e5ea07887a"}, - {file = "gssapi-1.8.3-cp39-cp39-win32.whl", hash = "sha256:465c6788f2ac6ef7c738394ba8fde1ede6004e5721766f386add63891d8c90af"}, - {file = "gssapi-1.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:8fb8ee70458f47b51ed881a6881f30b187c987c02af16cc0fff0079255d4d465"}, - {file = "gssapi-1.8.3.tar.gz", hash = "sha256:aa3c8d0b1526f52559552bb2c9d2d6be013d76a8e5db00b39a1db5727e93b0b0"}, + {file = "gssapi-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:261e00ac426d840055ddb2199f4989db7e3ce70fa18b1538f53e392b4823e8f1"}, + {file = "gssapi-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:14a1ae12fdf1e4c8889206195ba1843de09fe82587fa113112887cd5894587c6"}, + {file = "gssapi-1.9.0-cp310-cp310-win32.whl", hash = "sha256:2a9c745255e3a810c3e8072e267b7b302de0705f8e9a0f2c5abc92fe12b9475e"}, + {file = "gssapi-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:dfc1b4c0bfe9f539537601c9f187edc320daf488f694e50d02d0c1eb37416962"}, + {file = "gssapi-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:67d9be5e34403e47fb5749d5a1ad4e5a85b568e6a9add1695edb4a5b879f7560"}, + {file = "gssapi-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11e9b92cef11da547fc8c210fa720528fd854038504103c1b15ae2a89dce5fcd"}, + {file = "gssapi-1.9.0-cp311-cp311-win32.whl", hash = "sha256:6c5f8a549abd187687440ec0b72e5b679d043d620442b3637d31aa2766b27cbe"}, + {file = "gssapi-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:59e1a1a9a6c5dc430dc6edfcf497f5ca00cf417015f781c9fac2e85652cd738f"}, + {file = "gssapi-1.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b66a98827fbd2864bf8993677a039d7ba4a127ca0d2d9ed73e0ef4f1baa7fd7f"}, + {file = "gssapi-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2bddd1cc0c9859c5e0fd96d4d88eb67bd498fdbba45b14cdccfe10bfd329479f"}, + {file = "gssapi-1.9.0-cp312-cp312-win32.whl", hash = "sha256:10134db0cf01bd7d162acb445762dbcc58b5c772a613e17c46cf8ad956c4dfec"}, + {file = "gssapi-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:e28c7d45da68b7e36ed3fb3326744bfe39649f16e8eecd7b003b082206039c76"}, + {file = "gssapi-1.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cea344246935b5337e6f8a69bb6cc45619ab3a8d74a29fcb0a39fd1e5843c89c"}, + {file = "gssapi-1.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a5786bd9fcf435bd0c87dc95ae99ad68cefcc2bcc80c71fef4cb0ccdfb40f1e"}, + {file = "gssapi-1.9.0-cp313-cp313-win32.whl", hash = "sha256:c99959a9dd62358e370482f1691e936cb09adf9a69e3e10d4f6a097240e9fd28"}, + {file = "gssapi-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a2e43f50450e81fe855888c53df70cdd385ada979db79463b38031710a12acd9"}, + {file = "gssapi-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c0e378d62b2fc352ca0046030cda5911d808a965200f612fdd1d74501b83e98f"}, + {file = "gssapi-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b74031c70864d04864b7406c818f41be0c1637906fb9654b06823bcc79f151dc"}, + {file = "gssapi-1.9.0-cp38-cp38-win32.whl", hash = "sha256:f2f3a46784d8127cc7ef10d3367dedcbe82899ea296710378ccc9b7cefe96f4c"}, + {file = "gssapi-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:a81f30cde21031e7b1f8194a3eea7285e39e551265e7744edafd06eadc1c95bc"}, + {file = "gssapi-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cbc93fdadd5aab9bae594538b2128044b8c5cdd1424fe015a465d8a8a587411a"}, + {file = "gssapi-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b2a3c0a9beb895942d4b8e31f515e52c17026e55aeaa81ee0df9bbfdac76098"}, + {file = "gssapi-1.9.0-cp39-cp39-win32.whl", hash = "sha256:060b58b455d29ab8aca74770e667dca746264bee660ac5b6a7a17476edc2c0b8"}, + {file = "gssapi-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:11c9fe066edb0fa0785697eb0cecf2719c7ad1d9f2bf27be57b647a617bcfaa5"}, + {file = "gssapi-1.9.0.tar.gz", hash = "sha256:f468fac8f3f5fca8f4d1ca19e3cd4d2e10bd91074e7285464b22715d13548afe"}, ] [package.dependencies] From 0a18c67b7e3701b819ef957f0d1843b396427166 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 13:04:28 +0200 Subject: [PATCH 07/43] core: bump goauthentik.io/api/v3 from 3.2024083.11 to 3.2024083.12 (#11790) Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2024083.11 to 3.2024083.12. - [Release notes](https://github.com/goauthentik/client-go/releases) - [Changelog](https://github.com/goauthentik/client-go/blob/main/model_version_history.go) - [Commits](https://github.com/goauthentik/client-go/compare/v3.2024083.11...v3.2024083.12) --- updated-dependencies: - dependency-name: goauthentik.io/api/v3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 804d9d0dc5..383fb72577 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 github.com/wwt/guac v1.3.2 - goauthentik.io/api/v3 v3.2024083.11 + goauthentik.io/api/v3 v3.2024083.12 golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab golang.org/x/oauth2 v0.23.0 golang.org/x/sync v0.8.0 diff --git a/go.sum b/go.sum index 7c008d78ad..f6637d2f99 100644 --- a/go.sum +++ b/go.sum @@ -299,8 +299,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -goauthentik.io/api/v3 v3.2024083.11 h1:kF5WAnS0dB2cq9Uldqel8e8PDepJg/824JC3YFsQVHU= -goauthentik.io/api/v3 v3.2024083.11/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= +goauthentik.io/api/v3 v3.2024083.12 h1:GbmebkAgKHBdsqGxNwsM6NkgkqxTf9/OOuIyMGjShQs= +goauthentik.io/api/v3 v3.2024083.12/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= From 238a396309a78f49ddb158cfd05f8ce8fb4f243b Mon Sep 17 00:00:00 2001 From: "authentik-automation[bot]" <135050075+authentik-automation[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 13:05:33 +0200 Subject: [PATCH 08/43] core, web: update translations (#11789) Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: rissson <18313093+rissson@users.noreply.github.com> --- web/xliff/fr.xlf | 58 ++++++++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/web/xliff/fr.xlf b/web/xliff/fr.xlf index 48afe0c4d9..638d7871e9 100644 --- a/web/xliff/fr.xlf +++ b/web/xliff/fr.xlf @@ -1,4 +1,4 @@ - + @@ -596,9 +596,9 @@ - The URL "" was not found. - L'URL " - " n'a pas été trouvée. + The URL "" was not found. + L'URL " + " n'a pas été trouvée. @@ -1030,8 +1030,8 @@ - To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have. - Pour permettre n'importe quelle URI de redirection, définissez cette valeur sur ".*". Soyez conscient des possibles implications de sécurité que cela peut avoir. + To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have. + Pour permettre n'importe quelle URI de redirection, définissez cette valeur sur ".*". Soyez conscient des possibles implications de sécurité que cela peut avoir. @@ -1583,7 +1583,7 @@ Token to authenticate with. Currently only bearer authentication is supported. - Jeton d'authentification à utiliser. Actuellement, seule l'authentification "bearer authentication" est prise en charge. + Jeton d'authentification à utiliser. Actuellement, seule l'authentification "bearer authentication" est prise en charge. @@ -1751,8 +1751,8 @@ - Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test". - Entrez une URL complète, un chemin relatif ou utilisez 'fa://fa-test' pour utiliser l'icône Font Awesome "fa-test". + Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test". + Entrez une URL complète, un chemin relatif ou utilisez 'fa://fa-test' pour utiliser l'icône Font Awesome "fa-test". @@ -2830,7 +2830,7 @@ doesn't pass when either or both of the selected options are equal or above the To use SSL instead, use 'ldaps://' and disable this option. - Pour utiliser SSL à la base, utilisez "ldaps://" et désactviez cette option. + Pour utiliser SSL à la base, utilisez "ldaps://" et désactviez cette option. @@ -2914,8 +2914,8 @@ doesn't pass when either or both of the selected options are equal or above the - Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...' - Champ qui contient les membres d'un groupe. Si vous utilisez le champ "memberUid", la valeur est censée contenir un nom distinctif relatif, par exemple 'memberUid=un-utilisateur' au lieu de 'memberUid=cn=un-utilisateur,ou=groups,...' + Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...' + Champ qui contient les membres d'un groupe. Si vous utilisez le champ "memberUid", la valeur est censée contenir un nom distinctif relatif, par exemple 'memberUid=un-utilisateur' au lieu de 'memberUid=cn=un-utilisateur,ou=groups,...' @@ -3210,7 +3210,7 @@ doesn't pass when either or both of the selected options are equal or above the Time offset when temporary users should be deleted. This only applies if your IDP uses the NameID Format 'transient', and the user doesn't log out manually. - Moment où les utilisateurs temporaires doivent être supprimés. Cela ne s'applique que si votre IDP utilise le format NameID "transient" et que l'utilisateur ne se déconnecte pas manuellement. + Moment où les utilisateurs temporaires doivent être supprimés. Cela ne s'applique que si votre IDP utilise le format NameID "transient" et que l'utilisateur ne se déconnecte pas manuellement. @@ -3352,7 +3352,7 @@ doesn't pass when either or both of the selected options are equal or above the Optionally set the 'FriendlyName' value of the Assertion attribute. - Indiquer la valeur "FriendlyName" de l'attribut d'assertion (optionnel) + Indiquer la valeur "FriendlyName" de l'attribut d'assertion (optionnel) @@ -3661,8 +3661,8 @@ doesn't pass when either or both of the selected options are equal or above the - When using an external logging solution for archiving, this can be set to "minutes=5". - En cas d'utilisation d'une solution de journalisation externe pour l'archivage, cette valeur peut être fixée à "minutes=5". + When using an external logging solution for archiving, this can be set to "minutes=5". + En cas d'utilisation d'une solution de journalisation externe pour l'archivage, cette valeur peut être fixée à "minutes=5". @@ -3838,10 +3838,10 @@ doesn't pass when either or both of the selected options are equal or above the - Are you sure you want to update ""? + Are you sure you want to update ""? Êtes-vous sûr de vouloir mettre à jour - " - "? + " + "? @@ -4917,8 +4917,8 @@ doesn't pass when either or both of the selected options are equal or above the - A "roaming" authenticator, like a YubiKey - Un authentificateur "itinérant", comme une YubiKey + A "roaming" authenticator, like a YubiKey + Un authentificateur "itinérant", comme une YubiKey @@ -5243,7 +5243,7 @@ doesn't pass when either or both of the selected options are equal or above the Show arbitrary input fields to the user, for example during enrollment. Data is saved in the flow context under the 'prompt_data' variable. - Afficher des champs de saisie arbitraires à l'utilisateur, par exemple pendant l'inscription. Les données sont enregistrées dans le contexte du flux sous la variable "prompt_data". + Afficher des champs de saisie arbitraires à l'utilisateur, par exemple pendant l'inscription. Les données sont enregistrées dans le contexte du flux sous la variable "prompt_data". @@ -5296,8 +5296,8 @@ doesn't pass when either or both of the selected options are equal or above the - If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here. - Si défini à une durée supérieure à 0, l'utilisateur aura la possibilité de choisir de "rester connecté", ce qui prolongera sa session jusqu'à la durée spécifiée ici. + If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here. + Si défini à une durée supérieure à 0, l'utilisateur aura la possibilité de choisir de "rester connecté", ce qui prolongera sa session jusqu'à la durée spécifiée ici. @@ -6065,7 +6065,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti Can be in the format of 'unix://' when connecting to a local docker daemon, using 'ssh://' to connect via SSH, or 'https://:2376' when connecting to a remote system. - Peut être au format "unix://" pour une connexion à un service docker local, "ssh://" pour une connexion via SSH, ou "https://:2376" pour une connexion à un système distant. + Peut être au format "unix://" pour une connexion à un service docker local, "ssh://" pour une connexion via SSH, ou "https://:2376" pour une connexion à un système distant. @@ -7322,7 +7322,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a managed outpost, this is done for you). - Utilisez ce fournisseur avec l'option "auth_request" de Nginx ou "forwardAuth" de Traefik. Chaque application/domaine a besoin de son propre fournisseur. De plus, sur chaque domaine, "/outpost.goauthentik.io" doit être routé vers le poste avancé (lorsque vous utilisez un poste avancé géré, cela est fait pour vous). + Utilisez ce fournisseur avec l'option "auth_request" de Nginx ou "forwardAuth" de Traefik. Chaque application/domaine a besoin de son propre fournisseur. De plus, sur chaque domaine, "/outpost.goauthentik.io" doit être routé vers le poste avancé (lorsque vous utilisez un poste avancé géré, cela est fait pour vous). Default relay state @@ -7720,7 +7720,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti Utilisateur créé et ajouté au groupe avec succès - This user will be added to the group "". + This user will be added to the group "". Cet utilisateur sera ajouté au groupe &quot;&quot;. @@ -9078,7 +9078,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti Synchroniser le groupe - ("", of type ) + ("", of type ) (&quot;&quot;, de type ) @@ -9247,4 +9247,4 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti - \ No newline at end of file + From f4829374743fa4d51709a5ec82967044773f92c4 Mon Sep 17 00:00:00 2001 From: Simon Erhardt Date: Thu, 24 Oct 2024 16:34:45 +0200 Subject: [PATCH 09/43] providers/proxy: fix handling of AUTHENTIK_HOST_BROWSER (#11722) * providers/proxy: fix handling of AUTHENTIK_HOST_BROWSER (#9622/#4688/#6476) * chore: fix tests --- internal/outpost/proxyv2/application/endpoint.go | 3 +++ internal/outpost/proxyv2/application/endpoint_test.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/outpost/proxyv2/application/endpoint.go b/internal/outpost/proxyv2/application/endpoint.go index 9a91459182..c9cc50d40c 100644 --- a/internal/outpost/proxyv2/application/endpoint.go +++ b/internal/outpost/proxyv2/application/endpoint.go @@ -82,6 +82,9 @@ func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string, embedded bo if embedded { ep.Issuer = updateURL(ep.Issuer, newHost.Scheme, newHost.Host) ep.JwksUri = updateURL(jwksUri, newHost.Scheme, newHost.Host) + } else { + // Fixes: https://github.com/goauthentik/authentik/issues/9622 / ep.Issuer must be the HostBrowser URL + ep.Issuer = updateURL(ep.Issuer, newBrowserHost.Scheme, newBrowserHost.Host) } return ep } diff --git a/internal/outpost/proxyv2/application/endpoint_test.go b/internal/outpost/proxyv2/application/endpoint_test.go index d3d0f74262..bd2be424d6 100644 --- a/internal/outpost/proxyv2/application/endpoint_test.go +++ b/internal/outpost/proxyv2/application/endpoint_test.go @@ -55,7 +55,7 @@ func TestEndpointAuthentikHostBrowser(t *testing.T) { assert.Equal(t, "https://browser.test.goauthentik.io/application/o/authorize/", ep.AuthURL) assert.Equal(t, "https://browser.test.goauthentik.io/application/o/test-app/end-session/", ep.EndSessionEndpoint) assert.Equal(t, "https://test.goauthentik.io/application/o/token/", ep.TokenURL) - assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/", ep.Issuer) + assert.Equal(t, "https://browser.test.goauthentik.io/application/o/test-app/", ep.Issuer) assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/jwks/", ep.JwksUri) assert.Equal(t, "https://test.goauthentik.io/application/o/introspect/", ep.TokenIntrospection) } From 3fc0904425978403bf81a52191484ebbe985abc1 Mon Sep 17 00:00:00 2001 From: "Jens L." Date: Thu, 24 Oct 2024 17:56:20 +0200 Subject: [PATCH 10/43] web/admin: fix missing div in wizard forms (#11794) Signed-off-by: Jens Langhammer --- ...lication-wizard-authentication-by-oauth.ts | 62 +++---- .../proxy/AuthenticationByProxyPage.ts | 65 ++++---- ...rd-authentication-by-saml-configuration.ts | 155 +++++++++--------- 3 files changed, 137 insertions(+), 145 deletions(-) diff --git a/web/src/admin/applications/wizard/methods/oauth/ak-application-wizard-authentication-by-oauth.ts b/web/src/admin/applications/wizard/methods/oauth/ak-application-wizard-authentication-by-oauth.ts index 8880d30061..db1d6517f8 100644 --- a/web/src/admin/applications/wizard/methods/oauth/ak-application-wizard-authentication-by-oauth.ts +++ b/web/src/admin/applications/wizard/methods/oauth/ak-application-wizard-authentication-by-oauth.ts @@ -113,9 +113,8 @@ export class ApplicationWizardAuthenticationByOauth extends BaseProviderPanel { @@ -150,35 +149,36 @@ export class ApplicationWizardAuthenticationByOauth extends BaseProviderPanel { ${msg("Advanced flow settings")} - - -

- ${msg( - "Flow used when a user access this provider and is not authenticated.", - )} -

-
- - + + +

+ ${msg( + "Flow used when a user access this provider and is not authenticated.", + )} +

+
+
-

- ${msg("Flow used when logging out of this provider.")} -

-
+ > + +

+ ${msg("Flow used when logging out of this provider.")} +

+
diff --git a/web/src/admin/applications/wizard/methods/proxy/AuthenticationByProxyPage.ts b/web/src/admin/applications/wizard/methods/proxy/AuthenticationByProxyPage.ts index e6d66aea6f..867efbd0b3 100644 --- a/web/src/admin/applications/wizard/methods/proxy/AuthenticationByProxyPage.ts +++ b/web/src/admin/applications/wizard/methods/proxy/AuthenticationByProxyPage.ts @@ -161,11 +161,9 @@ export class AkTypeProxyApplicationWizardPage extends BaseProviderPanel { @@ -184,35 +182,36 @@ export class AkTypeProxyApplicationWizardPage extends BaseProviderPanel { ${msg("Advanced flow settings")} - - -

- ${msg( - "Flow used when a user access this provider and is not authenticated.", - )} -

-
- - + + +

+ ${msg( + "Flow used when a user access this provider and is not authenticated.", + )} +

+
+
-

- ${msg("Flow used when logging out of this provider.")} -

-
+ > + +

+ ${msg("Flow used when logging out of this provider.")} +

+
diff --git a/web/src/admin/applications/wizard/methods/saml/ak-application-wizard-authentication-by-saml-configuration.ts b/web/src/admin/applications/wizard/methods/saml/ak-application-wizard-authentication-by-saml-configuration.ts index 61c1f6403d..54cbe258ca 100644 --- a/web/src/admin/applications/wizard/methods/saml/ak-application-wizard-authentication-by-saml-configuration.ts +++ b/web/src/admin/applications/wizard/methods/saml/ak-application-wizard-authentication-by-saml-configuration.ts @@ -146,36 +146,37 @@ export class ApplicationWizardProviderSamlConfiguration extends BaseProviderPane - ${msg("Advanced flow settings")} - - -

- ${msg( - "Flow used when a user access this provider and is not authenticated.", - )} -

-
- - ${msg("Advanced flow settings")} +
+ + +

+ ${msg( + "Flow used when a user access this provider and is not authenticated.", + )} +

+
+ -

- ${msg("Flow used when logging out of this provider.")} -

-
+ > + +

+ ${msg("Flow used when logging out of this provider.")} +

+
@@ -199,60 +200,52 @@ export class ApplicationWizardProviderSamlConfiguration extends BaseProviderPane )}

- ${ - this.hasSigningKp - ? html` - +

+ ${msg( + "When enabled, the assertion element of the SAML response will be signed.", + )} +

+
` + : nothing} Date: Thu, 24 Oct 2024 22:01:10 +0200 Subject: [PATCH 11/43] providers/scim: clamp batch size for patch requests (#11797) * providers/scim: clamp batch size for patch requests Signed-off-by: Jens Langhammer * sanity check for empty patch request instead Signed-off-by: Jens Langhammer --------- Signed-off-by: Jens Langhammer --- authentik/providers/scim/clients/groups.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/authentik/providers/scim/clients/groups.py b/authentik/providers/scim/clients/groups.py index 44b3405dff..036a2a0fd7 100644 --- a/authentik/providers/scim/clients/groups.py +++ b/authentik/providers/scim/clients/groups.py @@ -197,6 +197,8 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]): chunk_size = self._config.bulk.maxOperations if chunk_size < 1: chunk_size = len(ops) + if len(ops) < 1: + return for chunk in batched(ops, chunk_size): req = PatchRequest(Operations=list(chunk)) self._request( @@ -244,6 +246,9 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]): for user in users_should: if len([x for x in current_group.members if x.value == user]) < 1: users_to_add.append(user) + # Only send request if we need to make changes + if len(users_to_add) < 1 and len(users_to_remove) < 1: + return return self._patch_chunked( scim_group.scim_id, *[ From b7cccf5ad2e697057d56caae93477df36803ab6f Mon Sep 17 00:00:00 2001 From: "Jens L." Date: Fri, 25 Oct 2024 00:42:59 +0200 Subject: [PATCH 12/43] website/docs: improve root page and redirect (#11798) Signed-off-by: Jens Langhammer --- website/src/pages/index.tsx | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/website/src/pages/index.tsx b/website/src/pages/index.tsx index dba1b8dfa1..0b1dc28b24 100644 --- a/website/src/pages/index.tsx +++ b/website/src/pages/index.tsx @@ -1,23 +1,8 @@ import React from "react"; -import clsx from "clsx"; -import Layout from "@theme/Layout"; -import BrowserOnly from "@docusaurus/BrowserOnly"; +import { Redirect } from "@docusaurus/router"; function Home() { - return ( - - - {() => { - window.location.href = "/docs"; - }} - -
-
-

authentik Documentation

-
-
-
- ); + return ; } export default Home; From 9ee0ba141c4cb4d6144430d39862f03ce9739a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simonyi=20Gerg=C5=91?= <28359278+gergosimonyi@users.noreply.github.com> Date: Fri, 25 Oct 2024 08:13:35 +0200 Subject: [PATCH 13/43] stages/identification: add captcha to identification stage (#11711) * add captcha to identification stage * simplify component invocations * fail fast on `onTokenChange` default behavior * reword docs * rename `token` to `captcha_token` in Identification stage contexts (In Captcha stage contexts the name `token` seems well-scoped.) * use `nothing` instead of ``` html`` ``` * remove rendered Captcha component from document flow on Identification stages Note: this doesn't remove the captcha itself, if interactive, only the loading indicator. * add invisible requirement to captcha on Identification stage * stylize docs * add friendlier error messages to Captcha stage * fix tests * make captcha error messages even friendlier * add test case to retriable captcha * use default Signed-off-by: Jens Langhammer --------- Signed-off-by: Jens Langhammer Co-authored-by: Jens Langhammer --- authentik/flows/tests/test_inspector.py | 1 + authentik/stages/captcha/stage.py | 89 +++++++----- authentik/stages/identification/api.py | 2 + .../0015_identificationstage_captcha_stage.py | 26 ++++ authentik/stages/identification/models.py | 14 ++ authentik/stages/identification/stage.py | 25 +++- authentik/stages/identification/tests.py | 132 ++++++++++++++++++ blueprints/schema.json | 5 + schema.yml | 28 ++++ .../identification/IdentificationStageForm.ts | 50 +++++-- web/src/flow/stages/captcha/CaptchaStage.ts | 32 ++--- .../identification/IdentificationStage.ts | 18 ++- .../stages/identification/index.md | 10 +- 13 files changed, 363 insertions(+), 69 deletions(-) create mode 100644 authentik/stages/identification/migrations/0015_identificationstage_captcha_stage.py diff --git a/authentik/flows/tests/test_inspector.py b/authentik/flows/tests/test_inspector.py index 2a01ea370c..81a04a797b 100644 --- a/authentik/flows/tests/test_inspector.py +++ b/authentik/flows/tests/test_inspector.py @@ -46,6 +46,7 @@ class TestFlowInspector(APITestCase): res.content, { "allow_show_password": False, + "captcha_stage": None, "component": "ak-stage-identification", "flow_info": { "background": flow.background_url, diff --git a/authentik/stages/captcha/stage.py b/authentik/stages/captcha/stage.py index 3967e6d3d3..73bcff5dec 100644 --- a/authentik/stages/captcha/stage.py +++ b/authentik/stages/captcha/stage.py @@ -1,10 +1,11 @@ """authentik captcha stage""" from django.http.response import HttpResponse -from django.utils.translation import gettext_lazy as _ +from django.utils.translation import gettext as _ from requests import RequestException from rest_framework.fields import CharField from rest_framework.serializers import ValidationError +from structlog.stdlib import get_logger from authentik.flows.challenge import ( Challenge, @@ -16,6 +17,7 @@ from authentik.lib.utils.http import get_http_session from authentik.root.middleware import ClientIPMiddleware from authentik.stages.captcha.models import CaptchaStage +LOGGER = get_logger() PLAN_CONTEXT_CAPTCHA = "captcha" @@ -27,6 +29,56 @@ class CaptchaChallenge(WithUserInfoChallenge): component = CharField(default="ak-stage-captcha") +def verify_captcha_token(stage: CaptchaStage, token: str, remote_ip: str): + """Validate captcha token""" + try: + response = get_http_session().post( + stage.api_url, + headers={ + "Content-type": "application/x-www-form-urlencoded", + }, + data={ + "secret": stage.private_key, + "response": token, + "remoteip": remote_ip, + }, + ) + response.raise_for_status() + data = response.json() + if stage.error_on_invalid_score: + if not data.get("success", False): + error_codes = data.get("error-codes", ["unknown-error"]) + LOGGER.warning("Failed to verify captcha token", error_codes=error_codes) + + # These cases can usually be fixed by simply requesting a new token and retrying. + # [reCAPTCHA](https://developers.google.com/recaptcha/docs/verify#error_code_reference) + # [hCaptcha](https://docs.hcaptcha.com/#siteverify-error-codes-table) + # [Turnstile](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#error-codes) + retriable_error_codes = [ + "missing-input-response", + "invalid-input-response", + "timeout-or-duplicate", + "expired-input-response", + "already-seen-response", + ] + + if set(error_codes).issubset(set(retriable_error_codes)): + error_message = _("Invalid captcha response. Retrying may solve this issue.") + else: + error_message = _("Invalid captcha response") + raise ValidationError(error_message) + if "score" in data: + score = float(data.get("score")) + if stage.score_max_threshold > -1 and score > stage.score_max_threshold: + raise ValidationError(_("Invalid captcha response")) + if stage.score_min_threshold > -1 and score < stage.score_min_threshold: + raise ValidationError(_("Invalid captcha response")) + except (RequestException, TypeError) as exc: + raise ValidationError(_("Failed to validate token")) from exc + + return data + + class CaptchaChallengeResponse(ChallengeResponse): """Validate captcha token""" @@ -36,38 +88,9 @@ class CaptchaChallengeResponse(ChallengeResponse): def validate_token(self, token: str) -> str: """Validate captcha token""" stage: CaptchaStage = self.stage.executor.current_stage - try: - response = get_http_session().post( - stage.api_url, - headers={ - "Content-type": "application/x-www-form-urlencoded", - }, - data={ - "secret": stage.private_key, - "response": token, - "remoteip": ClientIPMiddleware.get_client_ip(self.stage.request), - }, - ) - response.raise_for_status() - data = response.json() - if stage.error_on_invalid_score: - if not data.get("success", False): - raise ValidationError( - _( - "Failed to validate token: {error}".format( - error=data.get("error-codes", _("Unknown error")) - ) - ) - ) - if "score" in data: - score = float(data.get("score")) - if stage.score_max_threshold > -1 and score > stage.score_max_threshold: - raise ValidationError(_("Invalid captcha response")) - if stage.score_min_threshold > -1 and score < stage.score_min_threshold: - raise ValidationError(_("Invalid captcha response")) - except (RequestException, TypeError) as exc: - raise ValidationError(_("Failed to validate token")) from exc - return data + client_ip = ClientIPMiddleware.get_client_ip(self.stage.request) + + return verify_captcha_token(stage, token, client_ip) class CaptchaStageView(ChallengeStageView): diff --git a/authentik/stages/identification/api.py b/authentik/stages/identification/api.py index 9ad97320e8..c8de2d7436 100644 --- a/authentik/stages/identification/api.py +++ b/authentik/stages/identification/api.py @@ -27,6 +27,7 @@ class IdentificationStageSerializer(StageSerializer): fields = StageSerializer.Meta.fields + [ "user_fields", "password_stage", + "captcha_stage", "case_insensitive_matching", "show_matched_user", "enrollment_flow", @@ -46,6 +47,7 @@ class IdentificationStageViewSet(UsedByMixin, ModelViewSet): filterset_fields = [ "name", "password_stage", + "captcha_stage", "case_insensitive_matching", "show_matched_user", "enrollment_flow", diff --git a/authentik/stages/identification/migrations/0015_identificationstage_captcha_stage.py b/authentik/stages/identification/migrations/0015_identificationstage_captcha_stage.py new file mode 100644 index 0000000000..734dc7631c --- /dev/null +++ b/authentik/stages/identification/migrations/0015_identificationstage_captcha_stage.py @@ -0,0 +1,26 @@ +# Generated by Django 5.0.8 on 2024-08-29 11:31 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_stages_captcha", "0003_captchastage_error_on_invalid_score_and_more"), + ("authentik_stages_identification", "0014_identificationstage_pretend"), + ] + + operations = [ + migrations.AddField( + model_name="identificationstage", + name="captcha_stage", + field=models.ForeignKey( + default=None, + help_text="When set, adds functionality exactly like a Captcha stage, but baked into the Identification stage.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="authentik_stages_captcha.captchastage", + ), + ), + ] diff --git a/authentik/stages/identification/models.py b/authentik/stages/identification/models.py index 27cfcb92f1..ed6728c932 100644 --- a/authentik/stages/identification/models.py +++ b/authentik/stages/identification/models.py @@ -8,6 +8,7 @@ from rest_framework.serializers import BaseSerializer from authentik.core.models import Source from authentik.flows.models import Flow, Stage +from authentik.stages.captcha.models import CaptchaStage from authentik.stages.password.models import PasswordStage @@ -43,6 +44,19 @@ class IdentificationStage(Stage): ), ) + captcha_stage = models.ForeignKey( + CaptchaStage, + null=True, + default=None, + on_delete=models.SET_NULL, + help_text=_( + ( + "When set, adds functionality exactly like a Captcha stage, but baked into the " + "Identification stage." + ), + ), + ) + case_insensitive_matching = models.BooleanField( default=True, help_text=_("When enabled, user fields are matched regardless of their casing."), diff --git a/authentik/stages/identification/stage.py b/authentik/stages/identification/stage.py index dffd119da9..1d2dfe8cab 100644 --- a/authentik/stages/identification/stage.py +++ b/authentik/stages/identification/stage.py @@ -29,6 +29,7 @@ from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_ from authentik.lib.utils.reflection import all_subclasses from authentik.lib.utils.urls import reverse_with_qs from authentik.root.middleware import ClientIPMiddleware +from authentik.stages.captcha.stage import CaptchaChallenge, verify_captcha_token from authentik.stages.identification.models import IdentificationStage from authentik.stages.identification.signals import identification_failed from authentik.stages.password.stage import authenticate @@ -75,6 +76,7 @@ class IdentificationChallenge(Challenge): allow_show_password = BooleanField(default=False) application_pre = CharField(required=False) flow_designation = ChoiceField(FlowDesignation.choices) + captcha_stage = CaptchaChallenge(required=False) enroll_url = CharField(required=False) recovery_url = CharField(required=False) @@ -91,14 +93,16 @@ class IdentificationChallengeResponse(ChallengeResponse): uid_field = CharField() password = CharField(required=False, allow_blank=True, allow_null=True) + captcha_token = CharField(required=False, allow_blank=True, allow_null=True) component = CharField(default="ak-stage-identification") pre_user: User | None = None def validate(self, attrs: dict[str, Any]) -> dict[str, Any]: - """Validate that user exists, and optionally their password""" + """Validate that user exists, and optionally their password and captcha token""" uid_field = attrs["uid_field"] current_stage: IdentificationStage = self.stage.executor.current_stage + client_ip = ClientIPMiddleware.get_client_ip(self.stage.request) pre_user = self.stage.get_user(uid_field) if not pre_user: @@ -113,7 +117,7 @@ class IdentificationChallengeResponse(ChallengeResponse): self.stage.logger.info( "invalid_login", identifier=uid_field, - client_ip=ClientIPMiddleware.get_client_ip(self.stage.request), + client_ip=client_ip, action="invalid_identifier", context={ "stage": sanitize_item(self.stage), @@ -136,6 +140,15 @@ class IdentificationChallengeResponse(ChallengeResponse): return attrs raise ValidationError("Failed to authenticate.") self.pre_user = pre_user + + # Captcha check + if captcha_stage := current_stage.captcha_stage: + captcha_token = attrs.get("captcha_token", None) + if not captcha_token: + self.stage.logger.warning("Token not set for captcha attempt") + verify_captcha_token(captcha_stage, captcha_token, client_ip) + + # Password check if not current_stage.password_stage: # No password stage select, don't validate the password return attrs @@ -206,6 +219,14 @@ class IdentificationStageView(ChallengeStageView): "primary_action": self.get_primary_action(), "user_fields": current_stage.user_fields, "password_fields": bool(current_stage.password_stage), + "captcha_stage": ( + { + "js_url": current_stage.captcha_stage.js_url, + "site_key": current_stage.captcha_stage.public_key, + } + if current_stage.captcha_stage + else None + ), "allow_show_password": bool(current_stage.password_stage) and current_stage.password_stage.allow_show_password, "show_source_labels": current_stage.show_source_labels, diff --git a/authentik/stages/identification/tests.py b/authentik/stages/identification/tests.py index 57ffed1283..c39434e24a 100644 --- a/authentik/stages/identification/tests.py +++ b/authentik/stages/identification/tests.py @@ -1,6 +1,7 @@ """identification tests""" from django.urls import reverse +from requests_mock import Mocker from rest_framework.exceptions import ValidationError from authentik.core.tests.utils import create_test_admin_user, create_test_flow @@ -8,6 +9,8 @@ from authentik.flows.models import FlowDesignation, FlowStageBinding from authentik.flows.tests import FlowTestCase from authentik.lib.generators import generate_id from authentik.sources.oauth.models import OAuthSource +from authentik.stages.captcha.models import CaptchaStage +from authentik.stages.captcha.tests import RECAPTCHA_PRIVATE_KEY, RECAPTCHA_PUBLIC_KEY from authentik.stages.identification.api import IdentificationStageSerializer from authentik.stages.identification.models import IdentificationStage, UserFields from authentik.stages.password import BACKEND_INBUILT @@ -133,6 +136,135 @@ class TestIdentificationStage(FlowTestCase): user_fields=["email"], ) + @Mocker() + def test_valid_with_captcha(self, mock: Mocker): + """Test with valid email and captcha token in single step""" + mock.post( + "https://www.recaptcha.net/recaptcha/api/siteverify", + json={ + "success": True, + "score": 0.5, + }, + ) + + captcha_stage = CaptchaStage.objects.create( + name="captcha", + public_key=RECAPTCHA_PUBLIC_KEY, + private_key=RECAPTCHA_PRIVATE_KEY, + ) + self.stage.captcha_stage = captcha_stage + self.stage.save() + + form_data = {"uid_field": self.user.email, "captcha_token": "PASSED"} + url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) + response = self.client.post(url, form_data) + self.assertEqual(response.status_code, 200) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) + + @Mocker() + def test_invalid_with_captcha(self, mock: Mocker): + """Test with valid email and invalid captcha token in single step""" + mock.post( + "https://www.recaptcha.net/recaptcha/api/siteverify", + json={ + "success": False, + "score": 0.5, + }, + ) + + captcha_stage = CaptchaStage.objects.create( + name="captcha", + public_key=RECAPTCHA_PUBLIC_KEY, + private_key=RECAPTCHA_PRIVATE_KEY, + ) + + self.stage.captcha_stage = captcha_stage + self.stage.save() + + form_data = { + "uid_field": self.user.email, + "captcha_token": "FAILED", + } + url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) + response = self.client.post(url, form_data) + self.assertStageResponse( + response, + self.flow, + component="ak-stage-identification", + password_fields=False, + primary_action="Log in", + response_errors={ + "non_field_errors": [{"code": "invalid", "string": "Invalid captcha response"}] + }, + sources=[ + { + "challenge": { + "component": "xak-flow-redirect", + "to": "/source/oauth/login/test/", + }, + "icon_url": "/static/authentik/sources/default.svg", + "name": "test", + } + ], + show_source_labels=False, + user_fields=["email"], + ) + + @Mocker() + def test_invalid_with_captcha_retriable(self, mock: Mocker): + """Test with valid email and invalid captcha token in single step""" + mock.post( + "https://www.recaptcha.net/recaptcha/api/siteverify", + json={ + "success": False, + "score": 0.5, + "error-codes": ["timeout-or-duplicate"], + }, + ) + + captcha_stage = CaptchaStage.objects.create( + name="captcha", + public_key=RECAPTCHA_PUBLIC_KEY, + private_key=RECAPTCHA_PRIVATE_KEY, + ) + + self.stage.captcha_stage = captcha_stage + self.stage.save() + + form_data = { + "uid_field": self.user.email, + "captcha_token": "FAILED", + } + url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) + response = self.client.post(url, form_data) + self.assertStageResponse( + response, + self.flow, + component="ak-stage-identification", + password_fields=False, + primary_action="Log in", + response_errors={ + "non_field_errors": [ + { + "code": "invalid", + "string": "Invalid captcha response. Retrying may solve this issue.", + } + ] + }, + sources=[ + { + "challenge": { + "component": "xak-flow-redirect", + "to": "/source/oauth/login/test/", + }, + "icon_url": "/static/authentik/sources/default.svg", + "name": "test", + } + ], + show_source_labels=False, + user_fields=["email"], + ) + def test_invalid_with_username(self): """Test invalid with username (user exists but stage only allows email)""" form_data = {"uid_field": self.user.username} diff --git a/blueprints/schema.json b/blueprints/schema.json index 9b3b91eb74..0604c8d76d 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -10679,6 +10679,11 @@ "title": "Password stage", "description": "When set, shows a password field, instead of showing the password field as separate step." }, + "captcha_stage": { + "type": "integer", + "title": "Captcha stage", + "description": "When set, adds functionality exactly like a Captcha stage, but baked into the Identification stage." + }, "case_insensitive_matching": { "type": "boolean", "title": "Case insensitive matching", diff --git a/schema.yml b/schema.yml index f0c8447abb..c920d1ab11 100644 --- a/schema.yml +++ b/schema.yml @@ -33862,6 +33862,11 @@ paths: operationId: stages_identification_list description: IdentificationStage Viewset parameters: + - in: query + name: captcha_stage + schema: + type: string + format: uuid - in: query name: case_insensitive_matching schema: @@ -42504,6 +42509,8 @@ components: type: string flow_designation: $ref: '#/components/schemas/FlowDesignationEnum' + captcha_stage: + $ref: '#/components/schemas/CaptchaChallenge' enroll_url: type: string recovery_url: @@ -42538,6 +42545,9 @@ components: password: type: string nullable: true + captcha_token: + type: string + nullable: true required: - uid_field IdentificationStage: @@ -42583,6 +42593,12 @@ components: nullable: true description: When set, shows a password field, instead of showing the password field as separate step. + captcha_stage: + type: string + format: uuid + nullable: true + description: When set, adds functionality exactly like a Captcha stage, + but baked into the Identification stage. case_insensitive_matching: type: boolean description: When enabled, user fields are matched regardless of their casing. @@ -42651,6 +42667,12 @@ components: nullable: true description: When set, shows a password field, instead of showing the password field as separate step. + captcha_stage: + type: string + format: uuid + nullable: true + description: When set, adds functionality exactly like a Captcha stage, + but baked into the Identification stage. case_insensitive_matching: type: boolean description: When enabled, user fields are matched regardless of their casing. @@ -48241,6 +48263,12 @@ components: nullable: true description: When set, shows a password field, instead of showing the password field as separate step. + captcha_stage: + type: string + format: uuid + nullable: true + description: When set, adds functionality exactly like a Captcha stage, + but baked into the Identification stage. case_insensitive_matching: type: boolean description: When enabled, user fields are matched regardless of their casing. diff --git a/web/src/admin/stages/identification/IdentificationStageForm.ts b/web/src/admin/stages/identification/IdentificationStageForm.ts index 6a9c65de08..7a20af84d6 100644 --- a/web/src/admin/stages/identification/IdentificationStageForm.ts +++ b/web/src/admin/stages/identification/IdentificationStageForm.ts @@ -21,6 +21,7 @@ import { SourcesApi, Stage, StagesApi, + StagesCaptchaListRequest, StagesPasswordListRequest, UserFieldsEnum, } from "@goauthentik/api"; @@ -140,19 +141,13 @@ export class IdentificationStageForm extends BaseStageForm ).stagesPasswordList(args); return stages.results; }} - .groupBy=${(items: Stage[]) => { - return groupBy(items, (stage) => stage.verboseNamePlural); - }} - .renderElement=${(stage: Stage): string => { - return stage.name; - }} - .value=${(stage: Stage | undefined): string | undefined => { - return stage?.pk; - }} - .selected=${(stage: Stage): boolean => { - return stage.pk === this.instance?.passwordStage; - }} - ?blankable=${true} + .groupBy=${(items: Stage[]) => + groupBy(items, (stage) => stage.verboseNamePlural)} + .renderElement=${(stage: Stage): string => stage.name} + .value=${(stage: Stage | undefined): string | undefined => stage?.pk} + .selected=${(stage: Stage): boolean => + stage.pk === this.instance?.passwordStage} + blankable >

@@ -161,6 +156,35 @@ export class IdentificationStageForm extends BaseStageForm )}

+ + => { + const args: StagesCaptchaListRequest = { + ordering: "name", + }; + if (query !== undefined) { + args.search = query; + } + const stages = await new StagesApi( + DEFAULT_CONFIG, + ).stagesCaptchaList(args); + return stages.results; + }} + .groupBy=${(items: Stage[]) => + groupBy(items, (stage) => stage.verboseNamePlural)} + .renderElement=${(stage: Stage): string => stage.name} + .value=${(stage: Stage | undefined): string | undefined => stage?.pk} + .selected=${(stage: Stage): boolean => + stage.pk === this.instance?.captchaStage} + blankable + > + +

+ ${msg( + "When set, adds functionality exactly like a Captcha stage, but baked into the Identification stage.", + )} +

+