stages/identification: auto-redirect to source when no user fields are selected (#5583)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
7265a56f05
commit
61434c807d
|
@ -4,7 +4,7 @@
|
||||||
Please check the [Contributing guidelines](https://github.com/goauthentik/authentik/blob/main/CONTRIBUTING.md#how-can-i-contribute).
|
Please check the [Contributing guidelines](https://github.com/goauthentik/authentik/blob/main/CONTRIBUTING.md#how-can-i-contribute).
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# Details
|
## Details
|
||||||
|
|
||||||
- **Does this resolve an issue?**
|
- **Does this resolve an issue?**
|
||||||
Resolves #
|
Resolves #
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
"""Identification Stage API Views"""
|
"""Identification Stage API Views"""
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
|
@ -9,6 +11,16 @@ from authentik.stages.identification.models import IdentificationStage
|
||||||
class IdentificationStageSerializer(StageSerializer):
|
class IdentificationStageSerializer(StageSerializer):
|
||||||
"""IdentificationStage Serializer"""
|
"""IdentificationStage Serializer"""
|
||||||
|
|
||||||
|
def validate(self, attrs: dict) -> dict:
|
||||||
|
# Check that at least 1 source is selected when no user fields are selected.
|
||||||
|
sources = attrs.get("sources", [])
|
||||||
|
user_fields = attrs.get("user_fields", [])
|
||||||
|
if len(user_fields) < 1 and len(sources) < 1:
|
||||||
|
raise ValidationError(
|
||||||
|
_("When no user fields are selected, at least one source must be selected")
|
||||||
|
)
|
||||||
|
return super().validate(attrs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IdentificationStage
|
model = IdentificationStage
|
||||||
fields = StageSerializer.Meta.fields + [
|
fields = StageSerializer.Meta.fields + [
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
"""identification tests"""
|
"""identification tests"""
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||||
from authentik.flows.challenge import ChallengeTypes
|
from authentik.flows.challenge import ChallengeTypes
|
||||||
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
||||||
from authentik.flows.tests import FlowTestCase
|
from authentik.flows.tests import FlowTestCase
|
||||||
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.sources.oauth.models import OAuthSource
|
from authentik.sources.oauth.models import OAuthSource
|
||||||
|
from authentik.stages.identification.api import IdentificationStageSerializer
|
||||||
from authentik.stages.identification.models import IdentificationStage, UserFields
|
from authentik.stages.identification.models import IdentificationStage, UserFields
|
||||||
from authentik.stages.password import BACKEND_INBUILT
|
from authentik.stages.password import BACKEND_INBUILT
|
||||||
from authentik.stages.password.models import PasswordStage
|
from authentik.stages.password.models import PasswordStage
|
||||||
|
@ -222,3 +225,22 @@ class TestIdentificationStage(FlowTestCase):
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_api_validate(self):
|
||||||
|
"""Test API validation"""
|
||||||
|
self.assertTrue(
|
||||||
|
IdentificationStageSerializer(
|
||||||
|
data={
|
||||||
|
"name": generate_id(),
|
||||||
|
"user_fields": [UserFields.E_MAIL, UserFields.USERNAME],
|
||||||
|
}
|
||||||
|
).is_valid(raise_exception=True)
|
||||||
|
)
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
IdentificationStageSerializer(
|
||||||
|
data={
|
||||||
|
"name": generate_id(),
|
||||||
|
"user_fields": [],
|
||||||
|
"sources": [],
|
||||||
|
}
|
||||||
|
).is_valid(raise_exception=True)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { SeverityToLabel } from "@goauthentik/admin/events/RuleListPage";
|
import { SeverityToLabel } from "@goauthentik/admin/events/utils";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import "@goauthentik/admin/events/RuleForm";
|
import "@goauthentik/admin/events/RuleForm";
|
||||||
|
import { SeverityToLabel } from "@goauthentik/admin/events/utils";
|
||||||
import "@goauthentik/admin/policies/BoundPoliciesList";
|
import "@goauthentik/admin/policies/BoundPoliciesList";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||||
|
@ -14,20 +15,7 @@ import { t } from "@lingui/macro";
|
||||||
import { TemplateResult, html } from "lit";
|
import { TemplateResult, html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
|
||||||
import { EventsApi, NotificationRule, SeverityEnum } from "@goauthentik/api";
|
import { EventsApi, NotificationRule } from "@goauthentik/api";
|
||||||
|
|
||||||
export function SeverityToLabel(severity: SeverityEnum | null | undefined): string {
|
|
||||||
if (!severity) return t`Unknown severity`;
|
|
||||||
switch (severity) {
|
|
||||||
case SeverityEnum.Alert:
|
|
||||||
return t`Alert`;
|
|
||||||
case SeverityEnum.Notice:
|
|
||||||
return t`Notice`;
|
|
||||||
case SeverityEnum.Warning:
|
|
||||||
return t`Warning`;
|
|
||||||
}
|
|
||||||
return t`Unknown severity`;
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("ak-event-rule-list")
|
@customElement("ak-event-rule-list")
|
||||||
export class RuleListPage extends TablePage<NotificationRule> {
|
export class RuleListPage extends TablePage<NotificationRule> {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { t } from "@lingui/macro";
|
||||||
|
|
||||||
import { TemplateResult, html } from "lit";
|
import { TemplateResult, html } from "lit";
|
||||||
|
|
||||||
import { EventActions } from "@goauthentik/api";
|
import { EventActions, SeverityEnum } from "@goauthentik/api";
|
||||||
|
|
||||||
export function EventGeo(event: EventWithContext): TemplateResult {
|
export function EventGeo(event: EventWithContext): TemplateResult {
|
||||||
let geo: KeyUnknown | undefined = undefined;
|
let geo: KeyUnknown | undefined = undefined;
|
||||||
|
@ -78,3 +78,16 @@ export function ActionToLabel(action?: EventActions): string {
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SeverityToLabel(severity: SeverityEnum | null | undefined): string {
|
||||||
|
if (!severity) return t`Unknown severity`;
|
||||||
|
switch (severity) {
|
||||||
|
case SeverityEnum.Alert:
|
||||||
|
return t`Alert`;
|
||||||
|
case SeverityEnum.Notice:
|
||||||
|
return t`Notice`;
|
||||||
|
case SeverityEnum.Warning:
|
||||||
|
return t`Warning`;
|
||||||
|
}
|
||||||
|
return t`Unknown severity`;
|
||||||
|
}
|
||||||
|
|
|
@ -66,6 +66,23 @@ export class IdentificationStage extends BaseStage<
|
||||||
}
|
}
|
||||||
|
|
||||||
firstUpdated(): void {
|
firstUpdated(): void {
|
||||||
|
this.autoRedirect();
|
||||||
|
this.createHelperForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
autoRedirect(): void {
|
||||||
|
if (!this.challenge) return;
|
||||||
|
// we only want to auto-redirect to a source if there's only one source
|
||||||
|
if (this.challenge.sources?.length !== 1) return;
|
||||||
|
// and we also only do an auto-redirect if no user fields are select
|
||||||
|
// meaning that without the auto-redirect the user would only have the option
|
||||||
|
// to manually click on the source button
|
||||||
|
if ((this.challenge.userFields || []).length !== 0) return;
|
||||||
|
const source = this.challenge.sources[0];
|
||||||
|
this.host.challenge = source.challenge;
|
||||||
|
}
|
||||||
|
|
||||||
|
createHelperForm(): void {
|
||||||
this.form = document.createElement("form");
|
this.form = document.createElement("form");
|
||||||
document.documentElement.appendChild(this.form);
|
document.documentElement.appendChild(this.form);
|
||||||
// Only add the additional username input if we're in a shadow dom
|
// Only add the additional username input if we're in a shadow dom
|
||||||
|
@ -206,7 +223,6 @@ export class IdentificationStage extends BaseStage<
|
||||||
class="pf-c-form__group"
|
class="pf-c-form__group"
|
||||||
.errors=${(this.challenge.responseErrors || {})["uid_field"]}
|
.errors=${(this.challenge.responseErrors || {})["uid_field"]}
|
||||||
>
|
>
|
||||||
<!-- @ts-ignore -->
|
|
||||||
<input
|
<input
|
||||||
type=${type}
|
type=${type}
|
||||||
name="uidField"
|
name="uidField"
|
||||||
|
|
|
@ -4,24 +4,24 @@ title: Identification stage
|
||||||
|
|
||||||
This stage provides a ready-to-go form for users to identify themselves.
|
This stage provides a ready-to-go form for users to identify themselves.
|
||||||
|
|
||||||
## Options
|
## User Fields
|
||||||
|
|
||||||
### User Fields
|
Select which fields the user can use to identify themselves. Multiple fields can be selected. If no fields are selected, only sources will be shown.
|
||||||
|
|
||||||
Select which fields the user can use to identify themselves. Multiple fields can be specified and separated with a comma.
|
- Username
|
||||||
Valid choices:
|
- Email
|
||||||
|
- UPN
|
||||||
|
|
||||||
- email
|
UPN will attempt to identify the user based on the `upn` attribute, which can be imported with an [LDAP Source](/integrations/sources/ldap/index)
|
||||||
- username
|
|
||||||
|
|
||||||
### Template
|
:::info
|
||||||
|
Starting with authentik 2023.5, when no user fields are selected and only one source is selected, authentik will automatically redirect the user to that source.
|
||||||
|
:::
|
||||||
|
|
||||||
This specifies which template is rendered. Currently there are two templates:
|
## Password stage
|
||||||
|
|
||||||
The `Login` template shows configured Sources below the login form, as well as linking to the defined Enrollment and Recovery flows.
|
To prompt users for their password on the same step as identifying themselves, a password stage can be selected here. If a password stage is selected in the Identification stage, the password stage should not be bound to the flow.
|
||||||
|
|
||||||
The `Recovery` template shows only the form.
|
## Enrollment/Recovery Flow
|
||||||
|
|
||||||
### Enrollment/Recovery Flow
|
|
||||||
|
|
||||||
These fields specify if and which flows are linked on the form. The enrollment flow is linked as `Need an account? Sign up.`, and the recovery flow is linked as `Forgot username or password?`.
|
These fields specify if and which flows are linked on the form. The enrollment flow is linked as `Need an account? Sign up.`, and the recovery flow is linked as `Forgot username or password?`.
|
||||||
|
|
Reference in New Issue