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:
		
							
								
								
									
										2
									
								
								.github/pull_request_template.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/pull_request_template.md
									
									
									
									
										vendored
									
									
								
							@ -4,7 +4,7 @@
 | 
			
		||||
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?**
 | 
			
		||||
    Resolves #
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,6 @@
 | 
			
		||||
"""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 authentik.core.api.used_by import UsedByMixin
 | 
			
		||||
@ -9,6 +11,16 @@ from authentik.stages.identification.models import IdentificationStage
 | 
			
		||||
class IdentificationStageSerializer(StageSerializer):
 | 
			
		||||
    """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:
 | 
			
		||||
        model = IdentificationStage
 | 
			
		||||
        fields = StageSerializer.Meta.fields + [
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,14 @@
 | 
			
		||||
"""identification tests"""
 | 
			
		||||
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.flows.challenge import ChallengeTypes
 | 
			
		||||
from authentik.flows.models import FlowDesignation, FlowStageBinding
 | 
			
		||||
from authentik.flows.tests import FlowTestCase
 | 
			
		||||
from authentik.lib.generators import generate_id
 | 
			
		||||
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.password import BACKEND_INBUILT
 | 
			
		||||
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 "@goauthentik/elements/forms/HorizontalFormElement";
 | 
			
		||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
import "@goauthentik/admin/events/RuleForm";
 | 
			
		||||
import { SeverityToLabel } from "@goauthentik/admin/events/utils";
 | 
			
		||||
import "@goauthentik/admin/policies/BoundPoliciesList";
 | 
			
		||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
			
		||||
import { uiConfig } from "@goauthentik/common/ui/config";
 | 
			
		||||
@ -14,20 +15,7 @@ import { t } from "@lingui/macro";
 | 
			
		||||
import { TemplateResult, html } from "lit";
 | 
			
		||||
import { customElement, property } from "lit/decorators.js";
 | 
			
		||||
 | 
			
		||||
import { EventsApi, NotificationRule, SeverityEnum } 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`;
 | 
			
		||||
}
 | 
			
		||||
import { EventsApi, NotificationRule } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-event-rule-list")
 | 
			
		||||
export class RuleListPage extends TablePage<NotificationRule> {
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ import { t } from "@lingui/macro";
 | 
			
		||||
 | 
			
		||||
import { TemplateResult, html } from "lit";
 | 
			
		||||
 | 
			
		||||
import { EventActions } from "@goauthentik/api";
 | 
			
		||||
import { EventActions, SeverityEnum } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
export function EventGeo(event: EventWithContext): TemplateResult {
 | 
			
		||||
    let geo: KeyUnknown | undefined = undefined;
 | 
			
		||||
@ -78,3 +78,16 @@ export function ActionToLabel(action?: EventActions): string {
 | 
			
		||||
            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 {
 | 
			
		||||
        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");
 | 
			
		||||
        document.documentElement.appendChild(this.form);
 | 
			
		||||
        // 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"
 | 
			
		||||
                .errors=${(this.challenge.responseErrors || {})["uid_field"]}
 | 
			
		||||
            >
 | 
			
		||||
                <!-- @ts-ignore -->
 | 
			
		||||
                <input
 | 
			
		||||
                    type=${type}
 | 
			
		||||
                    name="uidField"
 | 
			
		||||
 | 
			
		||||
@ -4,24 +4,24 @@ title: Identification stage
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
Valid choices:
 | 
			
		||||
-   Username
 | 
			
		||||
-   Email
 | 
			
		||||
-   UPN
 | 
			
		||||
 | 
			
		||||
-   email
 | 
			
		||||
-   username
 | 
			
		||||
    UPN will attempt to identify the user based on the `upn` attribute, which can be imported with an [LDAP Source](/integrations/sources/ldap/index)
 | 
			
		||||
 | 
			
		||||
### 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?`.
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user