From 20572c728d58f063f0dfd159197fb50bb638ff06 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 23 Aug 2021 16:05:29 +0200 Subject: [PATCH 01/22] core: add new token intent and auth backend Signed-off-by: Jens Langhammer --- authentik/core/models.py | 3 +++ authentik/core/token_auth.py | 29 +++++++++++++++++++++++++++ authentik/stages/password/__init__.py | 1 + authentik/stages/password/models.py | 10 ++++++--- 4 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 authentik/core/token_auth.py diff --git a/authentik/core/models.py b/authentik/core/models.py index bc3fa43ae..214e4217f 100644 --- a/authentik/core/models.py +++ b/authentik/core/models.py @@ -408,6 +408,9 @@ class TokenIntents(models.TextChoices): # Recovery use for the recovery app INTENT_RECOVERY = "recovery" + # App-specific passwords + INTENT_APP_PASSWORD = "app_password" + class Token(ManagedModel, ExpiringModel): """Token used to authenticate the User for API Access or confirm another Stage like Email.""" diff --git a/authentik/core/token_auth.py b/authentik/core/token_auth.py new file mode 100644 index 000000000..1c95ce996 --- /dev/null +++ b/authentik/core/token_auth.py @@ -0,0 +1,29 @@ +"""Authenticate with tokens""" + +from typing import Any, Optional + +from django.contrib.auth.backends import ModelBackend +from django.http.request import HttpRequest + +from authentik.core.models import Token, TokenIntents, User + + +class TokenBackend(ModelBackend): + """Authenticate with token""" + + def authenticate( + self, request: HttpRequest, username: Optional[str], password: Optional[str], **kwargs: Any + ) -> Optional[User]: + try: + user = User._default_manager.get_by_natural_key(username) + except User.DoesNotExist: + # Run the default password hasher once to reduce the timing + # difference between an existing and a nonexistent user (#20760). + User().set_password(password) + return None + tokens = Token.filter_not_expired( + user=user, key=password, intent=TokenIntents.INTENT_APP_PASSWORD + ) + if not tokens.exists(): + return None + return user diff --git a/authentik/stages/password/__init__.py b/authentik/stages/password/__init__.py index a9c71fe82..9e42ec060 100644 --- a/authentik/stages/password/__init__.py +++ b/authentik/stages/password/__init__.py @@ -1,3 +1,4 @@ """Backend paths""" BACKEND_DJANGO = "django.contrib.auth.backends.ModelBackend" BACKEND_LDAP = "authentik.sources.ldap.auth.LDAPBackend" +BACKEND_TOKEN = "authentik.core.token_auth.TokenBackend" diff --git a/authentik/stages/password/models.py b/authentik/stages/password/models.py index e0df6b77b..66fd5d043 100644 --- a/authentik/stages/password/models.py +++ b/authentik/stages/password/models.py @@ -9,7 +9,7 @@ from rest_framework.serializers import BaseSerializer from authentik.core.types import UserSettingSerializer from authentik.flows.models import ConfigurableStage, Stage -from authentik.stages.password import BACKEND_DJANGO, BACKEND_LDAP +from authentik.stages.password import BACKEND_DJANGO, BACKEND_LDAP, BACKEND_TOKEN def get_authentication_backends(): @@ -17,11 +17,15 @@ def get_authentication_backends(): return [ ( BACKEND_DJANGO, - _("authentik-internal Userdatabase"), + _("User database + standard password"), + ), + ( + BACKEND_TOKEN, + _("User database + app passwords"), ), ( BACKEND_LDAP, - _("authentik LDAP"), + _("User database + LDAP password"), ), ] From 9a6a3e66b8dc66c5a892a3f925443ec680a33803 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 23 Aug 2021 16:14:33 +0200 Subject: [PATCH 02/22] root: update schema Signed-off-by: Jens Langhammer --- authentik/core/models.py | 2 +- authentik/stages/password/__init__.py | 2 +- schema.yml | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/authentik/core/models.py b/authentik/core/models.py index 214e4217f..de64221b4 100644 --- a/authentik/core/models.py +++ b/authentik/core/models.py @@ -409,7 +409,7 @@ class TokenIntents(models.TextChoices): INTENT_RECOVERY = "recovery" # App-specific passwords - INTENT_APP_PASSWORD = "app_password" + INTENT_APP_PASSWORD = "app_password" # nosec class Token(ManagedModel, ExpiringModel): diff --git a/authentik/stages/password/__init__.py b/authentik/stages/password/__init__.py index 9e42ec060..2bfdc92e1 100644 --- a/authentik/stages/password/__init__.py +++ b/authentik/stages/password/__init__.py @@ -1,4 +1,4 @@ """Backend paths""" BACKEND_DJANGO = "django.contrib.auth.backends.ModelBackend" BACKEND_LDAP = "authentik.sources.ldap.auth.LDAPBackend" -BACKEND_TOKEN = "authentik.core.token_auth.TokenBackend" +BACKEND_TOKEN = "authentik.core.token_auth.TokenBackend" # nosec diff --git a/schema.yml b/schema.yml index 271547231..38587c822 100644 --- a/schema.yml +++ b/schema.yml @@ -2515,6 +2515,7 @@ paths: type: string enum: - api + - app_password - recovery - verification - name: ordering @@ -20382,6 +20383,7 @@ components: BackendsEnum: enum: - django.contrib.auth.backends.ModelBackend + - authentik.core.token_auth.TokenBackend - authentik.sources.ldap.auth.LDAPBackend type: string BindingTypeEnum: @@ -22211,6 +22213,7 @@ components: - verification - api - recovery + - app_password type: string InvalidResponseActionEnum: enum: From f217d34a98e60d309cc3965a8cede5c84e40d72f Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 23 Aug 2021 16:20:49 +0200 Subject: [PATCH 03/22] web/admin: allow users to create app password tokens Signed-off-by: Jens Langhammer --- authentik/core/api/tokens.py | 8 +++++++- web/src/pages/user-settings/UserSettingsPage.ts | 2 +- .../pages/user-settings/tokens/UserTokenForm.ts | 8 ++++++-- .../pages/user-settings/tokens/UserTokenList.ts | 17 ++++++++++++++--- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/authentik/core/api/tokens.py b/authentik/core/api/tokens.py index e8268ebe8..f8e14a2e8 100644 --- a/authentik/core/api/tokens.py +++ b/authentik/core/api/tokens.py @@ -2,6 +2,7 @@ from django.http.response import Http404 from drf_spectacular.utils import OpenApiResponse, extend_schema from rest_framework.decorators import action +from rest_framework.exceptions import ValidationError from rest_framework.fields import CharField from rest_framework.request import Request from rest_framework.response import Response @@ -22,6 +23,12 @@ class TokenSerializer(ManagedSerializer, ModelSerializer): user = UserSerializer(required=False) + def validate_intent(self, value: str) -> str: + """Ensure only API or App password tokens are created.""" + if value not in [TokenIntents.INTENT_API, TokenIntents.INTENT_APP_PASSWORD]: + raise ValidationError(f"Invalid intent {value}") + return value + class Meta: model = Token @@ -69,7 +76,6 @@ class TokenViewSet(UsedByMixin, ModelViewSet): def perform_create(self, serializer: TokenSerializer): serializer.save( user=self.request.user, - intent=TokenIntents.INTENT_API, expiring=self.request.user.attributes.get(USER_ATTRIBUTE_TOKEN_EXPIRING, True), ) diff --git a/web/src/pages/user-settings/UserSettingsPage.ts b/web/src/pages/user-settings/UserSettingsPage.ts index 88534871b..e7b627857 100644 --- a/web/src/pages/user-settings/UserSettingsPage.ts +++ b/web/src/pages/user-settings/UserSettingsPage.ts @@ -148,7 +148,7 @@ export class UserSettingsPage extends LitElement {
diff --git a/web/src/pages/user-settings/tokens/UserTokenForm.ts b/web/src/pages/user-settings/tokens/UserTokenForm.ts index 68a035d7a..3c4ffd4c2 100644 --- a/web/src/pages/user-settings/tokens/UserTokenForm.ts +++ b/web/src/pages/user-settings/tokens/UserTokenForm.ts @@ -1,6 +1,6 @@ -import { CoreApi, Token } from "@goauthentik/api"; +import { CoreApi, IntentEnum, Token } from "@goauthentik/api"; import { t } from "@lingui/macro"; -import { customElement } from "lit-element"; +import { customElement, property } from "lit-element"; import { html, TemplateResult } from "lit-html"; import { DEFAULT_CONFIG } from "../../../api/Config"; import { ifDefined } from "lit-html/directives/if-defined"; @@ -9,6 +9,9 @@ import { ModelForm } from "../../../elements/forms/ModelForm"; @customElement("ak-user-token-form") export class UserTokenForm extends ModelForm { + @property() + intent: IntentEnum = IntentEnum.Api; + loadInstance(pk: string): Promise { return new CoreApi(DEFAULT_CONFIG).coreTokensRetrieve({ identifier: pk, @@ -30,6 +33,7 @@ export class UserTokenForm extends ModelForm { tokenRequest: data, }); } else { + data.intent = this.intent; return new CoreApi(DEFAULT_CONFIG).coreTokensCreate({ tokenRequest: data, }); diff --git a/web/src/pages/user-settings/tokens/UserTokenList.ts b/web/src/pages/user-settings/tokens/UserTokenList.ts index 2b542d32b..76d743f42 100644 --- a/web/src/pages/user-settings/tokens/UserTokenList.ts +++ b/web/src/pages/user-settings/tokens/UserTokenList.ts @@ -10,7 +10,7 @@ import "../../../elements/buttons/Dropdown"; import "../../../elements/buttons/TokenCopyButton"; import { Table, TableColumn } from "../../../elements/table/Table"; import { PAGE_SIZE } from "../../../constants"; -import { CoreApi, Token } from "@goauthentik/api"; +import { CoreApi, IntentEnum, Token } from "@goauthentik/api"; import { DEFAULT_CONFIG } from "../../../api/Config"; import "./UserTokenForm"; @@ -48,8 +48,19 @@ export class UserTokenList extends Table { ${t`Create`} ${t`Create Token`} - - + + + + + ${t`Create`} + ${t`Create App password`} + + + ${super.renderToolbar()} `; From c4832206fa3d21d696d804021ad465f2a3778ae4 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 23 Aug 2021 16:33:12 +0200 Subject: [PATCH 04/22] web/admin: display token's intents Signed-off-by: Jens Langhammer --- web/src/locales/en.po | 29 ++++++++++++++++++- web/src/locales/pseudo-LOCALE.po | 29 ++++++++++++++++++- web/src/pages/tokens/TokenListPage.ts | 17 ++++++++++- .../user-settings/tokens/UserTokenList.ts | 11 +++++++ 4 files changed, 83 insertions(+), 3 deletions(-) diff --git a/web/src/locales/en.po b/web/src/locales/en.po index f2c776929..c09f84d65 100644 --- a/web/src/locales/en.po +++ b/web/src/locales/en.po @@ -63,6 +63,10 @@ msgstr "ANY, any policy must match to grant access." msgid "ANY, any policy must match to include this stage access." msgstr "ANY, any policy must match to include this stage access." +#: src/pages/tokens/TokenListPage.ts +msgid "API Access" +msgstr "API Access" + #: src/pages/stages/authenticator_duo/AuthenticatorDuoStageForm.ts msgid "API Hostname" msgstr "API Hostname" @@ -231,6 +235,10 @@ msgstr "Always require consent" msgid "App" msgstr "App" +#: src/pages/tokens/TokenListPage.ts +msgid "App password" +msgstr "App password" + #: src/elements/user/UserConsentList.ts #: src/pages/admin-overview/TopApplicationsTable.ts #: src/pages/providers/ProviderListPage.ts @@ -952,6 +960,11 @@ msgstr "Copy recovery link" msgid "Create" msgstr "Create" +#: src/pages/user-settings/tokens/UserTokenList.ts +#: src/pages/user-settings/tokens/UserTokenList.ts +msgid "Create App password" +msgstr "Create App password" + #: src/pages/applications/ApplicationListPage.ts #: src/pages/providers/RelatedApplicationButton.ts msgid "Create Application" @@ -1018,6 +1031,7 @@ msgstr "Create Stage binding" msgid "Create Tenant" msgstr "Create Tenant" +#: src/pages/user-settings/tokens/UserTokenList.ts #: src/pages/user-settings/tokens/UserTokenList.ts msgid "Create Token" msgstr "Create Token" @@ -2047,6 +2061,11 @@ msgstr "Integration key" msgid "Integrations" msgstr "Integrations" +#: src/pages/tokens/TokenListPage.ts +#: src/pages/user-settings/tokens/UserTokenList.ts +msgid "Intent" +msgstr "Intent" + #: src/pages/providers/proxy/ProxyProviderViewPage.ts msgid "Internal Host" msgstr "Internal Host" @@ -3203,6 +3222,7 @@ msgid "Receive a push notification on your phone to prove your identity." msgstr "Receive a push notification on your phone to prove your identity." #: src/pages/flows/FlowForm.ts +#: src/pages/tokens/TokenListPage.ts #: src/pages/users/UserListPage.ts msgid "Recovery" msgstr "Recovery" @@ -4388,10 +4408,13 @@ msgstr "Token(s)" #: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts #: src/interfaces/AdminInterface.ts #: src/pages/tokens/TokenListPage.ts -#: src/pages/user-settings/UserSettingsPage.ts msgid "Tokens" msgstr "Tokens" +#: src/pages/user-settings/UserSettingsPage.ts +msgid "Tokens and App passwords" +msgstr "Tokens and App passwords" + #: src/pages/tokens/TokenListPage.ts msgid "Tokens are used throughout authentik for Email validation stages, Recovery keys and API access." msgstr "Tokens are used throughout authentik for Email validation stages, Recovery keys and API access." @@ -4859,6 +4882,10 @@ msgstr "Validation Policies" msgid "Validity days" msgstr "Validity days" +#: src/pages/tokens/TokenListPage.ts +msgid "Verification" +msgstr "Verification" + #: src/pages/providers/saml/SAMLProviderForm.ts msgid "Verification Certificate" msgstr "Verification Certificate" diff --git a/web/src/locales/pseudo-LOCALE.po b/web/src/locales/pseudo-LOCALE.po index 5de2beb61..36be57c10 100644 --- a/web/src/locales/pseudo-LOCALE.po +++ b/web/src/locales/pseudo-LOCALE.po @@ -63,6 +63,10 @@ msgstr "" msgid "ANY, any policy must match to include this stage access." msgstr "" +#: src/pages/tokens/TokenListPage.ts +msgid "API Access" +msgstr "" + #: src/pages/stages/authenticator_duo/AuthenticatorDuoStageForm.ts msgid "API Hostname" msgstr "" @@ -231,6 +235,10 @@ msgstr "" msgid "App" msgstr "" +#: src/pages/tokens/TokenListPage.ts +msgid "App password" +msgstr "" + #: src/elements/user/UserConsentList.ts #: src/pages/admin-overview/TopApplicationsTable.ts #: src/pages/providers/ProviderListPage.ts @@ -946,6 +954,11 @@ msgstr "" msgid "Create" msgstr "" +#: src/pages/user-settings/tokens/UserTokenList.ts +#: src/pages/user-settings/tokens/UserTokenList.ts +msgid "Create App password" +msgstr "" + #: src/pages/applications/ApplicationListPage.ts #: src/pages/providers/RelatedApplicationButton.ts msgid "Create Application" @@ -1012,6 +1025,7 @@ msgstr "" msgid "Create Tenant" msgstr "" +#: src/pages/user-settings/tokens/UserTokenList.ts #: src/pages/user-settings/tokens/UserTokenList.ts msgid "Create Token" msgstr "" @@ -2039,6 +2053,11 @@ msgstr "" msgid "Integrations" msgstr "" +#: src/pages/tokens/TokenListPage.ts +#: src/pages/user-settings/tokens/UserTokenList.ts +msgid "Intent" +msgstr "" + #: src/pages/providers/proxy/ProxyProviderViewPage.ts msgid "Internal Host" msgstr "" @@ -3195,6 +3214,7 @@ msgid "Receive a push notification on your phone to prove your identity." msgstr "" #: src/pages/flows/FlowForm.ts +#: src/pages/tokens/TokenListPage.ts #: src/pages/users/UserListPage.ts msgid "Recovery" msgstr "" @@ -4373,10 +4393,13 @@ msgstr "" #: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts #: src/interfaces/AdminInterface.ts #: src/pages/tokens/TokenListPage.ts -#: src/pages/user-settings/UserSettingsPage.ts msgid "Tokens" msgstr "" +#: src/pages/user-settings/UserSettingsPage.ts +msgid "Tokens and App passwords" +msgstr "" + #: src/pages/tokens/TokenListPage.ts msgid "Tokens are used throughout authentik for Email validation stages, Recovery keys and API access." msgstr "" @@ -4844,6 +4867,10 @@ msgstr "" msgid "Validity days" msgstr "" +#: src/pages/tokens/TokenListPage.ts +msgid "Verification" +msgstr "" + #: src/pages/providers/saml/SAMLProviderForm.ts msgid "Verification Certificate" msgstr "" diff --git a/web/src/pages/tokens/TokenListPage.ts b/web/src/pages/tokens/TokenListPage.ts index cec399447..604b756e4 100644 --- a/web/src/pages/tokens/TokenListPage.ts +++ b/web/src/pages/tokens/TokenListPage.ts @@ -8,9 +8,22 @@ import "../../elements/buttons/TokenCopyButton"; import "../../elements/forms/DeleteBulkForm"; import { TableColumn } from "../../elements/table/Table"; import { PAGE_SIZE } from "../../constants"; -import { CoreApi, Token } from "@goauthentik/api"; +import { CoreApi, IntentEnum, Token } from "@goauthentik/api"; import { DEFAULT_CONFIG } from "../../api/Config"; +export function IntentToLabel(intent: IntentEnum): string { + switch (intent) { + case IntentEnum.Api: + return t`API Access`; + case IntentEnum.AppPassword: + return t`App password`; + case IntentEnum.Recovery: + return t`Recovery`; + case IntentEnum.Verification: + return t`Verification`; + } +} + @customElement("ak-token-list") export class TokenListPage extends TablePage { searchEnabled(): boolean { @@ -46,6 +59,7 @@ export class TokenListPage extends TablePage { new TableColumn(t`User`, "user"), new TableColumn(t`Expires?`, "expiring"), new TableColumn(t`Expiry date`, "expires"), + new TableColumn(t`Intent`, "intent"), new TableColumn(t`Actions`), ]; } @@ -78,6 +92,7 @@ export class TokenListPage extends TablePage { html`${item.user?.username}`, html`${item.expiring ? t`Yes` : t`No`}`, html`${item.expiring ? item.expires?.toLocaleString() : "-"}`, + html`${IntentToLabel(item.intent || IntentEnum.Api)}`, html` ${t`Copy Key`} diff --git a/web/src/pages/user-settings/tokens/UserTokenList.ts b/web/src/pages/user-settings/tokens/UserTokenList.ts index 76d743f42..b9e6fc5c1 100644 --- a/web/src/pages/user-settings/tokens/UserTokenList.ts +++ b/web/src/pages/user-settings/tokens/UserTokenList.ts @@ -13,6 +13,7 @@ import { PAGE_SIZE } from "../../../constants"; import { CoreApi, IntentEnum, Token } from "@goauthentik/api"; import { DEFAULT_CONFIG } from "../../../api/Config"; import "./UserTokenForm"; +import { IntentToLabel } from "../../tokens/TokenListPage"; @customElement("ak-user-token-list") export class UserTokenList extends Table { @@ -100,6 +101,16 @@ export class UserTokenList extends Table { +
+
+ ${t`Intent`} +
+
+
+ ${IntentToLabel(item.intent || IntentEnum.Api)} +
+
+
From 4cf76fdcdaf6728a0d5a144497d53c1652c5d8ba Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 23 Aug 2021 16:38:46 +0200 Subject: [PATCH 05/22] stages/password: auto-enable app password backend Signed-off-by: Jens Langhammer --- .../migrations/0028_alter_token_intent.py | 26 ++++++++++ .../flows/migrations/0008_default_flows.py | 4 +- authentik/stages/password/__init__.py | 2 +- .../password/migrations/0007_app_password.py | 52 +++++++++++++++++++ authentik/stages/password/models.py | 4 +- 5 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 authentik/core/migrations/0028_alter_token_intent.py create mode 100644 authentik/stages/password/migrations/0007_app_password.py diff --git a/authentik/core/migrations/0028_alter_token_intent.py b/authentik/core/migrations/0028_alter_token_intent.py new file mode 100644 index 000000000..77fe3e0a1 --- /dev/null +++ b/authentik/core/migrations/0028_alter_token_intent.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.6 on 2021-08-23 14:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_core", "0027_bootstrap_token"), + ] + + operations = [ + migrations.AlterField( + model_name="token", + name="intent", + field=models.TextField( + choices=[ + ("verification", "Intent Verification"), + ("api", "Intent Api"), + ("recovery", "Intent Recovery"), + ("app_password", "Intent App Password"), + ], + default="verification", + ), + ), + ] diff --git a/authentik/flows/migrations/0008_default_flows.py b/authentik/flows/migrations/0008_default_flows.py index 8de070f80..e2498b6c5 100644 --- a/authentik/flows/migrations/0008_default_flows.py +++ b/authentik/flows/migrations/0008_default_flows.py @@ -6,7 +6,7 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor from authentik.flows.models import FlowDesignation from authentik.stages.identification.models import UserFields -from authentik.stages.password import BACKEND_DJANGO, BACKEND_LDAP +from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_DJANGO, BACKEND_LDAP def create_default_authentication_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): @@ -26,7 +26,7 @@ def create_default_authentication_flow(apps: Apps, schema_editor: BaseDatabaseSc password_stage, _ = PasswordStage.objects.using(db_alias).update_or_create( name="default-authentication-password", - defaults={"backends": [BACKEND_DJANGO, BACKEND_LDAP]}, + defaults={"backends": [BACKEND_DJANGO, BACKEND_LDAP, BACKEND_APP_PASSWORD]}, ) login_stage, _ = UserLoginStage.objects.using(db_alias).update_or_create( diff --git a/authentik/stages/password/__init__.py b/authentik/stages/password/__init__.py index 2bfdc92e1..fe333bd84 100644 --- a/authentik/stages/password/__init__.py +++ b/authentik/stages/password/__init__.py @@ -1,4 +1,4 @@ """Backend paths""" BACKEND_DJANGO = "django.contrib.auth.backends.ModelBackend" BACKEND_LDAP = "authentik.sources.ldap.auth.LDAPBackend" -BACKEND_TOKEN = "authentik.core.token_auth.TokenBackend" # nosec +BACKEND_APP_PASSWORD = "authentik.core.token_auth.TokenBackend" # nosec diff --git a/authentik/stages/password/migrations/0007_app_password.py b/authentik/stages/password/migrations/0007_app_password.py new file mode 100644 index 000000000..168424c95 --- /dev/null +++ b/authentik/stages/password/migrations/0007_app_password.py @@ -0,0 +1,52 @@ +# Generated by Django 3.2.6 on 2021-08-23 14:34 +import django.contrib.postgres.fields +from django.apps.registry import Apps +from django.db import migrations, models +from django.db.backends.base.schema import BaseDatabaseSchemaEditor + +from authentik.stages.password import BACKEND_APP_PASSWORD + + +def update_default_backends(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): + PasswordStage = apps.get_model("authentik_stages_password", "passwordstage") + db_alias = schema_editor.connection.alias + + stages = PasswordStage.objects.using(db_alias).filter(name="default-authentication-password") + if not stages.exists(): + return + stage = stages.first() + stage.backends.append(BACKEND_APP_PASSWORD) + stage.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_flows", "0008_default_flows"), + ("authentik_stages_password", "0006_passwordchange_rename"), + ] + + operations = [ + migrations.AlterField( + model_name="passwordstage", + name="backends", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.TextField( + choices=[ + ( + "django.contrib.auth.backends.ModelBackend", + "User database + standard password", + ), + ("authentik.core.token_auth.TokenBackend", "User database + app passwords"), + ( + "authentik.sources.ldap.auth.LDAPBackend", + "User database + LDAP password", + ), + ] + ), + help_text="Selection of backends to test the password against.", + size=None, + ), + ), + migrations.RunPython(update_default_backends), + ] diff --git a/authentik/stages/password/models.py b/authentik/stages/password/models.py index 66fd5d043..50932f29e 100644 --- a/authentik/stages/password/models.py +++ b/authentik/stages/password/models.py @@ -9,7 +9,7 @@ from rest_framework.serializers import BaseSerializer from authentik.core.types import UserSettingSerializer from authentik.flows.models import ConfigurableStage, Stage -from authentik.stages.password import BACKEND_DJANGO, BACKEND_LDAP, BACKEND_TOKEN +from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_DJANGO, BACKEND_LDAP def get_authentication_backends(): @@ -20,7 +20,7 @@ def get_authentication_backends(): _("User database + standard password"), ), ( - BACKEND_TOKEN, + BACKEND_APP_PASSWORD, _("User database + app passwords"), ), ( From 00e9b91f562edecb139785a6ec381737844e8a37 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 23 Aug 2021 16:47:38 +0200 Subject: [PATCH 06/22] web/admin: fix missing app passwords backend Signed-off-by: Jens Langhammer --- authentik/core/token_auth.py | 2 +- web/src/locales/en.po | 24 ++++++++++++++----- web/src/locales/pseudo-LOCALE.po | 24 ++++++++++++++----- .../stages/password/PasswordStageForm.ts | 12 ++++++++-- 4 files changed, 47 insertions(+), 15 deletions(-) diff --git a/authentik/core/token_auth.py b/authentik/core/token_auth.py index 1c95ce996..cc52ed116 100644 --- a/authentik/core/token_auth.py +++ b/authentik/core/token_auth.py @@ -26,4 +26,4 @@ class TokenBackend(ModelBackend): ) if not tokens.exists(): return None - return user + return tokens.first().user diff --git a/web/src/locales/en.po b/web/src/locales/en.po index c09f84d65..ac173bc2f 100644 --- a/web/src/locales/en.po +++ b/web/src/locales/en.po @@ -4763,6 +4763,18 @@ msgstr "User Reputation" msgid "User Settings" msgstr "User Settings" +#: src/pages/stages/password/PasswordStageForm.ts +msgid "User database + LDAP password" +msgstr "User database + LDAP password" + +#: src/pages/stages/password/PasswordStageForm.ts +msgid "User database + app passwords" +msgstr "User database + app passwords" + +#: src/pages/stages/password/PasswordStageForm.ts +msgid "User database + standard password" +msgstr "User database + standard password" + #: src/pages/user-settings/UserSettingsPage.ts msgid "User details" msgstr "User details" @@ -5049,13 +5061,13 @@ msgstr "You can only select providers that match the type of the outpost." msgid "You're currently impersonating {0}. Click to stop." msgstr "You're currently impersonating {0}. Click to stop." -#: src/pages/stages/password/PasswordStageForm.ts -msgid "authentik Builtin Database" -msgstr "authentik Builtin Database" +#: +#~ msgid "authentik Builtin Database" +#~ msgstr "authentik Builtin Database" -#: src/pages/stages/password/PasswordStageForm.ts -msgid "authentik LDAP Backend" -msgstr "authentik LDAP Backend" +#: +#~ msgid "authentik LDAP Backend" +#~ msgstr "authentik LDAP Backend" #: src/elements/forms/DeleteForm.ts msgid "connecting object will be deleted" diff --git a/web/src/locales/pseudo-LOCALE.po b/web/src/locales/pseudo-LOCALE.po index 36be57c10..1a9d0ae1f 100644 --- a/web/src/locales/pseudo-LOCALE.po +++ b/web/src/locales/pseudo-LOCALE.po @@ -4748,6 +4748,18 @@ msgstr "" msgid "User Settings" msgstr "" +#: src/pages/stages/password/PasswordStageForm.ts +msgid "User database + LDAP password" +msgstr "" + +#: src/pages/stages/password/PasswordStageForm.ts +msgid "User database + app passwords" +msgstr "" + +#: src/pages/stages/password/PasswordStageForm.ts +msgid "User database + standard password" +msgstr "" + #: src/pages/user-settings/UserSettingsPage.ts msgid "User details" msgstr "" @@ -5032,13 +5044,13 @@ msgstr "" msgid "You're currently impersonating {0}. Click to stop." msgstr "" -#: src/pages/stages/password/PasswordStageForm.ts -msgid "authentik Builtin Database" -msgstr "" +#: +#~ msgid "authentik Builtin Database" +#~ msgstr "" -#: src/pages/stages/password/PasswordStageForm.ts -msgid "authentik LDAP Backend" -msgstr "" +#: +#~ msgid "authentik LDAP Backend" +#~ msgstr "" #: src/elements/forms/DeleteForm.ts msgid "connecting object will be deleted" diff --git a/web/src/pages/stages/password/PasswordStageForm.ts b/web/src/pages/stages/password/PasswordStageForm.ts index 07152dfc4..dbe56340c 100644 --- a/web/src/pages/stages/password/PasswordStageForm.ts +++ b/web/src/pages/stages/password/PasswordStageForm.ts @@ -81,7 +81,15 @@ export class PasswordStageForm extends ModelForm { BackendsEnum.DjangoContribAuthBackendsModelBackend, )} > - ${t`authentik Builtin Database`} + ${t`User database + standard password`} + +

From 69a015361962d9ab1dd4d45a1bad1dca5b512c47 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 23 Aug 2021 17:07:06 +0200 Subject: [PATCH 07/22] core: use custom inbuilt backend, set backend login information in flow plan for events Signed-off-by: Jens Langhammer --- authentik/core/auth.py | 56 +++++++++++++++++++ authentik/core/sources/flow_manager.py | 4 +- authentik/core/token_auth.py | 29 ---------- authentik/events/signals.py | 5 ++ .../flows/migrations/0008_default_flows.py | 4 +- authentik/recovery/views.py | 4 +- authentik/root/settings.py | 3 +- authentik/sources/ldap/auth.py | 10 ++++ authentik/sources/ldap/settings.py | 4 -- authentik/sources/saml/processors/response.py | 6 +- authentik/stages/identification/tests.py | 6 +- authentik/stages/invitation/tests.py | 6 +- authentik/stages/password/__init__.py | 4 +- .../password/migrations/0007_app_password.py | 21 +++++-- authentik/stages/password/models.py | 4 +- authentik/stages/password/stage.py | 2 + authentik/stages/password/tests.py | 4 +- authentik/stages/user_login/stage.py | 4 +- authentik/stages/user_logout/tests.py | 4 +- 19 files changed, 115 insertions(+), 65 deletions(-) create mode 100644 authentik/core/auth.py delete mode 100644 authentik/core/token_auth.py diff --git a/authentik/core/auth.py b/authentik/core/auth.py new file mode 100644 index 000000000..42a84cf14 --- /dev/null +++ b/authentik/core/auth.py @@ -0,0 +1,56 @@ +"""Authenticate with tokens""" + +from typing import Any, Optional + +from django.contrib.auth.backends import ModelBackend +from django.http.request import HttpRequest + +from authentik.core.models import Token, TokenIntents, User +from authentik.flows.planner import FlowPlan +from authentik.flows.views import SESSION_KEY_PLAN +from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS + + +class InbuiltBackend(ModelBackend): + """Inbuilt backend""" + + def authenticate( + self, request: HttpRequest, username: Optional[str], password: Optional[str], **kwargs: Any + ) -> Optional[User]: + user = super().authenticate(request, username=username, password=password, **kwargs) + if not user: + return None + # Since we can't directly pass other variables to signals, and we want to log the method + # and the token used, we assume we're running in a flow and set a variable in the context + flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN] + flow_plan.context[PLAN_CONTEXT_METHOD] = "password" + request.session[SESSION_KEY_PLAN] = flow_plan + return user + + +class TokenBackend(ModelBackend): + """Authenticate with token""" + + def authenticate( + self, request: HttpRequest, username: Optional[str], password: Optional[str], **kwargs: Any + ) -> Optional[User]: + try: + user = User._default_manager.get_by_natural_key(username) + except User.DoesNotExist: + # Run the default password hasher once to reduce the timing + # difference between an existing and a nonexistent user (#20760). + User().set_password(password) + return None + tokens = Token.filter_not_expired( + user=user, key=password, intent=TokenIntents.INTENT_APP_PASSWORD + ) + if not tokens.exists(): + return None + token = tokens.first() + # Since we can't directly pass other variables to signals, and we want to log the method + # and the token used, we assume we're running in a flow and set a variable in the context + flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN] + flow_plan.context[PLAN_CONTEXT_METHOD] = "app_password" + flow_plan.context[PLAN_CONTEXT_METHOD_ARGS] = {"token": token} + request.session[SESSION_KEY_PLAN] = flow_plan + return token.user diff --git a/authentik/core/sources/flow_manager.py b/authentik/core/sources/flow_manager.py index 7666da07f..0cac12440 100644 --- a/authentik/core/sources/flow_manager.py +++ b/authentik/core/sources/flow_manager.py @@ -25,7 +25,7 @@ from authentik.flows.planner import ( from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN from authentik.lib.utils.urls import redirect_with_qs from authentik.policies.utils import delete_none_keys -from authentik.stages.password import BACKEND_DJANGO +from authentik.stages.password import BACKEND_INBUILT from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT @@ -189,7 +189,7 @@ class SourceFlowManager: kwargs.update( { # Since we authenticate the user by their token, they have no backend set - PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_DJANGO, + PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_INBUILT, PLAN_CONTEXT_SSO: True, PLAN_CONTEXT_SOURCE: self.source, PLAN_CONTEXT_REDIRECT: final_redirect, diff --git a/authentik/core/token_auth.py b/authentik/core/token_auth.py deleted file mode 100644 index cc52ed116..000000000 --- a/authentik/core/token_auth.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Authenticate with tokens""" - -from typing import Any, Optional - -from django.contrib.auth.backends import ModelBackend -from django.http.request import HttpRequest - -from authentik.core.models import Token, TokenIntents, User - - -class TokenBackend(ModelBackend): - """Authenticate with token""" - - def authenticate( - self, request: HttpRequest, username: Optional[str], password: Optional[str], **kwargs: Any - ) -> Optional[User]: - try: - user = User._default_manager.get_by_natural_key(username) - except User.DoesNotExist: - # Run the default password hasher once to reduce the timing - # difference between an existing and a nonexistent user (#20760). - User().set_password(password) - return None - tokens = Token.filter_not_expired( - user=user, key=password, intent=TokenIntents.INTENT_APP_PASSWORD - ) - if not tokens.exists(): - return None - return tokens.first().user diff --git a/authentik/events/signals.py b/authentik/events/signals.py index 7a4b5a032..be46da3d4 100644 --- a/authentik/events/signals.py +++ b/authentik/events/signals.py @@ -15,6 +15,7 @@ from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan from authentik.flows.views import SESSION_KEY_PLAN from authentik.stages.invitation.models import Invitation from authentik.stages.invitation.signals import invitation_used +from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS from authentik.stages.user_write.signals import user_write @@ -47,6 +48,10 @@ def on_user_logged_in(sender, request: HttpRequest, user: User, **_): if PLAN_CONTEXT_SOURCE in flow_plan.context: # Login request came from an external source, save it in the context thread.kwargs["using_source"] = flow_plan.context[PLAN_CONTEXT_SOURCE] + if PLAN_CONTEXT_METHOD in flow_plan.context: + thread.kwargs["method"] = flow_plan.context[PLAN_CONTEXT_METHOD] + # Save the login method used + thread.kwargs["method_args"] = flow_plan.context.get(PLAN_CONTEXT_METHOD_ARGS, {}) thread.user = user thread.run() diff --git a/authentik/flows/migrations/0008_default_flows.py b/authentik/flows/migrations/0008_default_flows.py index e2498b6c5..d507f209c 100644 --- a/authentik/flows/migrations/0008_default_flows.py +++ b/authentik/flows/migrations/0008_default_flows.py @@ -6,7 +6,7 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor from authentik.flows.models import FlowDesignation from authentik.stages.identification.models import UserFields -from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_DJANGO, BACKEND_LDAP +from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP def create_default_authentication_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): @@ -26,7 +26,7 @@ def create_default_authentication_flow(apps: Apps, schema_editor: BaseDatabaseSc password_stage, _ = PasswordStage.objects.using(db_alias).update_or_create( name="default-authentication-password", - defaults={"backends": [BACKEND_DJANGO, BACKEND_LDAP, BACKEND_APP_PASSWORD]}, + defaults={"backends": [BACKEND_INBUILT, BACKEND_LDAP, BACKEND_APP_PASSWORD]}, ) login_stage, _ = UserLoginStage.objects.using(db_alias).update_or_create( diff --git a/authentik/recovery/views.py b/authentik/recovery/views.py index 27b0023af..3253ab332 100644 --- a/authentik/recovery/views.py +++ b/authentik/recovery/views.py @@ -7,7 +7,7 @@ from django.utils.translation import gettext as _ from django.views import View from authentik.core.models import Token, TokenIntents -from authentik.stages.password import BACKEND_DJANGO +from authentik.stages.password import BACKEND_INBUILT class UseTokenView(View): @@ -19,7 +19,7 @@ class UseTokenView(View): if not tokens.exists(): raise Http404 token = tokens.first() - login(request, token.user, backend=BACKEND_DJANGO) + login(request, token.user, backend=BACKEND_INBUILT) token.delete() messages.warning(request, _("Used recovery-link to authenticate.")) return redirect("authentik_core:if-admin") diff --git a/authentik/root/settings.py b/authentik/root/settings.py index ab3f59cc5..08dc5697b 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -73,7 +73,8 @@ LANGUAGE_COOKIE_NAME = f"authentik_language{_cookie_suffix}" SESSION_COOKIE_NAME = f"authentik_session{_cookie_suffix}" AUTHENTICATION_BACKENDS = [ - "django.contrib.auth.backends.ModelBackend", + "authentik.core.auth.InbuiltBackend", + "authentik.core.auth.TokenBackend", "guardian.backends.ObjectPermissionBackend", ] diff --git a/authentik/sources/ldap/auth.py b/authentik/sources/ldap/auth.py index 9cf55d36d..10bd55b40 100644 --- a/authentik/sources/ldap/auth.py +++ b/authentik/sources/ldap/auth.py @@ -7,7 +7,10 @@ from django.http import HttpRequest from structlog.stdlib import get_logger from authentik.core.models import User +from authentik.flows.planner import FlowPlan +from authentik.flows.views import SESSION_KEY_PLAN from authentik.sources.ldap.models import LDAPSource +from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS LOGGER = get_logger() LDAP_DISTINGUISHED_NAME = "distinguishedName" @@ -24,6 +27,13 @@ class LDAPBackend(ModelBackend): LOGGER.debug("LDAP Auth attempt", source=source) user = self.auth_user(source, **kwargs) if user: + # Since we can't directly pass other variables to signals, and we want to log + # the method and the token used, we assume we're running in a flow and + # set a variable in the context + flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN] + flow_plan.context[PLAN_CONTEXT_METHOD] = "ldap" + flow_plan.context[PLAN_CONTEXT_METHOD_ARGS] = {"source": source} + request.session[SESSION_KEY_PLAN] = flow_plan return user return None diff --git a/authentik/sources/ldap/settings.py b/authentik/sources/ldap/settings.py index 850e9a04d..d3b90e901 100644 --- a/authentik/sources/ldap/settings.py +++ b/authentik/sources/ldap/settings.py @@ -1,10 +1,6 @@ """LDAP Settings""" from celery.schedules import crontab -AUTHENTICATION_BACKENDS = [ - "authentik.sources.ldap.auth.LDAPBackend", -] - CELERY_BEAT_SCHEDULE = { "sources_ldap_sync": { "task": "authentik.sources.ldap.tasks.ldap_sync_all", diff --git a/authentik/sources/saml/processors/response.py b/authentik/sources/saml/processors/response.py index 0e41b44c8..c2537770c 100644 --- a/authentik/sources/saml/processors/response.py +++ b/authentik/sources/saml/processors/response.py @@ -39,7 +39,7 @@ from authentik.sources.saml.processors.constants import ( from authentik.sources.saml.processors.request import SESSION_REQUEST_ID from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT -from authentik.stages.user_login.stage import BACKEND_DJANGO +from authentik.stages.user_login.stage import BACKEND_INBUILT LOGGER = get_logger() if TYPE_CHECKING: @@ -136,7 +136,7 @@ class ResponseProcessor: self._source.authentication_flow, **{ PLAN_CONTEXT_PENDING_USER: user, - PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_DJANGO, + PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_INBUILT, }, ) @@ -199,7 +199,7 @@ class ResponseProcessor: self._source.authentication_flow, **{ PLAN_CONTEXT_PENDING_USER: matching_users.first(), - PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_DJANGO, + PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_INBUILT, PLAN_CONTEXT_REDIRECT: final_redirect, }, ) diff --git a/authentik/stages/identification/tests.py b/authentik/stages/identification/tests.py index c9d5e267f..fb2beeabe 100644 --- a/authentik/stages/identification/tests.py +++ b/authentik/stages/identification/tests.py @@ -9,7 +9,7 @@ from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.providers.oauth2.generators import generate_client_secret from authentik.sources.oauth.models import OAuthSource from authentik.stages.identification.models import IdentificationStage, UserFields -from authentik.stages.password import BACKEND_DJANGO +from authentik.stages.password import BACKEND_INBUILT from authentik.stages.password.models import PasswordStage @@ -68,7 +68,7 @@ class TestIdentificationStage(TestCase): def test_valid_with_password(self): """Test with valid email and password in single step""" - pw_stage = PasswordStage.objects.create(name="password", backends=[BACKEND_DJANGO]) + pw_stage = PasswordStage.objects.create(name="password", backends=[BACKEND_INBUILT]) self.stage.password_stage = pw_stage self.stage.save() form_data = {"uid_field": self.user.email, "password": self.password} @@ -86,7 +86,7 @@ class TestIdentificationStage(TestCase): def test_invalid_with_password(self): """Test with valid email and invalid password in single step""" - pw_stage = PasswordStage.objects.create(name="password", backends=[BACKEND_DJANGO]) + pw_stage = PasswordStage.objects.create(name="password", backends=[BACKEND_INBUILT]) self.stage.password_stage = pw_stage self.stage.save() form_data = { diff --git a/authentik/stages/invitation/tests.py b/authentik/stages/invitation/tests.py index 45d0c97f5..6f84dca7b 100644 --- a/authentik/stages/invitation/tests.py +++ b/authentik/stages/invitation/tests.py @@ -17,7 +17,7 @@ from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK from authentik.flows.views import SESSION_KEY_PLAN from authentik.stages.invitation.models import Invitation, InvitationStage from authentik.stages.invitation.stage import INVITATION_TOKEN_KEY, PLAN_CONTEXT_PROMPT -from authentik.stages.password import BACKEND_DJANGO +from authentik.stages.password import BACKEND_INBUILT from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND @@ -45,7 +45,7 @@ class TestUserLoginStage(TestCase): """Test without any invitation, continue_flow_without_invitation not set.""" plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user - plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO + plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() @@ -74,7 +74,7 @@ class TestUserLoginStage(TestCase): self.stage.save() plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user - plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO + plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() diff --git a/authentik/stages/password/__init__.py b/authentik/stages/password/__init__.py index fe333bd84..d5c73cc96 100644 --- a/authentik/stages/password/__init__.py +++ b/authentik/stages/password/__init__.py @@ -1,4 +1,4 @@ """Backend paths""" -BACKEND_DJANGO = "django.contrib.auth.backends.ModelBackend" +BACKEND_INBUILT = "authentik.core.auth.InbuiltBackend" BACKEND_LDAP = "authentik.sources.ldap.auth.LDAPBackend" -BACKEND_APP_PASSWORD = "authentik.core.token_auth.TokenBackend" # nosec +BACKEND_APP_PASSWORD = "authentik.core.auth.TokenBackend" # nosec diff --git a/authentik/stages/password/migrations/0007_app_password.py b/authentik/stages/password/migrations/0007_app_password.py index 168424c95..349758219 100644 --- a/authentik/stages/password/migrations/0007_app_password.py +++ b/authentik/stages/password/migrations/0007_app_password.py @@ -4,7 +4,7 @@ from django.apps.registry import Apps from django.db import migrations, models from django.db.backends.base.schema import BaseDatabaseSchemaEditor -from authentik.stages.password import BACKEND_APP_PASSWORD +from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT def update_default_backends(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): @@ -19,6 +19,18 @@ def update_default_backends(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) stage.save() +def replace_inbuilt(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): + PasswordStage = apps.get_model("authentik_stages_password", "passwordstage") + db_alias = schema_editor.connection.alias + + for stage in PasswordStage.objects.using(db_alias).all(): + if "django.contrib.auth.backends.ModelBackend" not in stage.backends: + continue + stage.backends.remove("django.contrib.auth.backends.ModelBackend") + stage.backends.append(BACKEND_INBUILT) + stage.save() + + class Migration(migrations.Migration): dependencies = [ @@ -33,11 +45,8 @@ class Migration(migrations.Migration): field=django.contrib.postgres.fields.ArrayField( base_field=models.TextField( choices=[ - ( - "django.contrib.auth.backends.ModelBackend", - "User database + standard password", - ), - ("authentik.core.token_auth.TokenBackend", "User database + app passwords"), + ("authentik.core.auth.InbuiltBackend", "User database + standard password"), + ("authentik.core.auth.TokenBackend", "User database + app passwords"), ( "authentik.sources.ldap.auth.LDAPBackend", "User database + LDAP password", diff --git a/authentik/stages/password/models.py b/authentik/stages/password/models.py index 50932f29e..5369052a2 100644 --- a/authentik/stages/password/models.py +++ b/authentik/stages/password/models.py @@ -9,14 +9,14 @@ from rest_framework.serializers import BaseSerializer from authentik.core.types import UserSettingSerializer from authentik.flows.models import ConfigurableStage, Stage -from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_DJANGO, BACKEND_LDAP +from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP def get_authentication_backends(): """Return all available authentication backends as tuple set""" return [ ( - BACKEND_DJANGO, + BACKEND_INBUILT, _("User database + standard password"), ), ( diff --git a/authentik/stages/password/stage.py b/authentik/stages/password/stage.py index f5cf96c22..aaef61618 100644 --- a/authentik/stages/password/stage.py +++ b/authentik/stages/password/stage.py @@ -27,6 +27,8 @@ from authentik.stages.password.models import PasswordStage LOGGER = get_logger() PLAN_CONTEXT_AUTHENTICATION_BACKEND = "user_backend" +PLAN_CONTEXT_METHOD = "method" +PLAN_CONTEXT_METHOD_ARGS = "method_args" SESSION_INVALID_TRIES = "user_invalid_tries" diff --git a/authentik/stages/password/tests.py b/authentik/stages/password/tests.py index 39a191098..b30a3187c 100644 --- a/authentik/stages/password/tests.py +++ b/authentik/stages/password/tests.py @@ -14,7 +14,7 @@ from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK from authentik.flows.views import SESSION_KEY_PLAN from authentik.providers.oauth2.generators import generate_client_secret -from authentik.stages.password import BACKEND_DJANGO +from authentik.stages.password import BACKEND_INBUILT from authentik.stages.password.models import PasswordStage MOCK_BACKEND_AUTHENTICATE = MagicMock(side_effect=PermissionDenied("test")) @@ -36,7 +36,7 @@ class TestPasswordStage(TestCase): slug="test-password", designation=FlowDesignation.AUTHENTICATION, ) - self.stage = PasswordStage.objects.create(name="password", backends=[BACKEND_DJANGO]) + self.stage = PasswordStage.objects.create(name="password", backends=[BACKEND_INBUILT]) self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) @patch( diff --git a/authentik/stages/user_login/stage.py b/authentik/stages/user_login/stage.py index 773ccc3ab..9de80233d 100644 --- a/authentik/stages/user_login/stage.py +++ b/authentik/stages/user_login/stage.py @@ -8,7 +8,7 @@ from structlog.stdlib import get_logger from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER from authentik.flows.stage import StageView from authentik.lib.utils.time import timedelta_from_string -from authentik.stages.password import BACKEND_DJANGO +from authentik.stages.password import BACKEND_INBUILT from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND LOGGER = get_logger() @@ -26,7 +26,7 @@ class UserLoginStageView(StageView): LOGGER.debug(message) return self.executor.stage_invalid() backend = self.executor.plan.context.get( - PLAN_CONTEXT_AUTHENTICATION_BACKEND, BACKEND_DJANGO + PLAN_CONTEXT_AUTHENTICATION_BACKEND, BACKEND_INBUILT ) login( self.request, diff --git a/authentik/stages/user_logout/tests.py b/authentik/stages/user_logout/tests.py index 2958e3f97..cf2ec16bd 100644 --- a/authentik/stages/user_logout/tests.py +++ b/authentik/stages/user_logout/tests.py @@ -9,7 +9,7 @@ from authentik.flows.markers import StageMarker from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan from authentik.flows.views import SESSION_KEY_PLAN -from authentik.stages.password import BACKEND_DJANGO +from authentik.stages.password import BACKEND_INBUILT from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND from authentik.stages.user_logout.models import UserLogoutStage @@ -34,7 +34,7 @@ class TestUserLogoutStage(TestCase): """Test with a valid pending user and backend""" plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user - plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO + plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() From 07a4f474f4531a2484d94b256c5513e8a1d9bedb Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 23 Aug 2021 17:23:55 +0200 Subject: [PATCH 08/22] website/docs: add docs for `auth_method` and `auth_method_args` fields Signed-off-by: Jens Langhammer --- authentik/events/signals.py | 6 ++-- authentik/stages/password/stage.py | 4 +-- schema.yml | 4 +-- .../stages/password/PasswordStageForm.ts | 12 ++++---- website/docs/policies/expression.mdx | 28 +++++++++++++++++++ 5 files changed, 41 insertions(+), 13 deletions(-) diff --git a/authentik/events/signals.py b/authentik/events/signals.py index be46da3d4..35f651e53 100644 --- a/authentik/events/signals.py +++ b/authentik/events/signals.py @@ -47,11 +47,11 @@ def on_user_logged_in(sender, request: HttpRequest, user: User, **_): flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN] if PLAN_CONTEXT_SOURCE in flow_plan.context: # Login request came from an external source, save it in the context - thread.kwargs["using_source"] = flow_plan.context[PLAN_CONTEXT_SOURCE] + thread.kwargs[PLAN_CONTEXT_SOURCE] = flow_plan.context[PLAN_CONTEXT_SOURCE] if PLAN_CONTEXT_METHOD in flow_plan.context: - thread.kwargs["method"] = flow_plan.context[PLAN_CONTEXT_METHOD] + thread.kwargs[PLAN_CONTEXT_METHOD] = flow_plan.context[PLAN_CONTEXT_METHOD] # Save the login method used - thread.kwargs["method_args"] = flow_plan.context.get(PLAN_CONTEXT_METHOD_ARGS, {}) + thread.kwargs[PLAN_CONTEXT_METHOD_ARGS] = flow_plan.context.get(PLAN_CONTEXT_METHOD_ARGS, {}) thread.user = user thread.run() diff --git a/authentik/stages/password/stage.py b/authentik/stages/password/stage.py index aaef61618..646f68bdf 100644 --- a/authentik/stages/password/stage.py +++ b/authentik/stages/password/stage.py @@ -27,8 +27,8 @@ from authentik.stages.password.models import PasswordStage LOGGER = get_logger() PLAN_CONTEXT_AUTHENTICATION_BACKEND = "user_backend" -PLAN_CONTEXT_METHOD = "method" -PLAN_CONTEXT_METHOD_ARGS = "method_args" +PLAN_CONTEXT_METHOD = "auth_method" +PLAN_CONTEXT_METHOD_ARGS = "auth_method_args" SESSION_INVALID_TRIES = "user_invalid_tries" diff --git a/schema.yml b/schema.yml index 38587c822..421488100 100644 --- a/schema.yml +++ b/schema.yml @@ -20382,8 +20382,8 @@ components: - url BackendsEnum: enum: - - django.contrib.auth.backends.ModelBackend - - authentik.core.token_auth.TokenBackend + - authentik.core.auth.InbuiltBackend + - authentik.core.auth.TokenBackend - authentik.sources.ldap.auth.LDAPBackend type: string BindingTypeEnum: diff --git a/web/src/pages/stages/password/PasswordStageForm.ts b/web/src/pages/stages/password/PasswordStageForm.ts index dbe56340c..acbcebc10 100644 --- a/web/src/pages/stages/password/PasswordStageForm.ts +++ b/web/src/pages/stages/password/PasswordStageForm.ts @@ -76,25 +76,25 @@ export class PasswordStageForm extends ModelForm { >