From 0d207bb2acb2875b316099b74bc5f8632f1cdc1d Mon Sep 17 00:00:00 2001 From: Dominic R Date: Sun, 13 Sep 2026 20:35:09 -0400 Subject: [PATCH 1/6] stages: use managed secrets for email and CAPTCHA --- ...0007_preserve_email_captcha_permissions.py | 35 ++++++++++ authentik/stages/authenticator_email/api.py | 20 +++++- .../0005_authenticatoremailstage_secret.py | 66 +++++++++++++++++++ .../stages/authenticator_email/models.py | 15 ++++- authentik/stages/captcha/api.py | 6 +- .../migrations/0006_captchastage_secret.py | 57 ++++++++++++++++ authentik/stages/captcha/models.py | 16 ++++- authentik/stages/captcha/stage.py | 2 +- authentik/stages/captcha/tests.py | 16 ++++- authentik/stages/email/api.py | 3 +- .../migrations/0007_emailstage_secret.py | 54 +++++++++++++++ authentik/stages/email/models.py | 15 ++++- .../email/tests/test_management_commands.py | 3 +- authentik/stages/identification/tests.py | 9 +-- .../flows-login-conditional-captcha.yaml | 8 ++- blueprints/schema.json | 14 ++-- packages/client-ts/src/apis/StagesApi.ts | 16 ----- .../src/models/AuthenticatorEmailStage.ts | 11 ++++ .../models/AuthenticatorEmailStageRequest.ts | 11 +++- packages/client-ts/src/models/CaptchaStage.ts | 7 ++ .../src/models/CaptchaStageRequest.ts | 14 ++-- packages/client-ts/src/models/EmailStage.ts | 11 ++++ .../client-ts/src/models/EmailStageRequest.ts | 11 +++- .../PatchedAuthenticatorEmailStageRequest.ts | 11 +++- .../src/models/PatchedCaptchaStageRequest.ts | 6 +- .../src/models/PatchedEmailStageRequest.ts | 11 +++- schema.yml | 63 +++++++++++------- 27 files changed, 419 insertions(+), 92 deletions(-) create mode 100644 authentik/crypto/secrets/migrations/0007_preserve_email_captcha_permissions.py create mode 100644 authentik/stages/authenticator_email/migrations/0005_authenticatoremailstage_secret.py create mode 100644 authentik/stages/captcha/migrations/0006_captchastage_secret.py create mode 100644 authentik/stages/email/migrations/0007_emailstage_secret.py diff --git a/authentik/crypto/secrets/migrations/0007_preserve_email_captcha_permissions.py b/authentik/crypto/secrets/migrations/0007_preserve_email_captcha_permissions.py new file mode 100644 index 000000000000..3d25a83d5d96 --- /dev/null +++ b/authentik/crypto/secrets/migrations/0007_preserve_email_captcha_permissions.py @@ -0,0 +1,35 @@ +# Generated by Django 5.2.17 on 2026-09-14 00:33 + +from importlib import import_module + +from django.db import migrations + + +def preserve_permissions(apps, schema_editor): + migration = import_module("authentik.crypto.secrets.migrations.0002_preserve_role_permissions") + migration.preserve_role_permissions( + apps, + schema_editor, + [ + consumer + for consumer in migration.CONSUMERS + if consumer[0] + in [ + "authentik_stages_authenticator_email", + "authentik_stages_email", + "authentik_stages_captcha", + ] + ], + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_stages_authenticator_email", "0005_authenticatoremailstage_secret"), + ("authentik_stages_captcha", "0006_captchastage_secret"), + ("authentik_stages_email", "0007_emailstage_secret"), + ("authentik_secrets", "0006_preserve_mfa_stage_permissions"), + ] + + operations = [migrations.RunPython(preserve_permissions, migrations.RunPython.noop)] diff --git a/authentik/stages/authenticator_email/api.py b/authentik/stages/authenticator_email/api.py index d0e06256af69..5ddae8c7e169 100644 --- a/authentik/stages/authenticator_email/api.py +++ b/authentik/stages/authenticator_email/api.py @@ -22,7 +22,7 @@ class Meta: "host", "port", "username", - "password", + "secret", "use_tls", "use_ssl", "timeout", @@ -31,7 +31,6 @@ class Meta: "token_expiry", "template", ] - extra_kwargs = {"password": {"write_only": True}} class AuthenticatorEmailStageViewSet(UsedByMixin, ModelViewSet): @@ -39,7 +38,22 @@ class AuthenticatorEmailStageViewSet(UsedByMixin, ModelViewSet): queryset = AuthenticatorEmailStage.objects.all() serializer_class = AuthenticatorEmailStageSerializer - filterset_fields = "__all__" + filterset_fields = [ + "name", + "configure_flow", + "friendly_name", + "use_global_settings", + "host", + "port", + "username", + "use_tls", + "use_ssl", + "timeout", + "from_address", + "subject", + "token_expiry", + "template", + ] ordering = ["name"] search_fields = ["name"] diff --git a/authentik/stages/authenticator_email/migrations/0005_authenticatoremailstage_secret.py b/authentik/stages/authenticator_email/migrations/0005_authenticatoremailstage_secret.py new file mode 100644 index 000000000000..149271df1437 --- /dev/null +++ b/authentik/stages/authenticator_email/migrations/0005_authenticatoremailstage_secret.py @@ -0,0 +1,66 @@ +# Generated by Django 5.2.17 on 2026-08-30 21:32 + +import django.db.models.deletion +from django.db import migrations, models + +from authentik.crypto.secrets.migrations._credential_values import ( + migrate_credentials, + restore_credentials, +) + +FIELDS = [("_password", "secret", None, "SMTP password")] + + +def migrate_password(apps, schema_editor): + migrate_credentials( + apps, + schema_editor, + "authentik_stages_authenticator_email", + "AuthenticatorEmailStage", + FIELDS, + ) + + +def rollback_password(apps, schema_editor): + restore_credentials( + apps, + schema_editor, + "authentik_stages_authenticator_email", + "AuthenticatorEmailStage", + FIELDS, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_secrets", "0001_initial"), + ("authentik_stages_authenticator_email", "0004_alter_emaildevice_stage"), + ] + + operations = [ + migrations.AlterField( + model_name="authenticatoremailstage", + name="password", + field=models.TextField(blank=True, db_column="password", default=""), + ), + migrations.RenameField( + model_name="authenticatoremailstage", + old_name="password", + new_name="_password", + ), + migrations.AddField( + model_name="authenticatoremailstage", + name="secret", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="authenticator_email_stages", + to="authentik_secrets.secret", + verbose_name="SMTP password", + ), + ), + migrations.RunPython(migrate_password, rollback_password), + ] diff --git a/authentik/stages/authenticator_email/models.py b/authentik/stages/authenticator_email/models.py index 8022fc8b2044..08dc60e21e47 100644 --- a/authentik/stages/authenticator_email/models.py +++ b/authentik/stages/authenticator_email/models.py @@ -22,6 +22,9 @@ class AuthenticatorEmailStage(ConfigurableStage, FriendlyNamedStage, Stage): """Setup Email-based authentication for the user.""" + # Remove the legacy credential columns in 2027.2. + _password = models.TextField(blank=True, db_column="password", default="") + use_global_settings = models.BooleanField( default=False, help_text=_( @@ -33,7 +36,15 @@ class AuthenticatorEmailStage(ConfigurableStage, FriendlyNamedStage, Stage): host = models.TextField(default="localhost") port = models.IntegerField(default=25) username = models.TextField(default="", blank=True) - password = models.TextField(default="", blank=True) + secret = models.ForeignKey( + "authentik_secrets.Secret", + verbose_name=_("SMTP password"), + on_delete=models.PROTECT, + null=True, + blank=True, + default=None, + related_name="authenticator_email_stages", + ) use_tls = models.BooleanField(default=False) use_ssl = models.BooleanField(default=False) timeout = models.IntegerField(default=10) @@ -94,7 +105,7 @@ def backend(self) -> BaseEmailBackend: host=self.host, port=self.port, username=self.username, - password=self.password, + password=self.secret.value if self.secret else "", use_tls=self.use_tls, use_ssl=self.use_ssl, timeout=self.timeout, diff --git a/authentik/stages/captcha/api.py b/authentik/stages/captcha/api.py index 959d30df00af..9124d7334062 100644 --- a/authentik/stages/captcha/api.py +++ b/authentik/stages/captcha/api.py @@ -14,7 +14,7 @@ class Meta: model = CaptchaStage fields = StageSerializer.Meta.fields + [ "public_key", - "private_key", + "secret", "js_url", "api_url", "request_content_type", @@ -23,7 +23,9 @@ class Meta: "score_max_threshold", "error_on_invalid_score", ] - extra_kwargs = {"private_key": {"write_only": True}} + extra_kwargs = { + "secret": {"required": True, "allow_null": False}, + } class CaptchaStageViewSet(UsedByMixin, ModelViewSet): diff --git a/authentik/stages/captcha/migrations/0006_captchastage_secret.py b/authentik/stages/captcha/migrations/0006_captchastage_secret.py new file mode 100644 index 000000000000..1fd1c16c7023 --- /dev/null +++ b/authentik/stages/captcha/migrations/0006_captchastage_secret.py @@ -0,0 +1,57 @@ +# Generated by Django 5.2.17 on 2026-08-30 21:31 + +import django.db.models.deletion +from django.db import migrations, models + +from authentik.crypto.secrets.migrations._credential_values import ( + migrate_credentials, + restore_credentials, +) + +FIELDS = [("_private_key", "secret", None, "captcha private key")] + + +def migrate_private_key(apps, schema_editor): + migrate_credentials(apps, schema_editor, "authentik_stages_captcha", "CaptchaStage", FIELDS) + + +def rollback_private_key(apps, schema_editor): + restore_credentials(apps, schema_editor, "authentik_stages_captcha", "CaptchaStage", FIELDS) + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_secrets", "0001_initial"), + ("authentik_stages_captcha", "0005_captchastage_request_content_type"), + ] + + operations = [ + migrations.AlterField( + model_name="captchastage", + name="private_key", + field=models.TextField( + db_column="private_key", help_text="Private key, acquired your captcha Provider." + ), + ), + migrations.RenameField( + model_name="captchastage", + old_name="private_key", + new_name="_private_key", + ), + migrations.AddField( + model_name="captchastage", + name="secret", + field=models.ForeignKey( + blank=True, + default=None, + help_text="Private key, acquired your captcha Provider.", + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="captcha_stages", + to="authentik_secrets.secret", + verbose_name="Private key", + ), + ), + migrations.RunPython(migrate_private_key, rollback_private_key), + ] diff --git a/authentik/stages/captcha/models.py b/authentik/stages/captcha/models.py index 776911b91ead..49d631ba6c15 100644 --- a/authentik/stages/captcha/models.py +++ b/authentik/stages/captcha/models.py @@ -18,8 +18,22 @@ class CaptchaRequestContentType(models.TextChoices): class CaptchaStage(Stage): """Verify the user is human using Google's reCaptcha/other compatible CAPTCHA solutions.""" + # Remove the legacy credential columns in 2027.2. + _private_key = models.TextField( + db_column="private_key", help_text=_("Private key, acquired your captcha Provider.") + ) + public_key = models.TextField(help_text=_("Public key, acquired your captcha Provider.")) - private_key = models.TextField(help_text=_("Private key, acquired your captcha Provider.")) + secret = models.ForeignKey( + "authentik_secrets.Secret", + verbose_name=_("Private key"), + help_text=_("Private key, acquired your captcha Provider."), + on_delete=models.PROTECT, + null=True, + blank=True, + default=None, + related_name="captcha_stages", + ) interactive = models.BooleanField(default=False) diff --git a/authentik/stages/captcha/stage.py b/authentik/stages/captcha/stage.py index 66b823e7a771..2475a6298081 100644 --- a/authentik/stages/captcha/stage.py +++ b/authentik/stages/captcha/stage.py @@ -36,7 +36,7 @@ class CaptchaChallenge(WithUserInfoChallenge): def verify_captcha_token(stage: CaptchaStage, token: str, remote_ip: str, key: str | None = None): """Validate captcha token""" payload = { - "secret": key or stage.private_key, + "secret": key or stage.secret.value, "response": token, "remoteip": remote_ip, } diff --git a/authentik/stages/captcha/tests.py b/authentik/stages/captcha/tests.py index 39a7b4a32e57..ac91e24b999c 100644 --- a/authentik/stages/captcha/tests.py +++ b/authentik/stages/captcha/tests.py @@ -4,6 +4,7 @@ from requests_mock import Mocker from authentik.core.tests.utils import create_test_admin_user, create_test_flow +from authentik.crypto.secrets.tests.utils import create_test_secret from authentik.flows.markers import StageMarker from authentik.flows.models import FlowDesignation, FlowStageBinding from authentik.flows.planner import FlowPlan @@ -32,10 +33,21 @@ def setUp(self): self.stage: CaptchaStage = CaptchaStage.objects.create( name="captcha", public_key=RECAPTCHA_PUBLIC_KEY, - private_key=RECAPTCHA_PRIVATE_KEY, + secret=create_test_secret(RECAPTCHA_PRIVATE_KEY), ) self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) + def test_api_requires_secret(self): + """Creating a stage without a secret reference must fail, matching the + previously-required private_key""" + self.client.force_login(self.user) + response = self.client.post( + reverse("authentik_api:captchastage-list"), + data={"name": generate_id(), "public_key": RECAPTCHA_PUBLIC_KEY}, + ) + self.assertEqual(response.status_code, 400) + self.assertIn("secret", response.json()) + @Mocker() def test_valid(self, mock: Mocker): """Test valid captcha""" @@ -93,7 +105,7 @@ def test_valid_json_content_type(self, mock: Mocker): @Mocker() def test_valid_override(self, mock: Mocker): """Test valid captcha""" - self.stage.private_key = generate_id() + self.stage.secret.replace_value(generate_id()) self.stage.public_key = generate_id() mock.post( "https://www.recaptcha.net/recaptcha/api/siteverify", diff --git a/authentik/stages/email/api.py b/authentik/stages/email/api.py index 5c9814255a04..d2173a6b6257 100644 --- a/authentik/stages/email/api.py +++ b/authentik/stages/email/api.py @@ -35,7 +35,7 @@ class Meta: "host", "port", "username", - "password", + "secret", "use_tls", "use_ssl", "timeout", @@ -47,7 +47,6 @@ class Meta: "recovery_max_attempts", "recovery_cache_timeout", ] - extra_kwargs = {"password": {"write_only": True}} class EmailStageViewSet(UsedByMixin, ModelViewSet): diff --git a/authentik/stages/email/migrations/0007_emailstage_secret.py b/authentik/stages/email/migrations/0007_emailstage_secret.py new file mode 100644 index 000000000000..4a178457dba5 --- /dev/null +++ b/authentik/stages/email/migrations/0007_emailstage_secret.py @@ -0,0 +1,54 @@ +# Generated by Django 5.2.17 on 2026-08-30 21:32 + +import django.db.models.deletion +from django.db import migrations, models + +from authentik.crypto.secrets.migrations._credential_values import ( + migrate_credentials, + restore_credentials, +) + +FIELDS = [("_password", "secret", None, "SMTP password")] + + +def migrate_password(apps, schema_editor): + migrate_credentials(apps, schema_editor, "authentik_stages_email", "EmailStage", FIELDS) + + +def rollback_password(apps, schema_editor): + restore_credentials(apps, schema_editor, "authentik_stages_email", "EmailStage", FIELDS) + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_secrets", "0001_initial"), + ("authentik_stages_email", "0006_emailstage_recovery_cache_timeout_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="emailstage", + name="password", + field=models.TextField(blank=True, db_column="password", default=""), + ), + migrations.RenameField( + model_name="emailstage", + old_name="password", + new_name="_password", + ), + migrations.AddField( + model_name="emailstage", + name="secret", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="email_stages", + to="authentik_secrets.secret", + verbose_name="SMTP password", + ), + ), + migrations.RunPython(migrate_password, rollback_password), + ] diff --git a/authentik/stages/email/models.py b/authentik/stages/email/models.py index 41fccbc05361..6708db4728b0 100644 --- a/authentik/stages/email/models.py +++ b/authentik/stages/email/models.py @@ -68,6 +68,9 @@ def get_template_choices(): class EmailStage(Stage): """Send an Email to the user with a token to confirm their Email address.""" + # Remove the legacy credential columns in 2027.2. + _password = models.TextField(blank=True, db_column="password", default="") + use_global_settings = models.BooleanField( default=False, help_text=_( @@ -79,7 +82,15 @@ class EmailStage(Stage): host = models.TextField(default="localhost") port = models.IntegerField(default=25) username = models.TextField(default="", blank=True) - password = models.TextField(default="", blank=True) + secret = models.ForeignKey( + "authentik_secrets.Secret", + verbose_name=_("SMTP password"), + on_delete=models.PROTECT, + null=True, + blank=True, + default=None, + related_name="email_stages", + ) use_tls = models.BooleanField(default=False) use_ssl = models.BooleanField(default=False) timeout = models.IntegerField(default=10) @@ -147,7 +158,7 @@ def backend(self) -> BaseEmailBackend: host=self.host, port=self.port, username=self.username, - password=self.password, + password=self.secret.value if self.secret else "", use_tls=self.use_tls, use_ssl=self.use_ssl, timeout=self.timeout, diff --git a/authentik/stages/email/tests/test_management_commands.py b/authentik/stages/email/tests/test_management_commands.py index d39f11285072..ebf41483bdd8 100644 --- a/authentik/stages/email/tests/test_management_commands.py +++ b/authentik/stages/email/tests/test_management_commands.py @@ -8,6 +8,7 @@ from django.test import TestCase from authentik.core.tests.utils import create_test_admin_user +from authentik.crypto.secrets.tests.utils import create_test_secret from authentik.stages.email.models import EmailStage @@ -58,7 +59,7 @@ def test_test_email_command_uses_stage_settings(self): host="stage.authentik.local", port=587, username="stage-user", - password="stage-password", # nosec + secret=create_test_secret("stage-password"), # nosec use_tls=True, ) diff --git a/authentik/stages/identification/tests.py b/authentik/stages/identification/tests.py index 7830daa1bc2a..819bc5793286 100644 --- a/authentik/stages/identification/tests.py +++ b/authentik/stages/identification/tests.py @@ -5,6 +5,7 @@ from rest_framework.exceptions import ValidationError from authentik.core.tests.utils import create_test_admin_user, create_test_flow +from authentik.crypto.secrets.tests.utils import create_test_secret from authentik.flows.models import FlowDesignation, FlowStageBinding from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER from authentik.flows.tests import FlowTestCase @@ -271,7 +272,7 @@ def test_valid_with_captcha(self, mock: Mocker): captcha_stage = CaptchaStage.objects.create( name="captcha", public_key=RECAPTCHA_PUBLIC_KEY, - private_key=RECAPTCHA_PRIVATE_KEY, + secret=create_test_secret(RECAPTCHA_PRIVATE_KEY), ) self.stage.captcha_stage = captcha_stage self.stage.save() @@ -296,7 +297,7 @@ def test_valid_with_captcha_override(self, mock: Mocker): captcha_stage = CaptchaStage.objects.create( name="captcha", public_key=configured_site_key, - private_key=generate_id(), + secret=create_test_secret(generate_id()), ) self.stage.captcha_stage = captcha_stage self.stage.save() @@ -343,7 +344,7 @@ def test_invalid_with_captcha(self, mock: Mocker): captcha_stage = CaptchaStage.objects.create( name="captcha", public_key=RECAPTCHA_PUBLIC_KEY, - private_key=RECAPTCHA_PRIVATE_KEY, + secret=create_test_secret(RECAPTCHA_PRIVATE_KEY), ) self.stage.captcha_stage = captcha_stage @@ -395,7 +396,7 @@ def test_invalid_with_captcha_retriable(self, mock: Mocker): captcha_stage = CaptchaStage.objects.create( name="captcha", public_key=RECAPTCHA_PUBLIC_KEY, - private_key=RECAPTCHA_PRIVATE_KEY, + secret=create_test_secret(RECAPTCHA_PRIVATE_KEY), ) self.stage.captcha_stage = captcha_stage diff --git a/blueprints/example/flows-login-conditional-captcha.yaml b/blueprints/example/flows-login-conditional-captcha.yaml index 71f80a98711a..2629f51fbe11 100644 --- a/blueprints/example/flows-login-conditional-captcha.yaml +++ b/blueprints/example/flows-login-conditional-captcha.yaml @@ -17,13 +17,19 @@ entries: name: default-authentication-login id: default-authentication-login model: authentik_stages_user_login.userloginstage + - identifiers: + name: default-authentication-flow-captcha private key + id: default-authentication-flow-captcha-private-key + model: authentik_secrets.secret + attrs: + value: 6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe - identifiers: name: default-authentication-flow-captcha id: default-authentication-flow-captcha model: authentik_stages_captcha.captchastage attrs: public_key: 6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI - private_key: 6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe + secret: !KeyOf default-authentication-flow-captcha-private-key - identifiers: name: default-authentication-identification id: default-authentication-identification diff --git a/blueprints/schema.json b/blueprints/schema.json index f01311b71415..653211c70891 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -16918,9 +16918,10 @@ "type": "string", "title": "Username" }, - "password": { + "secret": { "type": "string", - "title": "Password" + "format": "uuid", + "title": "SMTP password" }, "use_tls": { "type": "boolean", @@ -17643,9 +17644,9 @@ "title": "Public key", "description": "Public key, acquired your captcha Provider." }, - "private_key": { + "secret": { "type": "string", - "minLength": 1, + "format": "uuid", "title": "Private key", "description": "Private key, acquired your captcha Provider." }, @@ -17875,9 +17876,10 @@ "type": "string", "title": "Username" }, - "password": { + "secret": { "type": "string", - "title": "Password" + "format": "uuid", + "title": "SMTP password" }, "use_tls": { "type": "boolean", diff --git a/packages/client-ts/src/apis/StagesApi.ts b/packages/client-ts/src/apis/StagesApi.ts index a672f3ecc1e9..7702f8fe540d 100644 --- a/packages/client-ts/src/apis/StagesApi.ts +++ b/packages/client-ts/src/apis/StagesApi.ts @@ -721,10 +721,6 @@ export interface StagesAuthenticatorEmailListRequest { * Number of results to return per page. */ pageSize?: number; - /** - * - */ - password?: string; /** * */ @@ -733,10 +729,6 @@ export interface StagesAuthenticatorEmailListRequest { * A search term. */ search?: string; - /** - * - */ - stageUuid?: string; /** * */ @@ -4868,10 +4860,6 @@ export class StagesApi extends runtime.BaseAPI { queryParameters["page_size"] = requestParameters["pageSize"]; } - if (requestParameters["password"] != null) { - queryParameters["password"] = requestParameters["password"]; - } - if (requestParameters["port"] != null) { queryParameters["port"] = requestParameters["port"]; } @@ -4880,10 +4868,6 @@ export class StagesApi extends runtime.BaseAPI { queryParameters["search"] = requestParameters["search"]; } - if (requestParameters["stageUuid"] != null) { - queryParameters["stage_uuid"] = requestParameters["stageUuid"]; - } - if (requestParameters["subject"] != null) { queryParameters["subject"] = requestParameters["subject"]; } diff --git a/packages/client-ts/src/models/AuthenticatorEmailStage.ts b/packages/client-ts/src/models/AuthenticatorEmailStage.ts index e6a76b8d10bb..965acbb5f644 100644 --- a/packages/client-ts/src/models/AuthenticatorEmailStage.ts +++ b/packages/client-ts/src/models/AuthenticatorEmailStage.ts @@ -73,6 +73,10 @@ export interface AuthenticatorEmailStage { * */ username?: string; + /** + * + */ + secret?: string | null; /** * */ @@ -172,6 +176,12 @@ export function AuthenticatorEmailStageFromJSONTyped( host: json["host"] == null ? undefined : json["host"], port: json["port"] == null ? undefined : json["port"], username: json["username"] == null ? undefined : json["username"], + secret: + json["secret"] === undefined + ? undefined + : json["secret"] === null + ? null + : json["secret"], useTls: json["use_tls"] == null ? undefined : json["use_tls"], useSsl: json["use_ssl"] == null ? undefined : json["use_ssl"], timeout: json["timeout"] == null ? undefined : json["timeout"], @@ -205,6 +215,7 @@ export function AuthenticatorEmailStageToJSONTyped( host: value["host"], port: value["port"], username: value["username"], + secret: value["secret"], use_tls: value["useTls"], use_ssl: value["useSsl"], timeout: value["timeout"], diff --git a/packages/client-ts/src/models/AuthenticatorEmailStageRequest.ts b/packages/client-ts/src/models/AuthenticatorEmailStageRequest.ts index 9f20f7637f2f..cc0605cc771f 100644 --- a/packages/client-ts/src/models/AuthenticatorEmailStageRequest.ts +++ b/packages/client-ts/src/models/AuthenticatorEmailStageRequest.ts @@ -49,7 +49,7 @@ export interface AuthenticatorEmailStageRequest { /** * */ - password?: string; + secret?: string | null; /** * */ @@ -115,7 +115,12 @@ export function AuthenticatorEmailStageRequestFromJSONTyped( host: json["host"] == null ? undefined : json["host"], port: json["port"] == null ? undefined : json["port"], username: json["username"] == null ? undefined : json["username"], - password: json["password"] == null ? undefined : json["password"], + secret: + json["secret"] === undefined + ? undefined + : json["secret"] === null + ? null + : json["secret"], useTls: json["use_tls"] == null ? undefined : json["use_tls"], useSsl: json["use_ssl"] == null ? undefined : json["use_ssl"], timeout: json["timeout"] == null ? undefined : json["timeout"], @@ -146,7 +151,7 @@ export function AuthenticatorEmailStageRequestToJSONTyped( host: value["host"], port: value["port"], username: value["username"], - password: value["password"], + secret: value["secret"], use_tls: value["useTls"], use_ssl: value["useSsl"], timeout: value["timeout"], diff --git a/packages/client-ts/src/models/CaptchaStage.ts b/packages/client-ts/src/models/CaptchaStage.ts index a64edb014825..2db5bc254cbf 100644 --- a/packages/client-ts/src/models/CaptchaStage.ts +++ b/packages/client-ts/src/models/CaptchaStage.ts @@ -58,6 +58,10 @@ export interface CaptchaStage { * Public key, acquired your captcha Provider. */ publicKey: string; + /** + * Private key, acquired your captcha Provider. + */ + secret: string; /** * */ @@ -130,6 +134,7 @@ export function instanceOfCaptchaStage(value: object): value is CaptchaStage { (value as Record)["public_key"] === undefined) ) return false; + if (!("secret" in value) || value["secret"] === undefined) return false; return true; } @@ -150,6 +155,7 @@ export function CaptchaStageFromJSONTyped(json: any, ignoreDiscriminator: boolea metaModelName: json["meta_model_name"], flowSet: (json["flow_set"] as Array).map(FlowSetFromJSON), publicKey: json["public_key"], + secret: json["secret"], jsUrl: json["js_url"] == null ? undefined : json["js_url"], apiUrl: json["api_url"] == null ? undefined : json["api_url"], requestContentType: @@ -184,6 +190,7 @@ export function CaptchaStageToJSONTyped( return { name: value["name"], public_key: value["publicKey"], + secret: value["secret"], js_url: value["jsUrl"], api_url: value["apiUrl"], request_content_type: RequestContentTypeEnumToJSON(value["requestContentType"]), diff --git a/packages/client-ts/src/models/CaptchaStageRequest.ts b/packages/client-ts/src/models/CaptchaStageRequest.ts index 543ae77a6855..7cff5021a219 100644 --- a/packages/client-ts/src/models/CaptchaStageRequest.ts +++ b/packages/client-ts/src/models/CaptchaStageRequest.ts @@ -35,7 +35,7 @@ export interface CaptchaStageRequest { /** * Private key, acquired your captcha Provider. */ - privateKey: string; + secret: string; /** * */ @@ -78,13 +78,7 @@ export function instanceOfCaptchaStageRequest(value: object): value is CaptchaSt (value as Record)["public_key"] === undefined) ) return false; - if ( - (!("privateKey" in (value as Record)) && - !("private_key" in (value as Record))) || - ((value as Record)["privateKey"] === undefined && - (value as Record)["private_key"] === undefined) - ) - return false; + if (!("secret" in value) || value["secret"] === undefined) return false; return true; } @@ -102,7 +96,7 @@ export function CaptchaStageRequestFromJSONTyped( return { name: json["name"], publicKey: json["public_key"], - privateKey: json["private_key"], + secret: json["secret"], jsUrl: json["js_url"] == null ? undefined : json["js_url"], apiUrl: json["api_url"] == null ? undefined : json["api_url"], requestContentType: @@ -134,7 +128,7 @@ export function CaptchaStageRequestToJSONTyped( return { name: value["name"], public_key: value["publicKey"], - private_key: value["privateKey"], + secret: value["secret"], js_url: value["jsUrl"], api_url: value["apiUrl"], request_content_type: RequestContentTypeEnumToJSON(value["requestContentType"]), diff --git a/packages/client-ts/src/models/EmailStage.ts b/packages/client-ts/src/models/EmailStage.ts index a7f00ec76e32..48afd3d77bf5 100644 --- a/packages/client-ts/src/models/EmailStage.ts +++ b/packages/client-ts/src/models/EmailStage.ts @@ -65,6 +65,10 @@ export interface EmailStage { * */ username?: string; + /** + * + */ + secret?: string | null; /** * */ @@ -166,6 +170,12 @@ export function EmailStageFromJSONTyped(json: any, ignoreDiscriminator: boolean) host: json["host"] == null ? undefined : json["host"], port: json["port"] == null ? undefined : json["port"], username: json["username"] == null ? undefined : json["username"], + secret: + json["secret"] === undefined + ? undefined + : json["secret"] === null + ? null + : json["secret"], useTls: json["use_tls"] == null ? undefined : json["use_tls"], useSsl: json["use_ssl"] == null ? undefined : json["use_ssl"], timeout: json["timeout"] == null ? undefined : json["timeout"], @@ -203,6 +213,7 @@ export function EmailStageToJSONTyped( host: value["host"], port: value["port"], username: value["username"], + secret: value["secret"], use_tls: value["useTls"], use_ssl: value["useSsl"], timeout: value["timeout"], diff --git a/packages/client-ts/src/models/EmailStageRequest.ts b/packages/client-ts/src/models/EmailStageRequest.ts index eba6a16f745d..df3281d67167 100644 --- a/packages/client-ts/src/models/EmailStageRequest.ts +++ b/packages/client-ts/src/models/EmailStageRequest.ts @@ -41,7 +41,7 @@ export interface EmailStageRequest { /** * */ - password?: string; + secret?: string | null; /** * */ @@ -110,7 +110,12 @@ export function EmailStageRequestFromJSONTyped( host: json["host"] == null ? undefined : json["host"], port: json["port"] == null ? undefined : json["port"], username: json["username"] == null ? undefined : json["username"], - password: json["password"] == null ? undefined : json["password"], + secret: + json["secret"] === undefined + ? undefined + : json["secret"] === null + ? null + : json["secret"], useTls: json["use_tls"] == null ? undefined : json["use_tls"], useSsl: json["use_ssl"] == null ? undefined : json["use_ssl"], timeout: json["timeout"] == null ? undefined : json["timeout"], @@ -145,7 +150,7 @@ export function EmailStageRequestToJSONTyped( host: value["host"], port: value["port"], username: value["username"], - password: value["password"], + secret: value["secret"], use_tls: value["useTls"], use_ssl: value["useSsl"], timeout: value["timeout"], diff --git a/packages/client-ts/src/models/PatchedAuthenticatorEmailStageRequest.ts b/packages/client-ts/src/models/PatchedAuthenticatorEmailStageRequest.ts index b1ea10c880b8..97a9613f3e15 100644 --- a/packages/client-ts/src/models/PatchedAuthenticatorEmailStageRequest.ts +++ b/packages/client-ts/src/models/PatchedAuthenticatorEmailStageRequest.ts @@ -49,7 +49,7 @@ export interface PatchedAuthenticatorEmailStageRequest { /** * */ - password?: string; + secret?: string | null; /** * */ @@ -116,7 +116,12 @@ export function PatchedAuthenticatorEmailStageRequestFromJSONTyped( host: json["host"] == null ? undefined : json["host"], port: json["port"] == null ? undefined : json["port"], username: json["username"] == null ? undefined : json["username"], - password: json["password"] == null ? undefined : json["password"], + secret: + json["secret"] === undefined + ? undefined + : json["secret"] === null + ? null + : json["secret"], useTls: json["use_tls"] == null ? undefined : json["use_tls"], useSsl: json["use_ssl"] == null ? undefined : json["use_ssl"], timeout: json["timeout"] == null ? undefined : json["timeout"], @@ -149,7 +154,7 @@ export function PatchedAuthenticatorEmailStageRequestToJSONTyped( host: value["host"], port: value["port"], username: value["username"], - password: value["password"], + secret: value["secret"], use_tls: value["useTls"], use_ssl: value["useSsl"], timeout: value["timeout"], diff --git a/packages/client-ts/src/models/PatchedCaptchaStageRequest.ts b/packages/client-ts/src/models/PatchedCaptchaStageRequest.ts index 40da7a57c41e..79045b23b2e5 100644 --- a/packages/client-ts/src/models/PatchedCaptchaStageRequest.ts +++ b/packages/client-ts/src/models/PatchedCaptchaStageRequest.ts @@ -35,7 +35,7 @@ export interface PatchedCaptchaStageRequest { /** * Private key, acquired your captcha Provider. */ - privateKey?: string; + secret?: string; /** * */ @@ -89,7 +89,7 @@ export function PatchedCaptchaStageRequestFromJSONTyped( return { name: json["name"] == null ? undefined : json["name"], publicKey: json["public_key"] == null ? undefined : json["public_key"], - privateKey: json["private_key"] == null ? undefined : json["private_key"], + secret: json["secret"] == null ? undefined : json["secret"], jsUrl: json["js_url"] == null ? undefined : json["js_url"], apiUrl: json["api_url"] == null ? undefined : json["api_url"], requestContentType: @@ -121,7 +121,7 @@ export function PatchedCaptchaStageRequestToJSONTyped( return { name: value["name"], public_key: value["publicKey"], - private_key: value["privateKey"], + secret: value["secret"], js_url: value["jsUrl"], api_url: value["apiUrl"], request_content_type: RequestContentTypeEnumToJSON(value["requestContentType"]), diff --git a/packages/client-ts/src/models/PatchedEmailStageRequest.ts b/packages/client-ts/src/models/PatchedEmailStageRequest.ts index 1b3f34f6cc87..853311d9cc76 100644 --- a/packages/client-ts/src/models/PatchedEmailStageRequest.ts +++ b/packages/client-ts/src/models/PatchedEmailStageRequest.ts @@ -41,7 +41,7 @@ export interface PatchedEmailStageRequest { /** * */ - password?: string; + secret?: string | null; /** * */ @@ -111,7 +111,12 @@ export function PatchedEmailStageRequestFromJSONTyped( host: json["host"] == null ? undefined : json["host"], port: json["port"] == null ? undefined : json["port"], username: json["username"] == null ? undefined : json["username"], - password: json["password"] == null ? undefined : json["password"], + secret: + json["secret"] === undefined + ? undefined + : json["secret"] === null + ? null + : json["secret"], useTls: json["use_tls"] == null ? undefined : json["use_tls"], useSsl: json["use_ssl"] == null ? undefined : json["use_ssl"], timeout: json["timeout"] == null ? undefined : json["timeout"], @@ -146,7 +151,7 @@ export function PatchedEmailStageRequestToJSONTyped( host: value["host"], port: value["port"], username: value["username"], - password: value["password"], + secret: value["secret"], use_tls: value["useTls"], use_ssl: value["useSsl"], timeout: value["timeout"], diff --git a/schema.yml b/schema.yml index 702c236dd658..7238fd7a063a 100644 --- a/schema.yml +++ b/schema.yml @@ -29549,20 +29549,11 @@ paths: - $ref: '#/components/parameters/QueryPaginationOrdering' - $ref: '#/components/parameters/QueryPaginationPage' - $ref: '#/components/parameters/QueryPaginationPageSize' - - in: query - name: password - schema: - type: string - in: query name: port schema: type: integer - $ref: '#/components/parameters/QuerySearch' - - in: query - name: stage_uuid - schema: - type: string - format: uuid - in: query name: subject schema: @@ -37151,6 +37142,11 @@ components: minimum: -2147483648 username: type: string + secret: + type: string + format: uuid + nullable: true + title: SMTP password use_tls: type: boolean use_ssl: @@ -37206,9 +37202,11 @@ components: minimum: -2147483648 username: type: string - password: + secret: type: string - writeOnly: true + format: uuid + nullable: true + title: SMTP password use_tls: type: boolean use_ssl: @@ -38518,6 +38516,11 @@ components: public_key: type: string description: Public key, acquired your captcha Provider. + secret: + type: string + format: uuid + title: Private key + description: Private key, acquired your captcha Provider. js_url: type: string api_url: @@ -38545,6 +38548,7 @@ components: - name - pk - public_key + - secret - verbose_name - verbose_name_plural CaptchaStageRequest: @@ -38558,10 +38562,10 @@ components: type: string minLength: 1 description: Public key, acquired your captcha Provider. - private_key: + secret: type: string - writeOnly: true - minLength: 1 + format: uuid + title: Private key description: Private key, acquired your captcha Provider. js_url: type: string @@ -38587,8 +38591,8 @@ components: in the context for policy decisions required: - name - - private_key - public_key + - secret CertAttributeEnum: enum: - subject @@ -40591,6 +40595,11 @@ components: minimum: -2147483648 username: type: string + secret: + type: string + format: uuid + nullable: true + title: SMTP password use_tls: type: boolean use_ssl: @@ -40650,9 +40659,11 @@ components: minimum: -2147483648 username: type: string - password: + secret: type: string - writeOnly: true + format: uuid + nullable: true + title: SMTP password use_tls: type: boolean use_ssl: @@ -50908,9 +50919,11 @@ components: minimum: -2147483648 username: type: string - password: + secret: type: string - writeOnly: true + format: uuid + nullable: true + title: SMTP password use_tls: type: boolean use_ssl: @@ -51247,10 +51260,10 @@ components: type: string minLength: 1 description: Public key, acquired your captcha Provider. - private_key: + secret: type: string - writeOnly: true - minLength: 1 + format: uuid + title: Private key description: Private key, acquired your captcha Provider. js_url: type: string @@ -51483,9 +51496,11 @@ components: minimum: -2147483648 username: type: string - password: + secret: type: string - writeOnly: true + format: uuid + nullable: true + title: SMTP password use_tls: type: boolean use_ssl: From 8f10c15380c12b100d26478dd1371290e0ae9898 Mon Sep 17 00:00:00 2001 From: Dominic R Date: Sun, 13 Sep 2026 21:56:31 -0400 Subject: [PATCH 2/6] api: retain legacy credential fields until frontend migration --- .../crypto/secrets/tests/test_legacy_api.py | 39 +++++++++++++++++++ blueprints/schema.json | 13 +++++++ .../models/AuthenticatorEmailStageRequest.ts | 6 +++ packages/client-ts/src/models/CaptchaStage.ts | 5 +-- .../src/models/CaptchaStageRequest.ts | 11 ++++-- .../client-ts/src/models/EmailStageRequest.ts | 6 +++ .../PatchedAuthenticatorEmailStageRequest.ts | 6 +++ .../src/models/PatchedCaptchaStageRequest.ts | 6 +++ .../src/models/PatchedEmailStageRequest.ts | 6 +++ schema.yml | 22 ++++++++++- 10 files changed, 112 insertions(+), 8 deletions(-) diff --git a/authentik/crypto/secrets/tests/test_legacy_api.py b/authentik/crypto/secrets/tests/test_legacy_api.py index 5fe06b7f0a85..fe1b20678c81 100644 --- a/authentik/crypto/secrets/tests/test_legacy_api.py +++ b/authentik/crypto/secrets/tests/test_legacy_api.py @@ -81,3 +81,42 @@ def test_legacy_reads_require_value_permission(self): self.client.get(self.url).json()["client_secret"], self.provider.secret.value ) self.assertTrue(Event.objects.filter(action=EventAction.SECRET_VIEW).exists()) + + +class TestLegacyCaptchaAPI(APITestCase): + def setUp(self): + self.client.force_login(create_test_admin_user()) + self.url = reverse("authentik_api:captchastage-list") + + def test_create_accepts_either_credential_input(self): + from authentik.stages.captcha.models import CaptchaStage + + secret = Secret.objects.create(name="captcha", value="reference credential") + for credential, expected in [ + ({"private_key": "literal credential"}, "literal credential"), + ({"secret": str(secret.pk)}, secret.value), + ]: + with self.subTest(credential=credential): + response = self.client.post( + self.url, + {"name": expected, "public_key": "public", **credential}, + format="json", + ) + self.assertEqual(response.status_code, 201, response.content) + stage = CaptchaStage.objects.get(pk=response.json()["pk"]) + self.assertEqual(stage.secret.value, expected) + original = stage.secret_id + response = self.client.patch( + reverse("authentik_api:captchastage-detail", kwargs={"pk": stage.pk}), + {"public_key": "changed public key"}, + ) + self.assertEqual(response.status_code, 200, response.content) + stage.refresh_from_db() + self.assertEqual(stage.secret_id, original) + + def test_create_requires_credential(self): + count = Secret.objects.count() + response = self.client.post(self.url, {"name": "missing", "public_key": "public"}) + self.assertEqual(response.status_code, 400, response.content) + self.assertIn("secret", response.json()) + self.assertEqual(Secret.objects.count(), count) diff --git a/blueprints/schema.json b/blueprints/schema.json index 653211c70891..7c172dc433b3 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -16959,6 +16959,10 @@ "type": "string", "minLength": 1, "title": "Template" + }, + "password": { + "type": "string", + "title": "Password" } }, "required": [] @@ -17684,6 +17688,11 @@ "type": "boolean", "title": "Error on invalid score", "description": "When enabled and the received captcha score is outside of the given threshold, the stage will show an error message. When not enabled, the flow will continue, but the data from the captcha will be available in the context for policy decisions" + }, + "private_key": { + "type": "string", + "minLength": 1, + "title": "Private key" } }, "required": [] @@ -17934,6 +17943,10 @@ "minLength": 1, "title": "Recovery cache timeout", "description": "The time window used to count recent account recovery attempts. If the number of attempts exceed recovery_max_attempts within this period, further attempts will be rate-limited. (Format: hours=1;minutes=2;seconds=3)." + }, + "password": { + "type": "string", + "title": "Password" } }, "required": [] diff --git a/packages/client-ts/src/models/AuthenticatorEmailStageRequest.ts b/packages/client-ts/src/models/AuthenticatorEmailStageRequest.ts index cc0605cc771f..f6f1a4904e75 100644 --- a/packages/client-ts/src/models/AuthenticatorEmailStageRequest.ts +++ b/packages/client-ts/src/models/AuthenticatorEmailStageRequest.ts @@ -78,6 +78,10 @@ export interface AuthenticatorEmailStageRequest { * */ template?: string; + /** + * + */ + password?: string; } /** @@ -128,6 +132,7 @@ export function AuthenticatorEmailStageRequestFromJSONTyped( subject: json["subject"] == null ? undefined : json["subject"], tokenExpiry: json["token_expiry"] == null ? undefined : json["token_expiry"], template: json["template"] == null ? undefined : json["template"], + password: json["password"] == null ? undefined : json["password"], }; } @@ -159,5 +164,6 @@ export function AuthenticatorEmailStageRequestToJSONTyped( subject: value["subject"], token_expiry: value["tokenExpiry"], template: value["template"], + password: value["password"], }; } diff --git a/packages/client-ts/src/models/CaptchaStage.ts b/packages/client-ts/src/models/CaptchaStage.ts index 2db5bc254cbf..ccdb28d3ebca 100644 --- a/packages/client-ts/src/models/CaptchaStage.ts +++ b/packages/client-ts/src/models/CaptchaStage.ts @@ -61,7 +61,7 @@ export interface CaptchaStage { /** * Private key, acquired your captcha Provider. */ - secret: string; + secret?: string; /** * */ @@ -134,7 +134,6 @@ export function instanceOfCaptchaStage(value: object): value is CaptchaStage { (value as Record)["public_key"] === undefined) ) return false; - if (!("secret" in value) || value["secret"] === undefined) return false; return true; } @@ -155,7 +154,7 @@ export function CaptchaStageFromJSONTyped(json: any, ignoreDiscriminator: boolea metaModelName: json["meta_model_name"], flowSet: (json["flow_set"] as Array).map(FlowSetFromJSON), publicKey: json["public_key"], - secret: json["secret"], + secret: json["secret"] == null ? undefined : json["secret"], jsUrl: json["js_url"] == null ? undefined : json["js_url"], apiUrl: json["api_url"] == null ? undefined : json["api_url"], requestContentType: diff --git a/packages/client-ts/src/models/CaptchaStageRequest.ts b/packages/client-ts/src/models/CaptchaStageRequest.ts index 7cff5021a219..d8c1f7341c78 100644 --- a/packages/client-ts/src/models/CaptchaStageRequest.ts +++ b/packages/client-ts/src/models/CaptchaStageRequest.ts @@ -35,7 +35,7 @@ export interface CaptchaStageRequest { /** * Private key, acquired your captcha Provider. */ - secret: string; + secret?: string; /** * */ @@ -64,6 +64,10 @@ export interface CaptchaStageRequest { * When enabled and the received captcha score is outside of the given threshold, the stage will show an error message. When not enabled, the flow will continue, but the data from the captcha will be available in the context for policy decisions */ errorOnInvalidScore?: boolean; + /** + * + */ + privateKey?: string; } /** @@ -78,7 +82,6 @@ export function instanceOfCaptchaStageRequest(value: object): value is CaptchaSt (value as Record)["public_key"] === undefined) ) return false; - if (!("secret" in value) || value["secret"] === undefined) return false; return true; } @@ -96,7 +99,7 @@ export function CaptchaStageRequestFromJSONTyped( return { name: json["name"], publicKey: json["public_key"], - secret: json["secret"], + secret: json["secret"] == null ? undefined : json["secret"], jsUrl: json["js_url"] == null ? undefined : json["js_url"], apiUrl: json["api_url"] == null ? undefined : json["api_url"], requestContentType: @@ -110,6 +113,7 @@ export function CaptchaStageRequestFromJSONTyped( json["score_max_threshold"] == null ? undefined : json["score_max_threshold"], errorOnInvalidScore: json["error_on_invalid_score"] == null ? undefined : json["error_on_invalid_score"], + privateKey: json["private_key"] == null ? undefined : json["private_key"], }; } @@ -136,5 +140,6 @@ export function CaptchaStageRequestToJSONTyped( score_min_threshold: value["scoreMinThreshold"], score_max_threshold: value["scoreMaxThreshold"], error_on_invalid_score: value["errorOnInvalidScore"], + private_key: value["privateKey"], }; } diff --git a/packages/client-ts/src/models/EmailStageRequest.ts b/packages/client-ts/src/models/EmailStageRequest.ts index df3281d67167..69993e3ff82c 100644 --- a/packages/client-ts/src/models/EmailStageRequest.ts +++ b/packages/client-ts/src/models/EmailStageRequest.ts @@ -82,6 +82,10 @@ export interface EmailStageRequest { * The time window used to count recent account recovery attempts. If the number of attempts exceed recovery_max_attempts within this period, further attempts will be rate-limited. (Format: hours=1;minutes=2;seconds=3). */ recoveryCacheTimeout?: string; + /** + * + */ + password?: string; } /** @@ -129,6 +133,7 @@ export function EmailStageRequestFromJSONTyped( json["recovery_max_attempts"] == null ? undefined : json["recovery_max_attempts"], recoveryCacheTimeout: json["recovery_cache_timeout"] == null ? undefined : json["recovery_cache_timeout"], + password: json["password"] == null ? undefined : json["password"], }; } @@ -161,5 +166,6 @@ export function EmailStageRequestToJSONTyped( activate_user_on_success: value["activateUserOnSuccess"], recovery_max_attempts: value["recoveryMaxAttempts"], recovery_cache_timeout: value["recoveryCacheTimeout"], + password: value["password"], }; } diff --git a/packages/client-ts/src/models/PatchedAuthenticatorEmailStageRequest.ts b/packages/client-ts/src/models/PatchedAuthenticatorEmailStageRequest.ts index 97a9613f3e15..7d469a330f91 100644 --- a/packages/client-ts/src/models/PatchedAuthenticatorEmailStageRequest.ts +++ b/packages/client-ts/src/models/PatchedAuthenticatorEmailStageRequest.ts @@ -78,6 +78,10 @@ export interface PatchedAuthenticatorEmailStageRequest { * */ template?: string; + /** + * + */ + password?: string; } /** @@ -129,6 +133,7 @@ export function PatchedAuthenticatorEmailStageRequestFromJSONTyped( subject: json["subject"] == null ? undefined : json["subject"], tokenExpiry: json["token_expiry"] == null ? undefined : json["token_expiry"], template: json["template"] == null ? undefined : json["template"], + password: json["password"] == null ? undefined : json["password"], }; } @@ -162,5 +167,6 @@ export function PatchedAuthenticatorEmailStageRequestToJSONTyped( subject: value["subject"], token_expiry: value["tokenExpiry"], template: value["template"], + password: value["password"], }; } diff --git a/packages/client-ts/src/models/PatchedCaptchaStageRequest.ts b/packages/client-ts/src/models/PatchedCaptchaStageRequest.ts index 79045b23b2e5..5d2fa70732aa 100644 --- a/packages/client-ts/src/models/PatchedCaptchaStageRequest.ts +++ b/packages/client-ts/src/models/PatchedCaptchaStageRequest.ts @@ -64,6 +64,10 @@ export interface PatchedCaptchaStageRequest { * When enabled and the received captcha score is outside of the given threshold, the stage will show an error message. When not enabled, the flow will continue, but the data from the captcha will be available in the context for policy decisions */ errorOnInvalidScore?: boolean; + /** + * + */ + privateKey?: string; } /** @@ -103,6 +107,7 @@ export function PatchedCaptchaStageRequestFromJSONTyped( json["score_max_threshold"] == null ? undefined : json["score_max_threshold"], errorOnInvalidScore: json["error_on_invalid_score"] == null ? undefined : json["error_on_invalid_score"], + privateKey: json["private_key"] == null ? undefined : json["private_key"], }; } @@ -129,5 +134,6 @@ export function PatchedCaptchaStageRequestToJSONTyped( score_min_threshold: value["scoreMinThreshold"], score_max_threshold: value["scoreMaxThreshold"], error_on_invalid_score: value["errorOnInvalidScore"], + private_key: value["privateKey"], }; } diff --git a/packages/client-ts/src/models/PatchedEmailStageRequest.ts b/packages/client-ts/src/models/PatchedEmailStageRequest.ts index 853311d9cc76..3513c2ca8ab0 100644 --- a/packages/client-ts/src/models/PatchedEmailStageRequest.ts +++ b/packages/client-ts/src/models/PatchedEmailStageRequest.ts @@ -82,6 +82,10 @@ export interface PatchedEmailStageRequest { * The time window used to count recent account recovery attempts. If the number of attempts exceed recovery_max_attempts within this period, further attempts will be rate-limited. (Format: hours=1;minutes=2;seconds=3). */ recoveryCacheTimeout?: string; + /** + * + */ + password?: string; } /** @@ -130,6 +134,7 @@ export function PatchedEmailStageRequestFromJSONTyped( json["recovery_max_attempts"] == null ? undefined : json["recovery_max_attempts"], recoveryCacheTimeout: json["recovery_cache_timeout"] == null ? undefined : json["recovery_cache_timeout"], + password: json["password"] == null ? undefined : json["password"], }; } @@ -162,5 +167,6 @@ export function PatchedEmailStageRequestToJSONTyped( activate_user_on_success: value["activateUserOnSuccess"], recovery_max_attempts: value["recoveryMaxAttempts"], recovery_cache_timeout: value["recoveryCacheTimeout"], + password: value["password"], }; } diff --git a/schema.yml b/schema.yml index 7238fd7a063a..99c23667f036 100644 --- a/schema.yml +++ b/schema.yml @@ -37230,6 +37230,9 @@ components: template: type: string minLength: 1 + password: + type: string + writeOnly: true required: - name AuthenticatorEndpointGDTCStage: @@ -38548,7 +38551,6 @@ components: - name - pk - public_key - - secret - verbose_name - verbose_name_plural CaptchaStageRequest: @@ -38589,10 +38591,13 @@ components: given threshold, the stage will show an error message. When not enabled, the flow will continue, but the data from the captcha will be available in the context for policy decisions + private_key: + type: string + writeOnly: true + minLength: 1 required: - name - public_key - - secret CertAttributeEnum: enum: - subject @@ -40700,6 +40705,9 @@ components: description: 'The time window used to count recent account recovery attempts. If the number of attempts exceed recovery_max_attempts within this period, further attempts will be rate-limited. (Format: hours=1;minutes=2;seconds=3).' + password: + type: string + writeOnly: true required: - name Endpoint: @@ -50947,6 +50955,9 @@ components: template: type: string minLength: 1 + password: + type: string + writeOnly: true PatchedAuthenticatorEndpointGDTCStageRequest: type: object description: AuthenticatorEndpointGDTCStage Serializer @@ -51287,6 +51298,10 @@ components: given threshold, the stage will show an error message. When not enabled, the flow will continue, but the data from the captcha will be available in the context for policy decisions + private_key: + type: string + writeOnly: true + minLength: 1 PatchedCertificateKeyPairRequest: type: object description: CertificateKeyPair Serializer @@ -51537,6 +51552,9 @@ components: description: 'The time window used to count recent account recovery attempts. If the number of attempts exceed recovery_max_attempts within this period, further attempts will be rate-limited. (Format: hours=1;minutes=2;seconds=3).' + password: + type: string + writeOnly: true PatchedEndpointDeviceRequest: type: object properties: From 43f554a97660618ab9471ebf7c352f31fba06b23 Mon Sep 17 00:00:00 2001 From: Dominic R Date: Wed, 23 Sep 2026 17:23:41 -0400 Subject: [PATCH 3/6] blueprints: use private_key_ref in conditional CAPTCHA example --- blueprints/example/flows-login-conditional-captcha.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blueprints/example/flows-login-conditional-captcha.yaml b/blueprints/example/flows-login-conditional-captcha.yaml index dad814cf62a0..44ca53a0c763 100644 --- a/blueprints/example/flows-login-conditional-captcha.yaml +++ b/blueprints/example/flows-login-conditional-captcha.yaml @@ -29,7 +29,7 @@ entries: model: authentik_stages_captcha.captchastage attrs: public_key: 6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI - secret: !KeyOf default-authentication-flow-captcha-private-key + private_key_ref: !KeyOf default-authentication-flow-captcha-private-key - identifiers: name: default-authentication-identification id: default-authentication-identification From c4bd957c458f6680523163ce87bb9e3e0997f701 Mon Sep 17 00:00:00 2001 From: Dominic R Date: Sat, 10 Oct 2026 15:32:24 -0400 Subject: [PATCH 4/6] stages: keep email and CAPTCHA legacy columns in place The legacy password and private key columns stay where they were, the authenticator email stage API keeps its stage_uuid filter, and the CAPTCHA secret field keeps its existing translation id. --- authentik/stages/authenticator_email/api.py | 1 + .../migrations/0005_managed_secrets.py | 14 ++++++------- .../stages/authenticator_email/models.py | 7 +++---- .../migrations/0006_managed_secrets.py | 20 ++++++++++++------- authentik/stages/captcha/models.py | 5 ++--- authentik/stages/captcha/stage.py | 2 +- .../email/migrations/0007_managed_secrets.py | 20 ++++++++++++------- authentik/stages/email/models.py | 7 +++---- .../AuthenticatorEmailStageForm.ts | 2 +- .../admin/stages/captcha/CaptchaStageForm.ts | 4 ++-- web/src/admin/stages/email/EmailStageForm.ts | 2 +- 11 files changed, 47 insertions(+), 37 deletions(-) diff --git a/authentik/stages/authenticator_email/api.py b/authentik/stages/authenticator_email/api.py index 6e8691ef9343..c083ed97ff9c 100644 --- a/authentik/stages/authenticator_email/api.py +++ b/authentik/stages/authenticator_email/api.py @@ -39,6 +39,7 @@ class AuthenticatorEmailStageViewSet(UsedByMixin, ModelViewSet): queryset = AuthenticatorEmailStage.objects.all() serializer_class = AuthenticatorEmailStageSerializer filterset_fields = [ + "stage_uuid", "name", "configure_flow", "friendly_name", diff --git a/authentik/stages/authenticator_email/migrations/0005_managed_secrets.py b/authentik/stages/authenticator_email/migrations/0005_managed_secrets.py index 568369be825c..9b123d1e0890 100644 --- a/authentik/stages/authenticator_email/migrations/0005_managed_secrets.py +++ b/authentik/stages/authenticator_email/migrations/0005_managed_secrets.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.17 on 2026-10-05 17:33 +# Generated by Django 5.2.17 on 2026-10-10 19:32 import django.db.models.deletion from django.db import migrations, models @@ -7,9 +7,9 @@ migrate_credentials, restore_credentials, ) -from authentik.crypto.secrets.migrations._permissions import preserve_role_permissions +from authentik.crypto.secrets.migrations._permissions import preserve_permissions -FIELDS = [("password", "password_ref", None, "SMTP password")] +FIELDS = [("password", "password_ref", "text", "SMTP password")] def forwards(apps, schema_editor): @@ -20,8 +20,8 @@ def forwards(apps, schema_editor): "AuthenticatorEmailStage", FIELDS, ) - preserve_role_permissions( - apps, schema_editor, [("authentik_stages_authenticator_email", "authenticatoremailstage")] + preserve_permissions( + apps, schema_editor, "authentik_stages_authenticator_email", "authenticatoremailstage" ) @@ -38,7 +38,8 @@ def backwards(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ("guardian", "0005_delete_userobjectpermission_and_groupobjectpermission"), + ("guardian", "0004_role_permissions"), + ("authentik_rbac", "0011_initialpermissionspermission"), ("authentik_crypto_secrets", "0001_initial"), ("authentik_stages_authenticator_email", "0004_alter_emaildevice_stage"), ] @@ -58,5 +59,4 @@ class Migration(migrations.Migration): ), ), migrations.RunPython(forwards, backwards), - migrations.RunSQL("SET CONSTRAINTS ALL IMMEDIATE", "SET CONSTRAINTS ALL IMMEDIATE"), ] diff --git a/authentik/stages/authenticator_email/models.py b/authentik/stages/authenticator_email/models.py index 5a35da8dcf44..068c0a582150 100644 --- a/authentik/stages/authenticator_email/models.py +++ b/authentik/stages/authenticator_email/models.py @@ -22,9 +22,6 @@ class AuthenticatorEmailStage(ConfigurableStage, FriendlyNamedStage, Stage): """Setup Email-based authentication for the user.""" - # Remove the legacy credential columns in 2027.2. - password = models.TextField(default="", blank=True) - use_global_settings = models.BooleanField( default=False, help_text=_( @@ -36,6 +33,8 @@ class AuthenticatorEmailStage(ConfigurableStage, FriendlyNamedStage, Stage): host = models.TextField(default="localhost") port = models.IntegerField(default=25) username = models.TextField(default="", blank=True) + # Legacy column, kept for downgrades. Remove in 2027.2. + password = models.TextField(default="", blank=True) password_ref = models.ForeignKey( "authentik_crypto_secrets.Secret", verbose_name=_("SMTP password"), @@ -105,7 +104,7 @@ def backend(self) -> BaseEmailBackend: host=self.host, port=self.port, username=self.username, - password=self.password_ref.value if self.password_ref else "", + password=self.password_ref.secret_value if self.password_ref else "", use_tls=self.use_tls, use_ssl=self.use_ssl, timeout=self.timeout, diff --git a/authentik/stages/captcha/migrations/0006_managed_secrets.py b/authentik/stages/captcha/migrations/0006_managed_secrets.py index 006e93fec53b..4b93cb007485 100644 --- a/authentik/stages/captcha/migrations/0006_managed_secrets.py +++ b/authentik/stages/captcha/migrations/0006_managed_secrets.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.17 on 2026-10-05 17:33 +# Generated by Django 5.2.17 on 2026-10-10 19:32 import django.db.models.deletion from django.db import migrations, models @@ -7,14 +7,20 @@ migrate_credentials, restore_credentials, ) -from authentik.crypto.secrets.migrations._permissions import preserve_role_permissions +from authentik.crypto.secrets.migrations._permissions import preserve_permissions -FIELDS = [("private_key", "private_key_ref", None, "captcha private key")] +FIELDS = [("private_key", "private_key_ref", "text", "captcha private key")] def forwards(apps, schema_editor): - migrate_credentials(apps, schema_editor, "authentik_stages_captcha", "CaptchaStage", FIELDS) - preserve_role_permissions(apps, schema_editor, [("authentik_stages_captcha", "captchastage")]) + migrate_credentials( + apps, + schema_editor, + "authentik_stages_captcha", + "CaptchaStage", + FIELDS, + ) + preserve_permissions(apps, schema_editor, "authentik_stages_captcha", "captchastage") def backwards(apps, schema_editor): @@ -24,7 +30,8 @@ def backwards(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ("guardian", "0005_delete_userobjectpermission_and_groupobjectpermission"), + ("guardian", "0004_role_permissions"), + ("authentik_rbac", "0011_initialpermissionspermission"), ("authentik_crypto_secrets", "0001_initial"), ("authentik_stages_captcha", "0005_captchastage_request_content_type"), ] @@ -45,5 +52,4 @@ class Migration(migrations.Migration): ), ), migrations.RunPython(forwards, backwards), - migrations.RunSQL("SET CONSTRAINTS ALL IMMEDIATE", "SET CONSTRAINTS ALL IMMEDIATE"), ] diff --git a/authentik/stages/captcha/models.py b/authentik/stages/captcha/models.py index 487373c36b12..190d67ab7da9 100644 --- a/authentik/stages/captcha/models.py +++ b/authentik/stages/captcha/models.py @@ -18,10 +18,9 @@ class CaptchaRequestContentType(models.TextChoices): class CaptchaStage(Stage): """Verify the user is human using Google's reCaptcha/other compatible CAPTCHA solutions.""" - # Remove the legacy credential columns in 2027.2. - private_key = models.TextField(help_text=_("Private key, acquired your captcha Provider.")) - public_key = models.TextField(help_text=_("Public key, acquired your captcha Provider.")) + # Legacy column, kept for downgrades. Remove in 2027.2. + private_key = models.TextField(help_text=_("Private key, acquired your captcha Provider.")) private_key_ref = models.ForeignKey( "authentik_crypto_secrets.Secret", verbose_name=_("Private key"), diff --git a/authentik/stages/captcha/stage.py b/authentik/stages/captcha/stage.py index f7446815d9f7..32d76422fdf3 100644 --- a/authentik/stages/captcha/stage.py +++ b/authentik/stages/captcha/stage.py @@ -36,7 +36,7 @@ class CaptchaChallenge(WithUserInfoChallenge): def verify_captcha_token(stage: CaptchaStage, token: str, remote_ip: str, key: str | None = None): """Validate captcha token""" payload = { - "secret": key or stage.private_key_ref.value, + "secret": key or stage.private_key_ref.secret_value, "response": token, "remoteip": remote_ip, } diff --git a/authentik/stages/email/migrations/0007_managed_secrets.py b/authentik/stages/email/migrations/0007_managed_secrets.py index 800cce01d850..66bbfc855344 100644 --- a/authentik/stages/email/migrations/0007_managed_secrets.py +++ b/authentik/stages/email/migrations/0007_managed_secrets.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.17 on 2026-10-05 17:33 +# Generated by Django 5.2.17 on 2026-10-10 19:32 import django.db.models.deletion from django.db import migrations, models @@ -7,14 +7,20 @@ migrate_credentials, restore_credentials, ) -from authentik.crypto.secrets.migrations._permissions import preserve_role_permissions +from authentik.crypto.secrets.migrations._permissions import preserve_permissions -FIELDS = [("password", "password_ref", None, "SMTP password")] +FIELDS = [("password", "password_ref", "text", "SMTP password")] def forwards(apps, schema_editor): - migrate_credentials(apps, schema_editor, "authentik_stages_email", "EmailStage", FIELDS) - preserve_role_permissions(apps, schema_editor, [("authentik_stages_email", "emailstage")]) + migrate_credentials( + apps, + schema_editor, + "authentik_stages_email", + "EmailStage", + FIELDS, + ) + preserve_permissions(apps, schema_editor, "authentik_stages_email", "emailstage") def backwards(apps, schema_editor): @@ -24,7 +30,8 @@ def backwards(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ("guardian", "0005_delete_userobjectpermission_and_groupobjectpermission"), + ("guardian", "0004_role_permissions"), + ("authentik_rbac", "0011_initialpermissionspermission"), ("authentik_crypto_secrets", "0001_initial"), ("authentik_stages_email", "0006_emailstage_recovery_cache_timeout_and_more"), ] @@ -44,5 +51,4 @@ class Migration(migrations.Migration): ), ), migrations.RunPython(forwards, backwards), - migrations.RunSQL("SET CONSTRAINTS ALL IMMEDIATE", "SET CONSTRAINTS ALL IMMEDIATE"), ] diff --git a/authentik/stages/email/models.py b/authentik/stages/email/models.py index ccb0bf00d86c..4edfc1f4cabe 100644 --- a/authentik/stages/email/models.py +++ b/authentik/stages/email/models.py @@ -68,9 +68,6 @@ def get_template_choices(): class EmailStage(Stage): """Send an Email to the user with a token to confirm their Email address.""" - # Remove the legacy credential columns in 2027.2. - password = models.TextField(default="", blank=True) - use_global_settings = models.BooleanField( default=False, help_text=_( @@ -82,6 +79,8 @@ class EmailStage(Stage): host = models.TextField(default="localhost") port = models.IntegerField(default=25) username = models.TextField(default="", blank=True) + # Legacy column, kept for downgrades. Remove in 2027.2. + password = models.TextField(default="", blank=True) password_ref = models.ForeignKey( "authentik_crypto_secrets.Secret", verbose_name=_("SMTP password"), @@ -158,7 +157,7 @@ def backend(self) -> BaseEmailBackend: host=self.host, port=self.port, username=self.username, - password=self.password_ref.value if self.password_ref else "", + password=self.password_ref.secret_value if self.password_ref else "", use_tls=self.use_tls, use_ssl=self.use_ssl, timeout=self.timeout, diff --git a/web/src/admin/stages/authenticator_email/AuthenticatorEmailStageForm.ts b/web/src/admin/stages/authenticator_email/AuthenticatorEmailStageForm.ts index ed302b93aa89..7bbcb2fded62 100644 --- a/web/src/admin/stages/authenticator_email/AuthenticatorEmailStageForm.ts +++ b/web/src/admin/stages/authenticator_email/AuthenticatorEmailStageForm.ts @@ -92,7 +92,7 @@ export class AuthenticatorEmailStageForm extends BaseStageForm { { Date: Sat, 10 Oct 2026 15:45:27 -0400 Subject: [PATCH 5/6] stages/authenticator_email: regenerate the API client for the stage_uuid filter --- packages/client-ts/src/apis/StagesApi.ts | 5 +++++ schema.yml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/packages/client-ts/src/apis/StagesApi.ts b/packages/client-ts/src/apis/StagesApi.ts index 79b7220aa059..f144d985c9b6 100644 --- a/packages/client-ts/src/apis/StagesApi.ts +++ b/packages/client-ts/src/apis/StagesApi.ts @@ -649,6 +649,7 @@ export interface StagesAuthenticatorEmailListRequest { * A search term. */ search?: string; + stageUuid?: string; subject?: string; template?: string; timeout?: number; @@ -4162,6 +4163,10 @@ export class StagesApi extends runtime.BaseAPI { queryParameters["search"] = requestParameters["search"]; } + if (requestParameters["stageUuid"] != null) { + queryParameters["stage_uuid"] = requestParameters["stageUuid"]; + } + if (requestParameters["subject"] != null) { queryParameters["subject"] = requestParameters["subject"]; } diff --git a/schema.yml b/schema.yml index 0bf574b68a26..ce494e6b688a 100644 --- a/schema.yml +++ b/schema.yml @@ -29541,6 +29541,11 @@ paths: schema: type: integer - $ref: '#/components/parameters/QuerySearch' + - in: query + name: stage_uuid + schema: + type: string + format: uuid - in: query name: subject schema: From 12b40b01cd06a3683149a7c1803203bc0aade161 Mon Sep 17 00:00:00 2001 From: Dominic R Date: Sat, 10 Oct 2026 17:35:27 -0400 Subject: [PATCH 6/6] stages/captcha: require the private key on the model and share the migration --- .../migrations/0005_managed_secrets.py | 31 ++----------------- authentik/stages/captcha/api.py | 3 -- .../migrations/0006_managed_secrets.py | 24 ++------------ authentik/stages/captcha/models.py | 1 - .../email/migrations/0007_managed_secrets.py | 23 ++------------ 5 files changed, 6 insertions(+), 76 deletions(-) diff --git a/authentik/stages/authenticator_email/migrations/0005_managed_secrets.py b/authentik/stages/authenticator_email/migrations/0005_managed_secrets.py index 9b123d1e0890..0fc692dcd9dc 100644 --- a/authentik/stages/authenticator_email/migrations/0005_managed_secrets.py +++ b/authentik/stages/authenticator_email/migrations/0005_managed_secrets.py @@ -3,38 +3,11 @@ import django.db.models.deletion from django.db import migrations, models -from authentik.crypto.secrets.migrations._credential_values import ( - migrate_credentials, - restore_credentials, -) -from authentik.crypto.secrets.migrations._permissions import preserve_permissions +from authentik.crypto.secrets.migrations._credential_values import move_credentials FIELDS = [("password", "password_ref", "text", "SMTP password")] -def forwards(apps, schema_editor): - migrate_credentials( - apps, - schema_editor, - "authentik_stages_authenticator_email", - "AuthenticatorEmailStage", - FIELDS, - ) - preserve_permissions( - apps, schema_editor, "authentik_stages_authenticator_email", "authenticatoremailstage" - ) - - -def backwards(apps, schema_editor): - restore_credentials( - apps, - schema_editor, - "authentik_stages_authenticator_email", - "AuthenticatorEmailStage", - FIELDS, - ) - - class Migration(migrations.Migration): dependencies = [ @@ -58,5 +31,5 @@ class Migration(migrations.Migration): verbose_name="SMTP password", ), ), - migrations.RunPython(forwards, backwards), + move_credentials("authentik_stages_authenticator_email", "AuthenticatorEmailStage", FIELDS), ] diff --git a/authentik/stages/captcha/api.py b/authentik/stages/captcha/api.py index d5e5b430c68d..156fb946c80a 100644 --- a/authentik/stages/captcha/api.py +++ b/authentik/stages/captcha/api.py @@ -23,9 +23,6 @@ class Meta: "score_max_threshold", "error_on_invalid_score", ] - extra_kwargs = { - "private_key_ref": {"required": True, "allow_null": False}, - } class CaptchaStageViewSet(UsedByMixin, ModelViewSet): diff --git a/authentik/stages/captcha/migrations/0006_managed_secrets.py b/authentik/stages/captcha/migrations/0006_managed_secrets.py index 4b93cb007485..af8dec406d30 100644 --- a/authentik/stages/captcha/migrations/0006_managed_secrets.py +++ b/authentik/stages/captcha/migrations/0006_managed_secrets.py @@ -3,30 +3,11 @@ import django.db.models.deletion from django.db import migrations, models -from authentik.crypto.secrets.migrations._credential_values import ( - migrate_credentials, - restore_credentials, -) -from authentik.crypto.secrets.migrations._permissions import preserve_permissions +from authentik.crypto.secrets.migrations._credential_values import move_credentials FIELDS = [("private_key", "private_key_ref", "text", "captcha private key")] -def forwards(apps, schema_editor): - migrate_credentials( - apps, - schema_editor, - "authentik_stages_captcha", - "CaptchaStage", - FIELDS, - ) - preserve_permissions(apps, schema_editor, "authentik_stages_captcha", "captchastage") - - -def backwards(apps, schema_editor): - restore_credentials(apps, schema_editor, "authentik_stages_captcha", "CaptchaStage", FIELDS) - - class Migration(migrations.Migration): dependencies = [ @@ -41,7 +22,6 @@ class Migration(migrations.Migration): model_name="captchastage", name="private_key_ref", field=models.ForeignKey( - blank=True, default=None, help_text="Private key, acquired your captcha Provider.", null=True, @@ -51,5 +31,5 @@ class Migration(migrations.Migration): verbose_name="Private key", ), ), - migrations.RunPython(forwards, backwards), + move_credentials("authentik_stages_captcha", "CaptchaStage", FIELDS), ] diff --git a/authentik/stages/captcha/models.py b/authentik/stages/captcha/models.py index 190d67ab7da9..6a4b68104043 100644 --- a/authentik/stages/captcha/models.py +++ b/authentik/stages/captcha/models.py @@ -27,7 +27,6 @@ class CaptchaStage(Stage): help_text=_("Private key, acquired your captcha Provider."), on_delete=models.PROTECT, null=True, - blank=True, default=None, related_name="captcha_stages", ) diff --git a/authentik/stages/email/migrations/0007_managed_secrets.py b/authentik/stages/email/migrations/0007_managed_secrets.py index 66bbfc855344..2b25405d074c 100644 --- a/authentik/stages/email/migrations/0007_managed_secrets.py +++ b/authentik/stages/email/migrations/0007_managed_secrets.py @@ -3,30 +3,11 @@ import django.db.models.deletion from django.db import migrations, models -from authentik.crypto.secrets.migrations._credential_values import ( - migrate_credentials, - restore_credentials, -) -from authentik.crypto.secrets.migrations._permissions import preserve_permissions +from authentik.crypto.secrets.migrations._credential_values import move_credentials FIELDS = [("password", "password_ref", "text", "SMTP password")] -def forwards(apps, schema_editor): - migrate_credentials( - apps, - schema_editor, - "authentik_stages_email", - "EmailStage", - FIELDS, - ) - preserve_permissions(apps, schema_editor, "authentik_stages_email", "emailstage") - - -def backwards(apps, schema_editor): - restore_credentials(apps, schema_editor, "authentik_stages_email", "EmailStage", FIELDS) - - class Migration(migrations.Migration): dependencies = [ @@ -50,5 +31,5 @@ class Migration(migrations.Migration): verbose_name="SMTP password", ), ), - migrations.RunPython(forwards, backwards), + move_credentials("authentik_stages_email", "EmailStage", FIELDS), ]