stages/identification: allow setting of a password stage to check password and identity in a single step

closes #970

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-06-05 14:02:15 +02:00
parent f996f9d4e3
commit 24da24b5d5
16 changed files with 260 additions and 49 deletions

View File

@ -2,6 +2,7 @@
from traceback import format_tb from traceback import format_tb
from typing import Any, Optional from typing import Any, Optional
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import Http404, HttpRequest, HttpResponse, HttpResponseRedirect from django.http import Http404, HttpRequest, HttpResponse, HttpResponseRedirect
from django.http.request import QueryDict from django.http.request import QueryDict
@ -203,6 +204,8 @@ class FlowExecutorView(APIView):
stage_response = self.current_stage_view.get(request, *args, **kwargs) stage_response = self.current_stage_view.get(request, *args, **kwargs)
return to_stage_response(request, stage_response) return to_stage_response(request, stage_response)
except Exception as exc: # pylint: disable=broad-except except Exception as exc: # pylint: disable=broad-except
if settings.DEBUG or settings.TEST:
raise exc
capture_exception(exc) capture_exception(exc)
self._logger.warning(exc) self._logger.warning(exc)
return to_stage_response(request, FlowErrorResponse(request, exc)) return to_stage_response(request, FlowErrorResponse(request, exc))
@ -242,6 +245,8 @@ class FlowExecutorView(APIView):
stage_response = self.current_stage_view.post(request, *args, **kwargs) stage_response = self.current_stage_view.post(request, *args, **kwargs)
return to_stage_response(request, stage_response) return to_stage_response(request, stage_response)
except Exception as exc: # pylint: disable=broad-except except Exception as exc: # pylint: disable=broad-except
if settings.DEBUG or settings.TEST:
raise exc
capture_exception(exc) capture_exception(exc)
self._logger.warning(exc) self._logger.warning(exc)
return to_stage_response(request, FlowErrorResponse(request, exc)) return to_stage_response(request, FlowErrorResponse(request, exc))

View File

@ -13,6 +13,7 @@ class IdentificationStageSerializer(StageSerializer):
model = IdentificationStage model = IdentificationStage
fields = StageSerializer.Meta.fields + [ fields = StageSerializer.Meta.fields + [
"user_fields", "user_fields",
"password_stage",
"case_insensitive_matching", "case_insensitive_matching",
"show_matched_user", "show_matched_user",
"enrollment_flow", "enrollment_flow",

View File

@ -0,0 +1,26 @@
# Generated by Django 3.2.3 on 2021-06-05 12:30
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_password", "0005_auto_20210402_2221"),
("authentik_stages_identification", "0009_identificationstage_sources"),
]
operations = [
migrations.AddField(
model_name="identificationstage",
name="password_stage",
field=models.ForeignKey(
default=None,
help_text="When set, shows a password field, instead of showing the password field as seaprate step.",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="authentik_stages_password.passwordstage",
),
),
]

View File

@ -9,6 +9,7 @@ from rest_framework.serializers import BaseSerializer
from authentik.core.models import Source from authentik.core.models import Source
from authentik.flows.models import Flow, Stage from authentik.flows.models import Flow, Stage
from authentik.stages.password.models import PasswordStage
class UserFields(models.TextChoices): class UserFields(models.TextChoices):
@ -32,6 +33,16 @@ class IdentificationStage(Stage):
), ),
) )
password_stage = models.ForeignKey(
PasswordStage,
null=True,
default=None,
on_delete=models.SET_NULL,
help_text=_(
"When set, shows a password field, instead of showing the "
"password field as seaprate step.",
),
)
case_insensitive_matching = models.BooleanField( case_insensitive_matching = models.BooleanField(
default=True, default=True,
help_text=_( help_text=_(

View File

@ -1,13 +1,14 @@
"""Identification stage logic""" """Identification stage logic"""
from dataclasses import asdict from dataclasses import asdict
from time import sleep from time import sleep
from typing import Optional from typing import Any, Optional
from django.core.exceptions import PermissionDenied
from django.db.models import Q from django.db.models import Q
from django.http import HttpResponse from django.http import HttpResponse
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from rest_framework.fields import CharField, ListField from rest_framework.fields import BooleanField, CharField, ListField
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
@ -22,6 +23,7 @@ from authentik.flows.stage import (
from authentik.flows.views import SESSION_KEY_APPLICATION_PRE from authentik.flows.views import SESSION_KEY_APPLICATION_PRE
from authentik.stages.identification.models import IdentificationStage from authentik.stages.identification.models import IdentificationStage
from authentik.stages.identification.signals import identification_failed from authentik.stages.identification.signals import identification_failed
from authentik.stages.password.stage import authenticate
LOGGER = get_logger() LOGGER = get_logger()
@ -30,6 +32,7 @@ class IdentificationChallenge(Challenge):
"""Identification challenges with all UI elements""" """Identification challenges with all UI elements"""
user_fields = ListField(child=CharField(), allow_empty=True, allow_null=True) user_fields = ListField(child=CharField(), allow_empty=True, allow_null=True)
password_fields = BooleanField()
application_pre = CharField(required=False) application_pre = CharField(required=False)
enroll_url = CharField(required=False) enroll_url = CharField(required=False)
@ -44,22 +47,43 @@ class IdentificationChallengeResponse(ChallengeResponse):
"""Identification challenge""" """Identification challenge"""
uid_field = CharField() uid_field = CharField()
password = CharField(required=False, allow_blank=True, allow_null=True)
component = CharField(default="ak-stage-identification") component = CharField(default="ak-stage-identification")
pre_user: Optional[User] = None pre_user: Optional[User] = None
def validate_uid_field(self, value: str) -> str: def validate(self, data: dict[str, Any]) -> dict[str, Any]:
"""Validate that user exists""" """Validate that user exists, and optionally their password"""
pre_user = self.stage.get_user(value) uid_field = data["uid_field"]
current_stage: IdentificationStage = self.stage.executor.current_stage
pre_user = self.stage.get_user(uid_field)
if not pre_user: if not pre_user:
sleep(0.150) sleep(0.150)
LOGGER.debug("invalid_login", identifier=value) LOGGER.debug("invalid_login", identifier=uid_field)
identification_failed.send( identification_failed.send(
sender=self, request=self.stage.request, uid_field=value sender=self, request=self.stage.request, uid_field=uid_field
) )
raise ValidationError("Failed to authenticate.") raise ValidationError("Failed to authenticate.")
self.pre_user = pre_user self.pre_user = pre_user
return value if not current_stage.password_stage:
# No password stage select, don't validate the password
return data
password = data["password"]
try:
user = authenticate(
self.stage.request,
current_stage.password_stage.backends,
username=self.pre_user.username,
password=password,
)
if not user:
raise ValidationError("Failed to authenticate.")
self.pre_user = user
except PermissionDenied as exc:
raise ValidationError(str(exc)) from exc
return data
class IdentificationStageView(ChallengeStageView): class IdentificationStageView(ChallengeStageView):
@ -92,6 +116,7 @@ class IdentificationStageView(ChallengeStageView):
"primary_action": _("Log in"), "primary_action": _("Log in"),
"component": "ak-stage-identification", "component": "ak-stage-identification",
"user_fields": current_stage.user_fields, "user_fields": current_stage.user_fields,
"password_fields": bool(current_stage.password_stage),
} }
) )
# If the user has been redirected to us whilst trying to access an # If the user has been redirected to us whilst trying to access an

View File

@ -6,8 +6,10 @@ from django.utils.encoding import force_str
from authentik.core.models import User from authentik.core.models import User
from authentik.flows.challenge import ChallengeTypes from authentik.flows.challenge import ChallengeTypes
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.providers.oauth2.generators import generate_client_secret
from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.models import OAuthSource
from authentik.stages.identification.models import IdentificationStage, UserFields from authentik.stages.identification.models import IdentificationStage, UserFields
from authentik.stages.password.models import PasswordStage
class TestIdentificationStage(TestCase): class TestIdentificationStage(TestCase):
@ -15,7 +17,10 @@ class TestIdentificationStage(TestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.user = User.objects.create(username="unittest", email="test@beryju.org") self.password = generate_client_secret()
self.user = User.objects.create_user(
username="unittest", email="test@beryju.org", password=self.password
)
self.client = Client() self.client = Client()
# OAuthSource for the login view # OAuthSource for the login view
@ -62,6 +67,73 @@ class TestIdentificationStage(TestCase):
}, },
) )
def test_valid_with_password(self):
"""Test with valid email and password in single step"""
pw_stage = PasswordStage.objects.create(
name="password", backends=["django.contrib.auth.backends.ModelBackend"]
)
self.stage.password_stage = pw_stage
self.stage.save()
form_data = {"uid_field": self.user.email, "password": self.password}
url = reverse(
"authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}
)
response = self.client.post(url, form_data)
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{
"component": "xak-flow-redirect",
"to": reverse("authentik_core:root-redirect"),
"type": ChallengeTypes.REDIRECT.value,
},
)
def test_invalid_with_password(self):
"""Test with valid email and invalid password in single step"""
pw_stage = PasswordStage.objects.create(
name="password", backends=["django.contrib.auth.backends.ModelBackend"]
)
self.stage.password_stage = pw_stage
self.stage.save()
form_data = {
"uid_field": self.user.email,
"password": self.password + "test",
}
url = reverse(
"authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}
)
response = self.client.post(url, form_data)
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{
"background": self.flow.background.url,
"type": ChallengeTypes.NATIVE.value,
"component": "ak-stage-identification",
"password_fields": True,
"primary_action": "Log in",
"response_errors": {
"non_field_errors": [
{"code": "invalid", "string": "Failed to " "authenticate."}
]
},
"sources": [
{
"challenge": {
"component": "xak-flow-redirect",
"to": "/source/oauth/login/test/",
"type": ChallengeTypes.REDIRECT.value,
},
"icon_url": "/static/authentik/sources/.svg",
"name": "test",
}
],
"title": "",
"user_fields": ["email"],
},
)
def test_invalid_with_username(self): def test_invalid_with_username(self):
"""Test invalid with username (user exists but stage only allows email)""" """Test invalid with username (user exists but stage only allows email)"""
form_data = {"uid_field": self.user.username} form_data = {"uid_field": self.user.username}
@ -113,6 +185,7 @@ class TestIdentificationStage(TestCase):
"type": ChallengeTypes.NATIVE.value, "type": ChallengeTypes.NATIVE.value,
"component": "ak-stage-identification", "component": "ak-stage-identification",
"user_fields": ["email"], "user_fields": ["email"],
"password_fields": False,
"enroll_url": reverse( "enroll_url": reverse(
"authentik_core:if-flow", "authentik_core:if-flow",
kwargs={"flow_slug": "unique-enrollment-string"}, kwargs={"flow_slug": "unique-enrollment-string"},
@ -160,6 +233,7 @@ class TestIdentificationStage(TestCase):
"type": ChallengeTypes.NATIVE.value, "type": ChallengeTypes.NATIVE.value,
"component": "ak-stage-identification", "component": "ak-stage-identification",
"user_fields": ["email"], "user_fields": ["email"],
"password_fields": False,
"recovery_url": reverse( "recovery_url": reverse(
"authentik_core:if-flow", "authentik_core:if-flow",
kwargs={"flow_slug": "unique-recovery-string"}, kwargs={"flow_slug": "unique-recovery-string"},

View File

@ -1,6 +1,4 @@
"""password tests""" """password tests"""
import string
from random import SystemRandom
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
@ -15,6 +13,7 @@ from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK
from authentik.flows.views import SESSION_KEY_PLAN from authentik.flows.views import SESSION_KEY_PLAN
from authentik.providers.oauth2.generators import generate_client_secret
from authentik.stages.password.models import PasswordStage from authentik.stages.password.models import PasswordStage
MOCK_BACKEND_AUTHENTICATE = MagicMock(side_effect=PermissionDenied("test")) MOCK_BACKEND_AUTHENTICATE = MagicMock(side_effect=PermissionDenied("test"))
@ -25,10 +24,7 @@ class TestPasswordStage(TestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.password = "".join( self.password = generate_client_secret()
SystemRandom().choice(string.ascii_uppercase + string.digits)
for _ in range(8)
)
self.user = User.objects.create_user( self.user = User.objects.create_user(
username="unittest", email="test@beryju.org", password=self.password username="unittest", email="test@beryju.org", password=self.password
) )

View File

@ -17679,6 +17679,8 @@ components:
items: items:
type: string type: string
nullable: true nullable: true
password_fields:
type: boolean
application_pre: application_pre:
type: string type: string
enroll_url: enroll_url:
@ -17692,6 +17694,7 @@ components:
items: items:
$ref: '#/components/schemas/UILoginButton' $ref: '#/components/schemas/UILoginButton'
required: required:
- password_fields
- primary_action - primary_action
- type - type
- user_fields - user_fields
@ -17704,6 +17707,9 @@ components:
default: ak-stage-identification default: ak-stage-identification
uid_field: uid_field:
type: string type: string
password:
type: string
nullable: true
required: required:
- uid_field - uid_field
IdentificationStage: IdentificationStage:
@ -17736,6 +17742,12 @@ components:
$ref: '#/components/schemas/UserFieldsEnum' $ref: '#/components/schemas/UserFieldsEnum'
description: Fields of the user object to match against. (Hold shift to description: Fields of the user object to match against. (Hold shift to
select multiple options) select multiple options)
password_stage:
type: string
format: uuid
nullable: true
description: When set, shows a password field, instead of showing the password
field as seaprate step.
case_insensitive_matching: case_insensitive_matching:
type: boolean type: boolean
description: When enabled, user fields are matched regardless of their casing. description: When enabled, user fields are matched regardless of their casing.
@ -17784,6 +17796,12 @@ components:
$ref: '#/components/schemas/UserFieldsEnum' $ref: '#/components/schemas/UserFieldsEnum'
description: Fields of the user object to match against. (Hold shift to description: Fields of the user object to match against. (Hold shift to
select multiple options) select multiple options)
password_stage:
type: string
format: uuid
nullable: true
description: When set, shows a password field, instead of showing the password
field as seaprate step.
case_insensitive_matching: case_insensitive_matching:
type: boolean type: boolean
description: When enabled, user fields are matched regardless of their casing. description: When enabled, user fields are matched regardless of their casing.
@ -22307,6 +22325,12 @@ components:
$ref: '#/components/schemas/UserFieldsEnum' $ref: '#/components/schemas/UserFieldsEnum'
description: Fields of the user object to match against. (Hold shift to description: Fields of the user object to match against. (Hold shift to
select multiple options) select multiple options)
password_stage:
type: string
format: uuid
nullable: true
description: When set, shows a password field, instead of showing the password
field as seaprate step.
case_insensitive_matching: case_insensitive_matching:
type: boolean type: boolean
description: When enabled, user fields are matched regardless of their casing. description: When enabled, user fields are matched regardless of their casing.

View File

@ -1,10 +0,0 @@
import { ChallengeChoices } from "authentik-api";
export interface Error {
code: string;
string: string;
}
export interface ErrorDict {
[key: string]: Error[];
}

View File

@ -1,8 +1,8 @@
import { customElement, LitElement, CSSResult, property, css } from "lit-element"; import { customElement, LitElement, CSSResult, property, css } from "lit-element";
import { TemplateResult, html } from "lit-html"; import { TemplateResult, html } from "lit-html";
import { Error } from "../../api/Flows";
import PFForm from "@patternfly/patternfly/components/Form/form.css"; import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import { ErrorDetail } from "authentik-api";
@customElement("ak-form-element") @customElement("ak-form-element")
export class FormElement extends LitElement { export class FormElement extends LitElement {
@ -25,7 +25,7 @@ export class FormElement extends LitElement {
required = false; required = false;
@property({ attribute: false }) @property({ attribute: false })
errors?: Error[]; errors?: ErrorDetail[];
updated(): void { updated(): void {
this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach(input => { this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach(input => {

View File

@ -1,4 +1,5 @@
import { LitElement, property } from "lit-element"; import { ErrorDetail } from "authentik-api";
import { html, LitElement, property, TemplateResult } from "lit-element";
export interface StageHost { export interface StageHost {
challenge?: unknown; challenge?: unknown;
@ -22,4 +23,23 @@ export class BaseStage<Tin, Tout> extends LitElement {
this.host?.submit(object as unknown as Tout); this.host?.submit(object as unknown as Tout);
} }
renderNonFieldErrors(errors: ErrorDetail[]): TemplateResult {
if (!errors) {
return html``;
}
return html`<div class="pf-c-form__alert">
${errors.map(err => {
return html`<div class="pf-c-alert pf-m-inline pf-m-danger">
<div class="pf-c-alert__icon">
<i class="fas fa-exclamation-circle"></i>
</div>
<h4 class="pf-c-alert__title">
${err.string}
</h4>
</div>`;
})}
</div>`;
}
} }

View File

@ -7,6 +7,7 @@ import PFFormControl from "@patternfly/patternfly/components/FormControl/form-co
import PFTitle from "@patternfly/patternfly/components/Title/title.css"; import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import AKGlobal from "../../../authentik.css"; import AKGlobal from "../../../authentik.css";
import "../../../elements/forms/FormElement"; import "../../../elements/forms/FormElement";
import "../../../elements/EmptyState"; import "../../../elements/EmptyState";
@ -25,7 +26,7 @@ export const PasswordManagerPrefill: {
export class IdentificationStage extends BaseStage<IdentificationChallenge, IdentificationChallengeResponseRequest> { export class IdentificationStage extends BaseStage<IdentificationChallenge, IdentificationChallengeResponseRequest> {
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal].concat( return [PFBase, PFAlert, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal].concat(
css` css`
/* login page's icons */ /* login page's icons */
.pf-c-login__main-footer-links-item button { .pf-c-login__main-footer-links-item button {
@ -160,7 +161,7 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
label=${label} label=${label}
?required="${true}" ?required="${true}"
class="pf-c-form__group" class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})["uid_field"]}> .errors=${(this.challenge.responseErrors || {})["uid_field"]}>
<!-- @ts-ignore --> <!-- @ts-ignore -->
<input type=${type} <input type=${type}
name="uidField" name="uidField"
@ -170,6 +171,25 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
class="pf-c-form-control" class="pf-c-form-control"
required> required>
</ak-form-element> </ak-form-element>
${this.challenge.passwordFields ? html`
<ak-form-element
label="${t`Password`}"
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge.responseErrors || {})["password"]}>
<input type="password"
name="password"
placeholder="${t`Password`}"
autofocus=""
autocomplete="current-password"
class="pf-c-form-control"
required
value=${PasswordManagerPrefill.password || ""}>
</ak-form-element>
`: html``}
${"non_field_errors" in (this.challenge?.responseErrors || {}) ?
this.renderNonFieldErrors(this.challenge?.responseErrors?.non_field_errors || []) :
html``}
<div class="pf-c-form__group pf-m-action"> <div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block"> <button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${this.challenge.primaryAction} ${this.challenge.primaryAction}

View File

@ -13,7 +13,6 @@ import { BaseStage } from "../base";
import "../../../elements/forms/FormElement"; import "../../../elements/forms/FormElement";
import "../../../elements/EmptyState"; import "../../../elements/EmptyState";
import "../../../elements/Divider"; import "../../../elements/Divider";
import { Error } from "../../../api/Flows";
import { PromptChallenge, PromptChallengeResponseRequest, StagePrompt } from "authentik-api"; import { PromptChallenge, PromptChallengeResponseRequest, StagePrompt } from "authentik-api";
@ -103,24 +102,6 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
return ""; return "";
} }
renderNonFieldErrors(errors: Error[]): TemplateResult {
if (!errors) {
return html``;
}
return html`<div class="pf-c-form__alert">
${errors.map(err => {
return html`<div class="pf-c-alert pf-m-inline pf-m-danger">
<div class="pf-c-alert__icon">
<i class="fas fa-exclamation-circle"></i>
</div>
<h4 class="pf-c-alert__title">
${err.string}
</h4>
</div>`;
})}
</div>`;
}
render(): TemplateResult { render(): TemplateResult {
if (!this.challenge) { if (!this.challenge) {
return html`<ak-empty-state return html`<ak-empty-state

View File

@ -2032,6 +2032,7 @@ msgstr "Loading"
#: src/pages/stages/identification/IdentificationStageForm.ts #: src/pages/stages/identification/IdentificationStageForm.ts
#: src/pages/stages/identification/IdentificationStageForm.ts #: src/pages/stages/identification/IdentificationStageForm.ts
#: src/pages/stages/identification/IdentificationStageForm.ts #: src/pages/stages/identification/IdentificationStageForm.ts
#: src/pages/stages/identification/IdentificationStageForm.ts
#: src/pages/stages/password/PasswordStageForm.ts #: src/pages/stages/password/PasswordStageForm.ts
#: src/pages/stages/prompt/PromptStageForm.ts #: src/pages/stages/prompt/PromptStageForm.ts
#: src/pages/stages/prompt/PromptStageForm.ts #: src/pages/stages/prompt/PromptStageForm.ts
@ -2549,6 +2550,8 @@ msgstr "Pass policy?"
msgid "Passing" msgid "Passing"
msgstr "Passing" msgstr "Passing"
#: src/flows/stages/identification/IdentificationStage.ts
#: src/flows/stages/identification/IdentificationStage.ts
#: src/flows/stages/password/PasswordStage.ts #: src/flows/stages/password/PasswordStage.ts
msgid "Password" msgid "Password"
msgstr "Password" msgstr "Password"
@ -2558,6 +2561,10 @@ msgstr "Password"
msgid "Password field" msgid "Password field"
msgstr "Password field" msgstr "Password field"
#: src/pages/stages/identification/IdentificationStageForm.ts
msgid "Password stage"
msgstr "Password stage"
#: src/pages/stages/prompt/PromptForm.ts #: src/pages/stages/prompt/PromptForm.ts
msgid "Password: Masked input, password is validated against sources. Policies still have to be applied to this Stage. If two of these are used in the same stage, they are ensured to be identical." msgid "Password: Masked input, password is validated against sources. Policies still have to be applied to this Stage. If two of these are used in the same stage, they are ensured to be identical."
msgstr "Password: Masked input, password is validated against sources. Policies still have to be applied to this Stage. If two of these are used in the same stage, they are ensured to be identical." msgstr "Password: Masked input, password is validated against sources. Policies still have to be applied to this Stage. If two of these are used in the same stage, they are ensured to be identical."
@ -4354,6 +4361,10 @@ msgstr "When enabled, the invitation will be deleted after usage."
msgid "When enabled, user fields are matched regardless of their casing." msgid "When enabled, user fields are matched regardless of their casing."
msgstr "When enabled, user fields are matched regardless of their casing." msgstr "When enabled, user fields are matched regardless of their casing."
#: src/pages/stages/identification/IdentificationStageForm.ts
msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks."
msgstr "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks."
#: src/pages/providers/saml/SAMLProviderForm.ts #: src/pages/providers/saml/SAMLProviderForm.ts
msgid "When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default." msgid "When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default."
msgstr "When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default." msgstr "When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default."

View File

@ -2031,6 +2031,7 @@ msgstr ""
#: #:
#: #:
#: #:
#:
msgid "Loading..." msgid "Loading..."
msgstr "" msgstr ""
@ -2541,6 +2542,8 @@ msgstr ""
msgid "Passing" msgid "Passing"
msgstr "" msgstr ""
#:
#:
#: #:
msgid "Password" msgid "Password"
msgstr "" msgstr ""
@ -2550,6 +2553,10 @@ msgstr ""
msgid "Password field" msgid "Password field"
msgstr "" msgstr ""
#:
msgid "Password stage"
msgstr ""
#: #:
msgid "Password: Masked input, password is validated against sources. Policies still have to be applied to this Stage. If two of these are used in the same stage, they are ensured to be identical." msgid "Password: Masked input, password is validated against sources. Policies still have to be applied to this Stage. If two of these are used in the same stage, they are ensured to be identical."
msgstr "" msgstr ""
@ -4342,6 +4349,10 @@ msgstr ""
msgid "When enabled, user fields are matched regardless of their casing." msgid "When enabled, user fields are matched regardless of their casing."
msgstr "" msgstr ""
#:
msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks."
msgstr ""
#: #:
msgid "When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default." msgid "When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default."
msgstr "" msgstr ""

View File

@ -76,6 +76,22 @@ export class IdentificationStageForm extends ModelForm<IdentificationStage, stri
<p class="pf-c-form__helper-text">${t`Fields a user can identify themselves with. If no fields are selected, the user will only be able to use sources.`}</p> <p class="pf-c-form__helper-text">${t`Fields a user can identify themselves with. If no fields are selected, the user will only be able to use sources.`}</p>
<p class="pf-c-form__helper-text">${t`Hold control/command to select multiple items.`}</p> <p class="pf-c-form__helper-text">${t`Hold control/command to select multiple items.`}</p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Password stage`}
name="passwordStage">
<select class="pf-c-form-control">
<option value="" ?selected=${this.instance?.passwordStage === undefined}>---------</option>
${until(new StagesApi(DEFAULT_CONFIG).stagesPasswordList({
ordering: "pk",
}).then(stages => {
return stages.results.map(stage => {
const selected = this.instance?.passwordStage === stage.pk;
return html`<option value=${ifDefined(stage.pk)} ?selected=${selected}>${stage.name}</option>`;
});
}), html`<option>${t`Loading...`}</option>`)}
</select>
<p class="pf-c-form__helper-text">${t`When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks.`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="caseInsensitiveMatching"> <ak-form-element-horizontal name="caseInsensitiveMatching">
<div class="pf-c-check"> <div class="pf-c-check">
<input type="checkbox" class="pf-c-check__input" ?checked=${first(this.instance?.caseInsensitiveMatching, true)}> <input type="checkbox" class="pf-c-check__input" ?checked=${first(this.instance?.caseInsensitiveMatching, true)}>