Skip to content

Commit

Permalink
web: provide 'show password' button (#10337)
Browse files Browse the repository at this point in the history
* web: fix esbuild issue with style sheets

Getting ESBuild, Lit, and Storybook to all agree on how to read and parse stylesheets is a serious
pain. This fix better identifies the value types (instances) being passed from various sources in
the repo to the three *different* kinds of style processors we're using (the native one, the
polyfill one, and whatever the heck Storybook does internally).

Falling back to using older CSS instantiating techniques one era at a time seems to do the trick.
It's ugly, but in the face of the aggressive styling we use to avoid Flashes of Unstyled Content
(FLoUC), it's the logic with which we're left.

In standard mode, the following warning appears on the console when running a Flow:

```
Autofocus processing was blocked because a document already has a focused element.
```

In compatibility mode, the following **error** appears on the console when running a Flow:

```
crawler-inject.js:1106 Uncaught TypeError: Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'.
    at initDomMutationObservers (crawler-inject.js:1106:18)
    at crawler-inject.js:1114:24
    at Array.forEach (<anonymous>)
    at initDomMutationObservers (crawler-inject.js:1114:10)
    at crawler-inject.js:1549:1
initDomMutationObservers @ crawler-inject.js:1106
(anonymous) @ crawler-inject.js:1114
initDomMutationObservers @ crawler-inject.js:1114
(anonymous) @ crawler-inject.js:1549
```

Despite this error, nothing seems to be broken and flows work as anticipated.

* web: provide `show password` on login page

Provide a `show password` icon, text, and button for the password field both in the
IdentificationStage and the PasswordStage. Essentially the same code for both, although the id of
the password field is unique to each.

Requested by Cloudflare.  Seems to be a common thing anyway.

Should it be an administrative option that this facility is available?  From where should I derive
that information?  I suspect the answer is "a site attribute," but I'd like to get confirmation.

* web: comment doesn't need to be exposed. It's sufficient where it is .

* web: fix button rendering issues

During testing, the buttons did not change as expected.  We are using pure DOM
state to control the look of the button, and avoiding using `.requestUpdate()`
to avoid losing customer input, so depending upon Lit to re-render just the
button was an error.

This commit goes old-school and updates the button's label and icon using
standard DOM features, although we do lean into Lit-html`s `render()`
function to create the DOM component for the icon.

* web: provide `show password` on login page

Provide a `show password` icon, text, and button for the password field both in the
IdentificationStage and the PasswordStage. Essentially the same code for both, although the id of
the password field is unique to each.

Provide a configuration detail server-side to allow administrator to enable or disable the 'show
password' feature.  Off by default.

Requested by Cloudflare.  Seems to be a common thing anyway.  Making it configurable wasn't in
Cloudfare's request, but it seemed logical to add.

* ensure the tests pass; quibbling over the wording of the admin field continues.

* Removed some manually identified fluff.

* web: break out `show password`-enabled input field into its own component

Provides a `show password` field, but as a LightDOM-oriented web component. This form of
input[type="password"] is for flows only, as it has a number of specializations for understanding a
flow's validating round-trip, possible error messages within the challenge, and is left within the
LightDOM both to support compatibility issues and to avoid using `elementInterals`, which is a DOM
feature not supported by some older browsers.

Avoids having to maintain two different instances of the same logic, both for permitting 'show
password', and for handling it.

* web: update PasswordStageForm according to lit-analyzer

With lit-analyzer in the mix and functional, we're seeing new complaints about
inconsistent typing in lit objects, and this was one of them.

* Another lit-analyze error found.
  • Loading branch information
kensternberg-authentik authored Jul 16, 2024
1 parent d0a4590 commit b0507d2
Show file tree
Hide file tree
Showing 13 changed files with 312 additions and 97 deletions.
1 change: 1 addition & 0 deletions authentik/flows/tests/test_inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def test(self):
self.assertJSONEqual(
res.content,
{
"allow_show_password": False,
"component": "ak-stage-identification",
"flow_info": {
"background": flow.background_url,
Expand Down
3 changes: 2 additions & 1 deletion authentik/stages/identification/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."),
Expand Down
3 changes: 3 additions & 0 deletions authentik/stages/identification/stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -197,6 +198,8 @@ def get_challenge(self) -> Challenge:
"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,
}
Expand Down
2 changes: 2 additions & 0 deletions authentik/stages/password/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class Meta:
"backends",
"configure_flow",
"failed_attempts_before_cancel",
"allow_show_password",
]


Expand All @@ -28,6 +29,7 @@ class PasswordStageViewSet(UsedByMixin, ModelViewSet):
"name",
"configure_flow",
"failed_attempts_before_cancel",
"allow_show_password",
]
search_fields = ["name"]
ordering = ["name"]
Original file line number Diff line number Diff line change
@@ -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.",
),
),
]
6 changes: 6 additions & 0 deletions authentik/stages/password/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
10 changes: 8 additions & 2 deletions authentik/stages/password/stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
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

Expand Down Expand Up @@ -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"""
Expand Down Expand Up @@ -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(
Expand Down
7 changes: 6 additions & 1 deletion blueprints/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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": []
Expand Down
28 changes: 25 additions & 3 deletions schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -37067,6 +37071,9 @@ components:
nullable: true
password_fields:
type: boolean
allow_show_password:
type: boolean
default: false
application_pre:
type: string
flow_designation:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -40953,6 +40960,9 @@ components:
type: string
recovery_url:
type: string
allow_show_password:
type: boolean
default: false
required:
- pending_user
- pending_user_avatar
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
15 changes: 10 additions & 5 deletions web/src/admin/stages/password/PasswordStageForm.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
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";

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,
Expand Down Expand Up @@ -72,10 +71,10 @@ export class PasswordStageForm extends BaseStageForm<PasswordStage> {
return html` <span>
${msg("Validate the user's password against the selected backend(s).")}
</span>
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
<ak-form-element-horizontal label=${msg("Name")} required name="name">
<input
type="text"
value="${ifDefined(this.instance?.name || "")}"
value="${this.instance?.name || ""}"
class="pf-c-form-control"
required
/>
Expand Down Expand Up @@ -158,7 +157,7 @@ export class PasswordStageForm extends BaseStageForm<PasswordStage> {
>
<input
type="number"
value="${first(this.instance?.failedAttemptsBeforeCancel, 5)}"
value="${this.instance?.failedAttemptsBeforeCancel ?? 5}"
class="pf-c-form-control"
required
/>
Expand All @@ -168,6 +167,12 @@ export class PasswordStageForm extends BaseStageForm<PasswordStage> {
)}
</p>
</ak-form-element-horizontal>
<ak-switch-input
name="allowShowPassword"
label="Allow Show Password"
?checked=${this.instance?.allowShowPassword ?? false}
help=${msg("Provide users with a 'show password' button.")}
></ak-switch-input>
</div>
</ak-form-group>`;
}
Expand Down
Loading

0 comments on commit b0507d2

Please sign in to comment.