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:
Jens L 2023-05-11 16:52:30 +02:00 committed by GitHub
parent 7265a56f05
commit 61434c807d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 81 additions and 30 deletions

View File

@ -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 #

View File

@ -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 + [

View File

@ -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)

View File

@ -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";

View File

@ -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> {

View File

@ -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`;
}

View File

@ -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"

View File

@ -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?`.