diff --git a/authentik/flows/tests/test_inspector.py b/authentik/flows/tests/test_inspector.py
index 59ca809066..2a01ea370c 100644
--- a/authentik/flows/tests/test_inspector.py
+++ b/authentik/flows/tests/test_inspector.py
@@ -45,6 +45,7 @@ class TestFlowInspector(APITestCase):
self.assertJSONEqual(
res.content,
{
+ "allow_show_password": False,
"component": "ak-stage-identification",
"flow_info": {
"background": flow.background_url,
diff --git a/authentik/stages/identification/models.py b/authentik/stages/identification/models.py
index 323b93ebf1..27cfcb92f1 100644
--- a/authentik/stages/identification/models.py
+++ b/authentik/stages/identification/models.py
@@ -38,10 +38,11 @@ class IdentificationStage(Stage):
help_text=_(
(
"When set, shows a password field, instead of showing the "
- "password field as seaprate step."
+ "password field as separate step."
),
),
)
+
case_insensitive_matching = models.BooleanField(
default=True,
help_text=_("When enabled, user fields are matched regardless of their casing."),
diff --git a/authentik/stages/identification/stage.py b/authentik/stages/identification/stage.py
index 129cd14b9a..8c5ecbcd3c 100644
--- a/authentik/stages/identification/stage.py
+++ b/authentik/stages/identification/stage.py
@@ -64,6 +64,7 @@ class IdentificationChallenge(Challenge):
user_fields = ListField(child=CharField(), allow_empty=True, allow_null=True)
password_fields = BooleanField()
+ allow_show_password = BooleanField(default=False)
application_pre = CharField(required=False)
flow_designation = ChoiceField(FlowDesignation.choices)
@@ -197,6 +198,8 @@ class IdentificationStageView(ChallengeStageView):
"primary_action": self.get_primary_action(),
"user_fields": current_stage.user_fields,
"password_fields": bool(current_stage.password_stage),
+ "allow_show_password": bool(current_stage.password_stage)
+ and current_stage.password_stage.allow_show_password,
"show_source_labels": current_stage.show_source_labels,
"flow_designation": self.executor.flow.designation,
}
diff --git a/authentik/stages/password/api.py b/authentik/stages/password/api.py
index 7ede2ade8f..d56dd2b557 100644
--- a/authentik/stages/password/api.py
+++ b/authentik/stages/password/api.py
@@ -16,6 +16,7 @@ class PasswordStageSerializer(StageSerializer):
"backends",
"configure_flow",
"failed_attempts_before_cancel",
+ "allow_show_password",
]
@@ -28,6 +29,7 @@ class PasswordStageViewSet(UsedByMixin, ModelViewSet):
"name",
"configure_flow",
"failed_attempts_before_cancel",
+ "allow_show_password",
]
search_fields = ["name"]
ordering = ["name"]
diff --git a/authentik/stages/password/migrations/0009_passwordstage_allow_show_password.py b/authentik/stages/password/migrations/0009_passwordstage_allow_show_password.py
new file mode 100644
index 0000000000..643edd2b72
--- /dev/null
+++ b/authentik/stages/password/migrations/0009_passwordstage_allow_show_password.py
@@ -0,0 +1,21 @@
+# Generated by Django 5.0.6 on 2024-07-02 18:14
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("authentik_stages_password", "0008_replace_inbuilt"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="passwordstage",
+ name="allow_show_password",
+ field=models.BooleanField(
+ default=False,
+ help_text="When enabled, provides a 'show password' button with the password input field.",
+ ),
+ ),
+ ]
diff --git a/authentik/stages/password/models.py b/authentik/stages/password/models.py
index 9887b68543..a88318b039 100644
--- a/authentik/stages/password/models.py
+++ b/authentik/stages/password/models.py
@@ -43,6 +43,12 @@ class PasswordStage(ConfigurableStage, Stage):
"To lock the user out, use a reputation policy and a user_write stage."
),
)
+ allow_show_password = models.BooleanField(
+ default=False,
+ help_text=_(
+ "When enabled, provides a 'show password' button with the password input field."
+ ),
+ )
@property
def serializer(self) -> type[BaseSerializer]:
diff --git a/authentik/stages/password/stage.py b/authentik/stages/password/stage.py
index 03a6040ded..e8c0bde3e2 100644
--- a/authentik/stages/password/stage.py
+++ b/authentik/stages/password/stage.py
@@ -9,7 +9,7 @@ from django.http import HttpRequest, HttpResponse
from django.urls import reverse
from django.utils.translation import gettext as _
from rest_framework.exceptions import ValidationError
-from rest_framework.fields import CharField
+from rest_framework.fields import BooleanField, CharField
from sentry_sdk.hub import Hub
from structlog.stdlib import get_logger
@@ -76,6 +76,8 @@ class PasswordChallenge(WithUserInfoChallenge):
component = CharField(default="ak-stage-password")
+ allow_show_password = BooleanField(default=False)
+
class PasswordChallengeResponse(ChallengeResponse):
"""Password challenge response"""
@@ -134,7 +136,11 @@ class PasswordStageView(ChallengeStageView):
response_class = PasswordChallengeResponse
def get_challenge(self) -> Challenge:
- challenge = PasswordChallenge(data={})
+ challenge = PasswordChallenge(
+ data={
+ "allow_show_password": self.executor.current_stage.allow_show_password,
+ }
+ )
recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY)
if recovery_flow.exists():
recover_url = reverse(
diff --git a/blueprints/schema.json b/blueprints/schema.json
index ab61d8ca93..a98aa64a0f 100644
--- a/blueprints/schema.json
+++ b/blueprints/schema.json
@@ -6905,7 +6905,7 @@
"password_stage": {
"type": "integer",
"title": "Password stage",
- "description": "When set, shows a password field, instead of showing the password field as seaprate step."
+ "description": "When set, shows a password field, instead of showing the password field as separate step."
},
"case_insensitive_matching": {
"type": "boolean",
@@ -7207,6 +7207,11 @@
"maximum": 2147483647,
"title": "Failed attempts before cancel",
"description": "How many attempts a user has before the flow is canceled. To lock the user out, use a reputation policy and a user_write stage."
+ },
+ "allow_show_password": {
+ "type": "boolean",
+ "title": "Allow show password",
+ "description": "When enabled, provides a 'show password' button with the password input field."
}
},
"required": []
diff --git a/schema.yml b/schema.yml
index f459e3453e..35b30701c1 100644
--- a/schema.yml
+++ b/schema.yml
@@ -29993,6 +29993,10 @@ paths:
operationId: stages_password_list
description: PasswordStage Viewset
parameters:
+ - in: query
+ name: allow_show_password
+ schema:
+ type: boolean
- in: query
name: configure_flow
schema:
@@ -37067,6 +37071,9 @@ components:
nullable: true
password_fields:
type: boolean
+ allow_show_password:
+ type: boolean
+ default: false
application_pre:
type: string
flow_designation:
@@ -37149,7 +37156,7 @@ components:
format: uuid
nullable: true
description: When set, shows a password field, instead of showing the password
- field as seaprate step.
+ field as separate step.
case_insensitive_matching:
type: boolean
description: When enabled, user fields are matched regardless of their casing.
@@ -37217,7 +37224,7 @@ components:
format: uuid
nullable: true
description: When set, shows a password field, instead of showing the password
- field as seaprate step.
+ field as separate step.
case_insensitive_matching:
type: boolean
description: When enabled, user fields are matched regardless of their casing.
@@ -40953,6 +40960,9 @@ components:
type: string
recovery_url:
type: string
+ allow_show_password:
+ type: boolean
+ default: false
required:
- pending_user
- pending_user_avatar
@@ -41235,6 +41245,10 @@ components:
minimum: -2147483648
description: How many attempts a user has before the flow is canceled. To
lock the user out, use a reputation policy and a user_write stage.
+ allow_show_password:
+ type: boolean
+ description: When enabled, provides a 'show password' button with the password
+ input field.
required:
- backends
- component
@@ -41271,6 +41285,10 @@ components:
minimum: -2147483648
description: How many attempts a user has before the flow is canceled. To
lock the user out, use a reputation policy and a user_write stage.
+ allow_show_password:
+ type: boolean
+ description: When enabled, provides a 'show password' button with the password
+ input field.
required:
- backends
- name
@@ -42092,7 +42110,7 @@ components:
format: uuid
nullable: true
description: When set, shows a password field, instead of showing the password
- field as seaprate step.
+ field as separate step.
case_insensitive_matching:
type: boolean
description: When enabled, user fields are matched regardless of their casing.
@@ -42804,6 +42822,10 @@ components:
minimum: -2147483648
description: How many attempts a user has before the flow is canceled. To
lock the user out, use a reputation policy and a user_write stage.
+ allow_show_password:
+ type: boolean
+ description: When enabled, provides a 'show password' button with the password
+ input field.
PatchedPermissionAssignRequest:
type: object
description: Request to assign a new permission
diff --git a/web/src/admin/stages/password/PasswordStageForm.ts b/web/src/admin/stages/password/PasswordStageForm.ts
index 339542c973..81c861bdf0 100644
--- a/web/src/admin/stages/password/PasswordStageForm.ts
+++ b/web/src/admin/stages/password/PasswordStageForm.ts
@@ -1,7 +1,7 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
-import { first } from "@goauthentik/common/utils";
+import "@goauthentik/components/ak-switch-input.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
@@ -9,7 +9,6 @@ import "@goauthentik/elements/forms/SearchSelect";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
-import { ifDefined } from "lit/directives/if-defined.js";
import {
BackendsEnum,
@@ -72,10 +71,10 @@ export class PasswordStageForm extends BaseStageForm