stages/redirect: create redirect stage (#12275)

* create redirect stage

* show "keep context" toggle in Flow mode only

* fix typos

* add docs

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>

* simplify property pass

* simplify toggle

* remove `print` statements

whoops

* fix typo

* remove default from `RedirectStage.mode`

* remove migration

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* oops

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* adjust docs

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Simonyi Gergő
2024-12-12 18:00:09 +01:00
committed by GitHub
parent 587f2d74ac
commit ff504a3b80
35 changed files with 1314 additions and 40 deletions

View File

@ -40,6 +40,7 @@ class Migration(migrations.Migration):
("require_authenticated", "Require Authenticated"),
("require_unauthenticated", "Require Unauthenticated"),
("require_superuser", "Require Superuser"),
("require_redirect", "Require Redirect"),
("require_outpost", "Require Outpost"),
],
default="none",

View File

@ -33,6 +33,7 @@ class FlowAuthenticationRequirement(models.TextChoices):
REQUIRE_AUTHENTICATED = "require_authenticated"
REQUIRE_UNAUTHENTICATED = "require_unauthenticated"
REQUIRE_SUPERUSER = "require_superuser"
REQUIRE_REDIRECT = "require_redirect"
REQUIRE_OUTPOST = "require_outpost"

View File

@ -42,6 +42,8 @@ PLAN_CONTEXT_OUTPOST = "outpost"
# Is set by the Flow Planner when a FlowToken was used, and the currently active flow plan
# was restored.
PLAN_CONTEXT_IS_RESTORED = "is_restored"
PLAN_CONTEXT_IS_REDIRECTED = "is_redirected"
PLAN_CONTEXT_REDIRECT_STAGE_TARGET = "redirect_stage_target"
CACHE_TIMEOUT = CONFIG.get_int("cache.timeout_flows")
CACHE_PREFIX = "goauthentik.io/flows/planner/"
@ -181,7 +183,7 @@ class FlowPlanner:
self.flow = flow
self._logger = get_logger().bind(flow_slug=flow.slug)
def _check_authentication(self, request: HttpRequest):
def _check_authentication(self, request: HttpRequest, context: dict[str, Any]):
"""Check the flow's authentication level is matched by `request`"""
if (
self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_AUTHENTICATED
@ -198,6 +200,11 @@ class FlowPlanner:
and not request.user.is_superuser
):
raise FlowNonApplicableException()
if (
self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_REDIRECT
and context.get(PLAN_CONTEXT_IS_REDIRECTED) is None
):
raise FlowNonApplicableException()
outpost_user = ClientIPMiddleware.get_outpost_user(request)
if self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_OUTPOST:
if not outpost_user:
@ -229,18 +236,13 @@ class FlowPlanner:
)
context = default_context or {}
# Bit of a workaround here, if there is a pending user set in the default context
# we use that user for our cache key
# to make sure they don't get the generic response
# we use that user for our cache key to make sure they don't get the generic response
if context and PLAN_CONTEXT_PENDING_USER in context:
user = context[PLAN_CONTEXT_PENDING_USER]
else:
user = request.user
# We only need to check the flow authentication if it's planned without a user
# in the context, as a user in the context can only be set via the explicit code API
# or if a flow is restarted due to `invalid_response_action` being set to
# `restart_with_context`, which can only happen if the user was already authorized
# to use the flow
context.update(self._check_authentication(request))
context.update(self._check_authentication(request, context))
# First off, check the flow's direct policy bindings
# to make sure the user even has access to the flow
engine = PolicyEngine(self.flow, user, request)

View File

@ -93,7 +93,11 @@ class ChallengeStageView(StageView):
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Return a challenge for the frontend to solve"""
challenge = self._get_challenge(*args, **kwargs)
try:
challenge = self._get_challenge(*args, **kwargs)
except StageInvalidException as exc:
self.logger.debug("Got StageInvalidException", exc=exc)
return self.executor.stage_invalid()
if not challenge.is_valid():
self.logger.warning(
"f(ch): Invalid challenge",
@ -169,11 +173,7 @@ class ChallengeStageView(StageView):
stage_type=self.__class__.__name__, method="get_challenge"
).time(),
):
try:
challenge = self.get_challenge(*args, **kwargs)
except StageInvalidException as exc:
self.logger.debug("Got StageInvalidException", exc=exc)
return self.executor.stage_invalid()
challenge = self.get_challenge(*args, **kwargs)
with start_span(
op="authentik.flow.stage._get_challenge",
name=self.__class__.__name__,

View File

@ -22,7 +22,12 @@ from authentik.flows.models import (
FlowStageBinding,
in_memory_stage,
)
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
from authentik.flows.planner import (
PLAN_CONTEXT_IS_REDIRECTED,
PLAN_CONTEXT_PENDING_USER,
FlowPlanner,
cache_key,
)
from authentik.flows.stage import StageView
from authentik.lib.tests.utils import dummy_get_response
from authentik.outposts.apps import MANAGED_OUTPOST
@ -81,6 +86,24 @@ class TestFlowPlanner(TestCase):
planner.allow_empty_flows = True
planner.plan(request)
def test_authentication_redirect_required(self):
"""Test flow authentication (redirect required)"""
flow = create_test_flow()
flow.authentication = FlowAuthenticationRequirement.REQUIRE_REDIRECT
request = self.request_factory.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
request.user = AnonymousUser()
planner = FlowPlanner(flow)
planner.allow_empty_flows = True
with self.assertRaises(FlowNonApplicableException):
planner.plan(request)
context = {}
context[PLAN_CONTEXT_IS_REDIRECTED] = create_test_flow()
planner.plan(request, context)
@reconcile_app("authentik_outposts")
def test_authentication_outpost(self):
"""Test flow authentication (outpost)"""

View File

@ -171,7 +171,8 @@ class FlowExecutorView(APIView):
# Existing plan is deleted from session and instance
self.plan = None
self.cancel()
self._logger.debug("f(exec): Continuing existing plan")
else:
self._logger.debug("f(exec): Continuing existing plan")
# Initial flow request, check if we have an upstream query string passed in
request.session[SESSION_KEY_GET] = get_params

View File

@ -114,6 +114,7 @@ TENANT_APPS = [
"authentik.stages.invitation",
"authentik.stages.password",
"authentik.stages.prompt",
"authentik.stages.redirect",
"authentik.stages.user_delete",
"authentik.stages.user_login",
"authentik.stages.user_logout",

View File

View File

@ -0,0 +1,42 @@
"""RedirectStage API Views"""
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import ValidationError
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer
from authentik.stages.redirect.models import RedirectMode, RedirectStage
class RedirectStageSerializer(StageSerializer):
"""RedirectStage Serializer"""
def validate(self, attrs):
mode = attrs.get("mode")
target_static = attrs.get("target_static")
target_flow = attrs.get("target_flow")
if mode == RedirectMode.STATIC and not target_static:
raise ValidationError(_("Target URL should be present when mode is Static."))
if mode == RedirectMode.FLOW and not target_flow:
raise ValidationError(_("Target Flow should be present when mode is Flow."))
return attrs
class Meta:
model = RedirectStage
fields = StageSerializer.Meta.fields + [
"keep_context",
"mode",
"target_static",
"target_flow",
]
class RedirectStageViewSet(UsedByMixin, ModelViewSet):
"""RedirectStage Viewset"""
queryset = RedirectStage.objects.all()
serializer_class = RedirectStageSerializer
filterset_fields = ["name"]
search_fields = ["name"]
ordering = ["name"]

View File

@ -0,0 +1,11 @@
"""authentik redirect app"""
from django.apps import AppConfig
class AuthentikStageRedirectConfig(AppConfig):
"""authentik redirect app"""
name = "authentik.stages.redirect"
label = "authentik_stages_redirect"
verbose_name = "authentik Stages.Redirect"

View File

@ -0,0 +1,49 @@
# Generated by Django 5.0.10 on 2024-12-11 14:40
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("authentik_flows", "0027_auto_20231028_1424"),
]
operations = [
migrations.CreateModel(
name="RedirectStage",
fields=[
(
"stage_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_flows.stage",
),
),
("keep_context", models.BooleanField(default=True)),
("mode", models.TextField(choices=[("static", "Static"), ("flow", "Flow")])),
("target_static", models.CharField(blank=True, default="")),
(
"target_flow",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="authentik_flows.flow",
),
),
],
options={
"verbose_name": "Redirect Stage",
"verbose_name_plural": "Redirect Stages",
},
bases=("authentik_flows.stage",),
),
]

View File

@ -0,0 +1,49 @@
"""authentik redirect stage"""
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.views import View
from rest_framework.serializers import BaseSerializer
from authentik.flows.models import Flow, Stage
class RedirectMode(models.TextChoices):
"""Mode a Redirect stage can operate in"""
STATIC = "static"
FLOW = "flow"
class RedirectStage(Stage):
"""Redirect the user to another flow, potentially with all gathered context"""
keep_context = models.BooleanField(default=True)
mode = models.TextField(choices=RedirectMode.choices)
target_static = models.CharField(blank=True, default="")
target_flow = models.ForeignKey(
Flow,
null=True,
blank=True,
on_delete=models.SET_NULL,
)
@property
def serializer(self) -> type[BaseSerializer]:
from authentik.stages.redirect.api import RedirectStageSerializer
return RedirectStageSerializer
@property
def view(self) -> type[View]:
from authentik.stages.redirect.stage import RedirectStageView
return RedirectStageView
@property
def component(self) -> str:
return "ak-stage-redirect-form"
class Meta:
verbose_name = _("Redirect Stage")
verbose_name_plural = _("Redirect Stages")

View File

@ -0,0 +1,110 @@
"""authentik redirect stage"""
from urllib.parse import urlsplit
from django.http.response import HttpResponse
from rest_framework.fields import CharField
from authentik.flows.challenge import (
Challenge,
ChallengeResponse,
RedirectChallenge,
)
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import (
Flow,
)
from authentik.flows.planner import (
PLAN_CONTEXT_IS_REDIRECTED,
PLAN_CONTEXT_REDIRECT_STAGE_TARGET,
FlowPlanner,
)
from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import SESSION_KEY_PLAN, InvalidStageError
from authentik.lib.utils.urls import reverse_with_qs
from authentik.stages.redirect.models import RedirectMode, RedirectStage
URL_SCHEME_FLOW = "ak-flow"
class RedirectChallengeResponse(ChallengeResponse):
"""Redirect challenge response"""
component = CharField(default="xak-flow-redirect")
to = CharField()
class RedirectStageView(ChallengeStageView):
"""Redirect stage to redirect to other Flows with context"""
response_class = RedirectChallengeResponse
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
return self.executor.stage_ok()
def parse_target(self, target: str) -> str | Flow:
parsed_target = urlsplit(target)
if parsed_target.scheme != URL_SCHEME_FLOW:
return target
flow = Flow.objects.filter(slug=parsed_target.netloc).first()
if not flow:
self.logger.warning(
f"Flow set by {PLAN_CONTEXT_REDIRECT_STAGE_TARGET} does not exist",
flow_slug=parsed_target.path,
)
return flow
def switch_flow_with_context(self, flow: Flow, keep_context=True) -> str:
"""Switch to another flow, optionally keeping all context"""
self.logger.info(
"f(exec): Switching to new flow", new_flow=flow.slug, keep_context=keep_context
)
planner = FlowPlanner(flow)
planner.use_cache = False
default_context = self.executor.plan.context if keep_context else {}
try:
default_context[PLAN_CONTEXT_IS_REDIRECTED] = self.executor.flow
plan = planner.plan(self.request, default_context)
except FlowNonApplicableException as exc:
raise InvalidStageError() from exc
self.request.session[SESSION_KEY_PLAN] = plan
kwargs = self.executor.kwargs
kwargs.update({"flow_slug": flow.slug})
return reverse_with_qs("authentik_core:if-flow", self.request.GET, kwargs=kwargs)
def get_challenge(self, *args, **kwargs) -> Challenge:
"""Get the redirect target. Prioritize `redirect_stage_target` if present."""
current_stage: RedirectStage = self.executor.current_stage
target: str | Flow = ""
target_url_override = self.executor.plan.context.get(PLAN_CONTEXT_REDIRECT_STAGE_TARGET, "")
if target_url_override:
target = self.parse_target(target_url_override)
# `target` is falsy if the override was to a Flow but that Flow doesn't exist.
if not target:
if current_stage.mode == RedirectMode.STATIC:
target = current_stage.target_static
if current_stage.mode == RedirectMode.FLOW:
target = current_stage.target_flow
if isinstance(target, str):
redirect_to = target
else:
redirect_to = self.switch_flow_with_context(
target, keep_context=current_stage.keep_context
)
if not redirect_to:
raise InvalidStageError(
"No target found for Redirect stage. The stage's target_flow may have been deleted."
)
return RedirectChallenge(
data={
"component": "xak-flow-redirect",
"to": redirect_to,
}
)

View File

@ -0,0 +1,172 @@
"""Test Redirect stage"""
from django.urls.base import reverse
from rest_framework.exceptions import ValidationError
from authentik.core.tests.utils import create_test_flow
from authentik.flows.models import FlowAuthenticationRequirement, FlowDesignation, FlowStageBinding
from authentik.flows.tests import FlowTestCase
from authentik.lib.generators import generate_id
from authentik.policies.expression.models import ExpressionPolicy
from authentik.policies.models import PolicyBinding
from authentik.stages.dummy.models import DummyStage
from authentik.stages.redirect.api import RedirectStageSerializer
from authentik.stages.redirect.models import RedirectMode, RedirectStage
URL = "https://url.test/"
URL_OVERRIDE = "https://urloverride.test/"
class TestRedirectStage(FlowTestCase):
"""Test Redirect stage API"""
def setUp(self):
super().setUp()
self.target_flow = create_test_flow(FlowDesignation.AUTHENTICATION)
self.dummy_stage = DummyStage.objects.create(name="dummy")
FlowStageBinding.objects.create(target=self.target_flow, stage=self.dummy_stage, order=0)
self.flow = create_test_flow(FlowDesignation.AUTHENTICATION)
self.stage = RedirectStage.objects.create(
name="redirect",
keep_context=True,
mode=RedirectMode.STATIC,
target_static=URL,
target_flow=self.target_flow,
)
self.binding = FlowStageBinding.objects.create(
target=self.flow,
stage=self.stage,
order=0,
)
def test_static(self):
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
)
self.assertStageRedirects(response, URL)
def test_flow(self):
self.stage.mode = RedirectMode.FLOW
self.stage.save()
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
)
self.assertStageRedirects(
response, reverse("authentik_core:if-flow", kwargs={"flow_slug": self.target_flow.slug})
)
def test_override_static(self):
policy = ExpressionPolicy.objects.create(
name=generate_id(),
expression=f"context['flow_plan'].context['redirect_stage_target'] = "
f"'{URL_OVERRIDE}'; return True",
)
PolicyBinding.objects.create(policy=policy, target=self.binding, order=0)
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
)
self.assertStageRedirects(response, URL_OVERRIDE)
def test_override_flow(self):
target_flow_override = create_test_flow(FlowDesignation.AUTHENTICATION)
dummy_stage_override = DummyStage.objects.create(name="dummy_override")
FlowStageBinding.objects.create(
target=target_flow_override, stage=dummy_stage_override, order=0
)
policy = ExpressionPolicy.objects.create(
name=generate_id(),
expression=f"context['flow_plan'].context['redirect_stage_target'] = "
f"'ak-flow://{target_flow_override.slug}'; return True",
)
PolicyBinding.objects.create(policy=policy, target=self.binding, order=0)
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
)
self.assertStageRedirects(
response,
reverse("authentik_core:if-flow", kwargs={"flow_slug": target_flow_override.slug}),
)
def test_override_nonexistant_flow(self):
policy = ExpressionPolicy.objects.create(
name=generate_id(),
expression="context['flow_plan'].context['redirect_stage_target'] = "
"'ak-flow://nonexistent'; return True",
)
PolicyBinding.objects.create(policy=policy, target=self.binding, order=0)
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
)
self.assertStageRedirects(response, URL)
def test_target_flow_requires_redirect(self):
self.target_flow.authentication = FlowAuthenticationRequirement.REQUIRE_REDIRECT
self.target_flow.save()
self.stage.mode = RedirectMode.FLOW
self.stage.save()
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
)
self.assertStageRedirects(
response, reverse("authentik_core:if-flow", kwargs={"flow_slug": self.target_flow.slug})
)
def test_target_flow_non_applicable(self):
self.target_flow.authentication = FlowAuthenticationRequirement.REQUIRE_AUTHENTICATED
self.target_flow.save()
self.stage.mode = RedirectMode.FLOW
self.stage.save()
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
)
self.assertStageResponse(response, component="ak-stage-access-denied")
def test_serializer(self):
with self.assertRaises(ValidationError):
RedirectStageSerializer(
data={
"name": generate_id(20),
"mode": RedirectMode.STATIC,
}
).is_valid(raise_exception=True)
self.assertTrue(
RedirectStageSerializer(
data={
"name": generate_id(20),
"mode": RedirectMode.STATIC,
"target_static": URL,
}
).is_valid(raise_exception=True)
)
with self.assertRaises(ValidationError):
RedirectStageSerializer(
data={
"name": generate_id(20),
"mode": RedirectMode.FLOW,
}
).is_valid(raise_exception=True)
self.assertTrue(
RedirectStageSerializer(
data={
"name": generate_id(20),
"mode": RedirectMode.FLOW,
"target_flow": create_test_flow().flow_uuid,
}
).is_valid(raise_exception=True)
)

View File

@ -0,0 +1,5 @@
"""API URLs"""
from authentik.stages.redirect.api import RedirectStageViewSet
api_urlpatterns = [("stages/redirect", RedirectStageViewSet)]

View File

@ -2801,6 +2801,46 @@
}
}
},
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_stages_redirect.redirectstage"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created",
"must_created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"permissions": {
"$ref": "#/$defs/model_authentik_stages_redirect.redirectstage_permissions"
},
"attrs": {
"$ref": "#/$defs/model_authentik_stages_redirect.redirectstage"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_stages_redirect.redirectstage"
}
}
},
{
"type": "object",
"required": [
@ -4023,6 +4063,7 @@
"require_authenticated",
"require_unauthenticated",
"require_superuser",
"require_redirect",
"require_outpost"
],
"title": "Authentication",
@ -4493,6 +4534,7 @@
"authentik.stages.invitation",
"authentik.stages.password",
"authentik.stages.prompt",
"authentik.stages.redirect",
"authentik.stages.user_delete",
"authentik.stages.user_login",
"authentik.stages.user_logout",
@ -4588,6 +4630,7 @@
"authentik_stages_password.passwordstage",
"authentik_stages_prompt.prompt",
"authentik_stages_prompt.promptstage",
"authentik_stages_redirect.redirectstage",
"authentik_stages_user_delete.userdeletestage",
"authentik_stages_user_login.userloginstage",
"authentik_stages_user_logout.userlogoutstage",
@ -6813,6 +6856,10 @@
"authentik_stages_prompt.delete_promptstage",
"authentik_stages_prompt.view_prompt",
"authentik_stages_prompt.view_promptstage",
"authentik_stages_redirect.add_redirectstage",
"authentik_stages_redirect.change_redirectstage",
"authentik_stages_redirect.delete_redirectstage",
"authentik_stages_redirect.view_redirectstage",
"authentik_stages_source.add_sourcestage",
"authentik_stages_source.change_sourcestage",
"authentik_stages_source.delete_sourcestage",
@ -11485,6 +11532,146 @@
}
}
},
"model_authentik_stages_redirect.redirectstage": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"title": "Name"
},
"flow_set": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"title": "Name"
},
"slug": {
"type": "string",
"maxLength": 50,
"minLength": 1,
"pattern": "^[-a-zA-Z0-9_]+$",
"title": "Slug",
"description": "Visible in the URL."
},
"title": {
"type": "string",
"minLength": 1,
"title": "Title",
"description": "Shown as the Title in Flow pages."
},
"designation": {
"type": "string",
"enum": [
"authentication",
"authorization",
"invalidation",
"enrollment",
"unenrollment",
"recovery",
"stage_configuration"
],
"title": "Designation",
"description": "Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik."
},
"policy_engine_mode": {
"type": "string",
"enum": [
"all",
"any"
],
"title": "Policy engine mode"
},
"compatibility_mode": {
"type": "boolean",
"title": "Compatibility mode",
"description": "Enable compatibility mode, increases compatibility with password managers on mobile devices."
},
"layout": {
"type": "string",
"enum": [
"stacked",
"content_left",
"content_right",
"sidebar_left",
"sidebar_right"
],
"title": "Layout"
},
"denied_action": {
"type": "string",
"enum": [
"message_continue",
"message",
"continue"
],
"title": "Denied action",
"description": "Configure what should happen when a flow denies access to a user."
}
},
"required": [
"name",
"slug",
"title",
"designation"
]
},
"title": "Flow set"
},
"keep_context": {
"type": "boolean",
"title": "Keep context"
},
"mode": {
"type": "string",
"enum": [
"static",
"flow"
],
"title": "Mode"
},
"target_static": {
"type": "string",
"title": "Target static"
},
"target_flow": {
"type": "string",
"format": "uuid",
"title": "Target flow"
}
},
"required": []
},
"model_authentik_stages_redirect.redirectstage_permissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"permission"
],
"properties": {
"permission": {
"type": "string",
"enum": [
"add_redirectstage",
"change_redirectstage",
"delete_redirectstage",
"view_redirectstage"
]
},
"user": {
"type": "integer"
},
"role": {
"type": "string"
}
}
}
},
"model_authentik_stages_user_delete.userdeletestage": {
"type": "object",
"properties": {
@ -12830,6 +13017,10 @@
"authentik_stages_prompt.delete_promptstage",
"authentik_stages_prompt.view_prompt",
"authentik_stages_prompt.view_promptstage",
"authentik_stages_redirect.add_redirectstage",
"authentik_stages_redirect.change_redirectstage",
"authentik_stages_redirect.delete_redirectstage",
"authentik_stages_redirect.view_redirectstage",
"authentik_stages_source.add_sourcestage",
"authentik_stages_source.change_sourcestage",
"authentik_stages_source.delete_sourcestage",

View File

@ -23390,6 +23390,7 @@ paths:
- authentik_stages_password.passwordstage
- authentik_stages_prompt.prompt
- authentik_stages_prompt.promptstage
- authentik_stages_redirect.redirectstage
- authentik_stages_source.sourcestage
- authentik_stages_user_delete.userdeletestage
- authentik_stages_user_login.userloginstage
@ -23629,6 +23630,7 @@ paths:
- authentik_stages_password.passwordstage
- authentik_stages_prompt.prompt
- authentik_stages_prompt.promptstage
- authentik_stages_redirect.redirectstage
- authentik_stages_source.sourcestage
- authentik_stages_user_delete.userdeletestage
- authentik_stages_user_login.userloginstage
@ -35647,6 +35649,275 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/stages/redirect/:
get:
operationId: stages_redirect_list
description: RedirectStage Viewset
parameters:
- in: query
name: name
schema:
type: string
- name: ordering
required: false
in: query
description: Which field to use when ordering the results.
schema:
type: string
- name: page
required: false
in: query
description: A page number within the paginated result set.
schema:
type: integer
- name: page_size
required: false
in: query
description: Number of results to return per page.
schema:
type: integer
- name: search
required: false
in: query
description: A search term.
schema:
type: string
tags:
- stages
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedRedirectStageList'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
post:
operationId: stages_redirect_create
description: RedirectStage Viewset
tags:
- stages
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/RedirectStageRequest'
required: true
security:
- authentik: []
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/RedirectStage'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/stages/redirect/{stage_uuid}/:
get:
operationId: stages_redirect_retrieve
description: RedirectStage Viewset
parameters:
- in: path
name: stage_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Redirect Stage.
required: true
tags:
- stages
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/RedirectStage'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
put:
operationId: stages_redirect_update
description: RedirectStage Viewset
parameters:
- in: path
name: stage_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Redirect Stage.
required: true
tags:
- stages
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/RedirectStageRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/RedirectStage'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
patch:
operationId: stages_redirect_partial_update
description: RedirectStage Viewset
parameters:
- in: path
name: stage_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Redirect Stage.
required: true
tags:
- stages
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedRedirectStageRequest'
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/RedirectStage'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
delete:
operationId: stages_redirect_destroy
description: RedirectStage Viewset
parameters:
- in: path
name: stage_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Redirect Stage.
required: true
tags:
- stages
security:
- authentik: []
responses:
'204':
description: No response body
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/stages/redirect/{stage_uuid}/used_by/:
get:
operationId: stages_redirect_used_by_list
description: Get a list of all objects that use this object
parameters:
- in: path
name: stage_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Redirect Stage.
required: true
tags:
- stages
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/UsedBy'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/stages/source/:
get:
operationId: stages_source_list
@ -37678,6 +37949,7 @@ components:
- authentik.stages.invitation
- authentik.stages.password
- authentik.stages.prompt
- authentik.stages.redirect
- authentik.stages.user_delete
- authentik.stages.user_login
- authentik.stages.user_logout
@ -37990,6 +38262,7 @@ components:
- require_authenticated
- require_unauthenticated
- require_superuser
- require_redirect
- require_outpost
type: string
AuthenticatorAttachmentEnum:
@ -41431,6 +41704,7 @@ components:
- $ref: '#/components/schemas/PasswordChallengeResponseRequest'
- $ref: '#/components/schemas/PlexAuthenticationChallengeResponseRequest'
- $ref: '#/components/schemas/PromptChallengeResponseRequest'
- $ref: '#/components/schemas/RedirectChallengeResponseRequest'
- $ref: '#/components/schemas/UserLoginChallengeResponseRequest'
discriminator:
propertyName: component
@ -41454,6 +41728,7 @@ components:
ak-stage-password: '#/components/schemas/PasswordChallengeResponseRequest'
ak-source-plex: '#/components/schemas/PlexAuthenticationChallengeResponseRequest'
ak-stage-prompt: '#/components/schemas/PromptChallengeResponseRequest'
xak-flow-redirect: '#/components/schemas/RedirectChallengeResponseRequest'
ak-stage-user-login: '#/components/schemas/UserLoginChallengeResponseRequest'
FlowDesignationEnum:
enum:
@ -44447,6 +44722,7 @@ components:
- authentik_stages_password.passwordstage
- authentik_stages_prompt.prompt
- authentik_stages_prompt.promptstage
- authentik_stages_redirect.redirectstage
- authentik_stages_user_delete.userdeletestage
- authentik_stages_user_login.userloginstage
- authentik_stages_user_logout.userlogoutstage
@ -46544,6 +46820,18 @@ components:
required:
- pagination
- results
PaginatedRedirectStageList:
type: object
properties:
pagination:
$ref: '#/components/schemas/Pagination'
results:
type: array
items:
$ref: '#/components/schemas/RedirectStage'
required:
- pagination
- results
PaginatedReputationList:
type: object
properties:
@ -49611,6 +49899,27 @@ components:
should only be enabled if all users that will bind to this provider have
a TOTP device configured, as otherwise a password may incorrectly be rejected
if it contains a semicolon.
PatchedRedirectStageRequest:
type: object
description: RedirectStage Serializer
properties:
name:
type: string
minLength: 1
flow_set:
type: array
items:
$ref: '#/components/schemas/FlowSetRequest'
keep_context:
type: boolean
mode:
$ref: '#/components/schemas/RedirectStageModeEnum'
target_static:
type: string
target_flow:
type: string
format: uuid
nullable: true
PatchedReputationPolicyRequest:
type: object
description: Reputation Policy Serializer
@ -52149,6 +52458,97 @@ components:
type: string
required:
- to
RedirectChallengeResponseRequest:
type: object
description: Redirect challenge response
properties:
component:
type: string
minLength: 1
default: xak-flow-redirect
to:
type: string
minLength: 1
required:
- to
RedirectStage:
type: object
description: RedirectStage Serializer
properties:
pk:
type: string
format: uuid
readOnly: true
title: Stage uuid
name:
type: string
component:
type: string
description: Get object type so that we know how to edit the object
readOnly: true
verbose_name:
type: string
description: Return object's verbose_name
readOnly: true
verbose_name_plural:
type: string
description: Return object's plural verbose_name
readOnly: true
meta_model_name:
type: string
description: Return internal model name
readOnly: true
flow_set:
type: array
items:
$ref: '#/components/schemas/FlowSet'
keep_context:
type: boolean
mode:
$ref: '#/components/schemas/RedirectStageModeEnum'
target_static:
type: string
target_flow:
type: string
format: uuid
nullable: true
required:
- component
- meta_model_name
- mode
- name
- pk
- verbose_name
- verbose_name_plural
RedirectStageModeEnum:
enum:
- static
- flow
type: string
RedirectStageRequest:
type: object
description: RedirectStage Serializer
properties:
name:
type: string
minLength: 1
flow_set:
type: array
items:
$ref: '#/components/schemas/FlowSetRequest'
keep_context:
type: boolean
mode:
$ref: '#/components/schemas/RedirectStageModeEnum'
target_static:
type: string
target_flow:
type: string
format: uuid
nullable: true
required:
- mode
- name
RedirectURI:
type: object
description: A single allowed redirect URI entry

View File

@ -189,21 +189,28 @@ export class FlowForm extends WithCapabilitiesConfig(ModelForm<Flow, string>) {
?selected=${this.instance?.authentication ===
AuthenticationEnum.RequireUnauthenticated}
>
${msg("Require no authentication.")}
${msg("Require no authentication")}
</option>
<option
value=${AuthenticationEnum.RequireSuperuser}
?selected=${this.instance?.authentication ===
AuthenticationEnum.RequireSuperuser}
>
${msg("Require superuser.")}
${msg("Require superuser")}
</option>
<option
value=${AuthenticationEnum.RequireRedirect}
?selected=${this.instance?.authentication ===
AuthenticationEnum.RequireRedirect}
>
${msg("Require being redirected from another flow")}
</option>
<option
value=${AuthenticationEnum.RequireOutpost}
?selected=${this.instance?.authentication ===
AuthenticationEnum.RequireOutpost}
>
${msg("Require Outpost (flow can only be executed from an outpost).")}
${msg("Require Outpost (flow can only be executed from an outpost)")}
</option>
</select>
<p class="pf-c-form__helper-text">

View File

@ -17,6 +17,7 @@ import "@goauthentik/admin/stages/identification/IdentificationStageForm";
import "@goauthentik/admin/stages/invitation/InvitationStageForm";
import "@goauthentik/admin/stages/password/PasswordStageForm";
import "@goauthentik/admin/stages/prompt/PromptStageForm";
import "@goauthentik/admin/stages/redirect/RedirectStageForm";
import "@goauthentik/admin/stages/source/SourceStageForm";
import "@goauthentik/admin/stages/user_delete/UserDeleteStageForm";
import "@goauthentik/admin/stages/user_login/UserLoginStageForm";

View File

@ -15,6 +15,7 @@ import "@goauthentik/admin/stages/identification/IdentificationStageForm";
import "@goauthentik/admin/stages/invitation/InvitationStageForm";
import "@goauthentik/admin/stages/password/PasswordStageForm";
import "@goauthentik/admin/stages/prompt/PromptStageForm";
import "@goauthentik/admin/stages/redirect/RedirectStageForm";
import "@goauthentik/admin/stages/source/SourceStageForm";
import "@goauthentik/admin/stages/user_delete/UserDeleteStageForm";
import "@goauthentik/admin/stages/user_login/UserLoginStageForm";

View File

@ -83,13 +83,13 @@ export class ConsentStageForm extends BaseStageForm<ConsentStage> {
value=${ConsentStageModeEnum.Permanent}
?selected=${this.instance?.mode === ConsentStageModeEnum.Permanent}
>
${msg("Consent given last indefinitely")}
${msg("Consent given lasts indefinitely")}
</option>
<option
value=${ConsentStageModeEnum.Expiring}
?selected=${this.instance?.mode === ConsentStageModeEnum.Expiring}
>
${msg("Consent expires.")}
${msg("Consent expires")}
</option>
</select>
</ak-form-element-horizontal>

View File

@ -0,0 +1,145 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import {
Flow,
FlowsApi,
FlowsInstancesListRequest,
RedirectStage,
RedirectStageModeEnum,
StagesApi,
} from "@goauthentik/api";
@customElement("ak-stage-redirect-form")
export class RedirectStageForm extends BaseStageForm<RedirectStage> {
@property({ type: String })
mode: string = RedirectStageModeEnum.Static;
loadInstance(pk: string): Promise<RedirectStage> {
return new StagesApi(DEFAULT_CONFIG)
.stagesRedirectRetrieve({
stageUuid: pk,
})
.then((stage) => {
this.mode = stage.mode ?? RedirectStageModeEnum.Static;
return stage;
});
}
async send(data: RedirectStage): Promise<RedirectStage> {
if (this.instance) {
return new StagesApi(DEFAULT_CONFIG).stagesRedirectUpdate({
stageUuid: this.instance.pk || "",
redirectStageRequest: data,
});
} else {
return new StagesApi(DEFAULT_CONFIG).stagesRedirectCreate({
redirectStageRequest: data,
});
}
}
renderForm(): TemplateResult {
return html`<span>
${msg("Redirect the user to another flow, potentially with all gathered context")}
</span>
<ak-form-element-horizontal label=${msg("Name")} required name="name">
<input
type="text"
value="${this.instance?.name ?? ""}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Stage-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Mode")} required name="mode">
<select
class="pf-c-form-control"
@change=${(ev: Event) => {
const target = ev.target as HTMLSelectElement;
this.mode = target.selectedOptions[0].value;
}}
>
<option
value=${RedirectStageModeEnum.Static}
?selected=${this.instance?.mode === RedirectStageModeEnum.Static}
>
${msg("Static")}
</option>
<option
value=${RedirectStageModeEnum.Flow}
?selected=${this.instance?.mode === RedirectStageModeEnum.Flow}
>
${msg("Flow")}
</option>
</select>
</ak-form-element-horizontal>
<ak-form-element-horizontal
?hidden=${this.mode !== RedirectStageModeEnum.Static}
label=${msg("Target URL")}
name="targetStatic"
required
>
<input
type="text"
value="${this.instance?.targetStatic ?? ""}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${msg("Redirect the user to a static URL.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
?hidden=${this.mode !== RedirectStageModeEnum.Flow}
label=${msg("Target Flow")}
name="targetFlow"
required
>
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
const args: FlowsInstancesListRequest = {
ordering: "slug",
};
if (query !== undefined) {
args.search = query;
}
const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList(
args,
);
return flows.results;
}}
.renderElement=${(flow: Flow): string => RenderFlowOption(flow)}
.renderDescription=${(flow: Flow): TemplateResult => html`${flow.name}`}
.value=${(flow: Flow | undefined): string | undefined => flow?.pk}
.selected=${(flow: Flow): boolean =>
this.instance?.targetFlow === flow.pk}
blankable
>
</ak-search-select>
<p class="pf-c-form__helper-text">${msg("Redirect the user to a Flow.")}</p>
</ak-form-element-horizontal>
<ak-switch-input
?hidden=${this.mode !== RedirectStageModeEnum.Flow}
name="keepContext"
label=${msg("Keep flow context")}
?checked="${this.instance?.keepContext ?? true}"
>
</ak-switch-input>
</div>
</ak-form-group>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-stage-redirect-form": RedirectStageForm;
}
}

View File

@ -12,6 +12,8 @@ For example, in the Identification Stage (part of the default login flow), you c
Any data can be stored in the flow context, however there are some reserved keys in the context dictionary that are used by authentik stages.
To manage flow context on a more granular level, see [Setting flow context keys](../../../../customize/policies/expression/managing_flow_context_keys.md).
## Context dictionary and reserved keys
This section describes the data (the context) that are used in authentik, and provides a list of keys, what they are used for and when they are set.
@ -68,11 +70,15 @@ When a flow is executed by an Outpost (for example the [LDAP](../../../providers
#### `is_sso` (boolean)
Set to `True` when the flow is executed from an "SSO" context. For example, this is set when a flow is used during the authentication or enrollment via an external source, and if a flow is executed to authorize access to an application.
This key is set to `True` when the flow is executed from an "SSO" context. For example, this is set when a flow is used during the authentication or enrollment via an external source, and if a flow is executed to authorize access to an application.
#### `is_restored` (Token object)
Set when a flow execution is continued from a token. This happens for example when an [Email stage](../../stages/email/index.mdx) is used and the user clicks on the link within the email. The token object contains the key that was used to restore the flow execution.
This key is set when a flow execution is continued from a token. This happens for example when an [Email stage](../../stages/email/index.mdx) is used and the user clicks on the link within the email. The token object contains the key that was used to restore the flow execution.
#### `is_redirected` (Flow object)
This key is set when the current flow was reached through a [Redirect stage](../../stages/redirect/index.md) in Flow mode.
### Stage-specific keys
@ -189,3 +195,11 @@ Optionally override the email address that the email will be sent to. If not set
##### `pending_user_identifier` (string)
If _Show matched user_ is disabled, this key will be set to the user identifier entered by the user in the identification stage.
#### Redirect stage
##### `redirect_stage_target` (string)
[Set this key](../../../../customize/policies/expression/managing_flow_context_keys.md) in an Expression Policy to override [Redirect stage](../../stages/redirect/index.md) to force it to redirect to a certain URL or flow. This is useful when a flow requires that the redirection target be decided dynamically.
Use the format `ak-flow://{slug}` to use the Redirect stage in Flow mode. Any other format will result in the Redirect stage running in Static mode.

View File

@ -44,7 +44,7 @@ To create a flow, follow these steps:
2. In the Admin interface, navigate to **Flows and Stages -> Flows**.
3. Click **Create**, define the flow using the [configuration settings](#flow-configuration-options) described below, and then click **Finish**.
After creating the flow, you can then [bind specific stages](../stages/index.md#bind-a-stage-to-a-flow) to the flow and [bind policies](../../../customize/policies/working_with_policies/working_with_policies.md) to the flow to further customize the user's log in and authentication process.
After creating the flow, you can then [bind specific stages](../stages/index.md#bind-a-stage-to-a-flow) to the flow and [bind policies](../../../customize/policies/working_with_policies.md) to the flow to further customize the user's log in and authentication process.
To determine which flow should be used, authentik will first check which default authentication flow is configured in the active [**Brand**](../../../customize/brands.md). If no default is configured there, the policies in all flows with the matching designation are checked, and the first flow with matching policies sorted by `slug` will be used.
@ -66,7 +66,7 @@ import Defaultflowlist from "../flow/flow_list/\_defaultflowlist.mdx";
<Defaultflowlist />
**Authentication**: Using this option, you can configure whether the the flow requires initial authentication or not, whether the user must be a superuser, or if the flow requires an outpost.
**Authentication**: Using this option, you can configure whether the the flow requires initial authentication or not, whether the user must be a superuser, if the flow can only be started after being redirected by a [Redirect stage](../stages/redirect/index.md), or if the flow requires an outpost.
**Behavior settings**:

View File

@ -43,7 +43,7 @@ To create a stage, follow these steps:
2. In the Admin interface, navigate to **Flows and Stages -> Stages**.
3. Click **Create**, define the flow using the configuration settings, and then click **Finish**.
After creating the stage, you can then [bind the stage to a flow](#bind-a-stage-to-a-flow) or [bind a policy to the stage](../../../customize/policies/working_with_policies/working_with_policies.md) (the policy determines whether or not the stage will be implemented in the flow).
After creating the stage, you can then [bind the stage to a flow](#bind-a-stage-to-a-flow) or [bind a policy to the stage](../../../customize/policies/working_with_policies.md) (the policy determines whether or not the stage will be implemented in the flow).
## Bind a stage to a flow

View File

@ -0,0 +1,17 @@
---
title: Redirect stage
---
This stage's main purpose is to redirect the user to a new Flow while keeping flow context. For convenience, it can also redirect the user to a static URL.
## Redirect stage modes
### Static mode
When the user reaches this stage, they are redirected to a static URL.
### Flow mode
When the user reaches this stage, they are redirected to a specified flow, retaining all [flow context](../../flow/context).
Optionally, untoggle the "Keep flow context" switch. If this is untoggled, all flow context is cleared with the exception of the [is_redirected](../../flow/context#is_redirected-flow-object) key.

View File

@ -0,0 +1,17 @@
---
title: Managing flow context keys
---
[Flow context](../../../add-secure-apps/flows-stages/flow/context/index.md) can be managed in [Expression policies](../expression.mdx) via the `context['flow_plan'].context` variable.
Here's an example of setting a key in an Expression policy:
```python
context['flow_plan'].context['redirect_stage_target'] = 'ak-flow://redirected-authentication-flow'
```
And here's an example of removing that key:
```python
context['flow_plan'].context.pop('redirect_stage_target', None)
```

View File

@ -8,7 +8,7 @@ In effect, policies determine whether or not a specific stage is applied to a fl
For example, you can create a policy that, for certain users, skips over a stage that prompts for MFA input. Or, you can define a policy that allows users to access a login flow only if the policy criteria are met. See below for other policies, including the reputation policy and an events-driven policy to manage notifications.
For instructions about creating and binding policies to flows and stages, refer to ["Working with policies](./working_with_policies/working_with_policies.md)".
For instructions about creating and binding policies to flows and stages, refer to ["Working with policies](./working_with_policies.md)".
## Standard policies

View File

@ -2,11 +2,11 @@
title: Working with policies
---
For an overview of policies, refer to our documentation on [Policies](../index.md).
For an overview of policies, refer to our documentation on [Policies](./index.md).
authentik provides several [standard policy types](../index.md#standard-policies), which can be configured for your specific needs.
authentik provides several [standard policy types](./index.md#standard-policies), which can be configured for your specific needs.
We also document how to use a policy to [whitelist email domains](./whitelist_email.md) and to [ensure unique email addresses](./unique_email.md).
We also document how to use a policy to [whitelist email domains](./expression/whitelist_email.md) and to [ensure unique email addresses](./expression/unique_email.md).
## Create a policy
@ -19,7 +19,7 @@ To create a new policy, follow these steps:
## Bind a policy to a flow or stage
After creating the policy, you can bind it to either a [flow](../../../add-secure-apps/flows-stages/flow/index.md) or to a [stage](../../../add-secure-apps/flows-stages/stages/index.md).
After creating the policy, you can bind it to either a [flow](../../add-secure-apps/flows-stages/flow/index.md) or to a [stage](../../add-secure-apps/flows-stages/stages/index.md).
:::info
Bindings are instantiated objects themselves, and conceptually can be considered as the "connector" between the policy and the stage or flow. This is why you might read about "binding a binding", because technically, a binding is "spliced" into another binding, in order to intercept and enforce the criteria defined in the policy. You can edit bindings on a flow's **Stage Bindings** tab.

View File

@ -4,7 +4,7 @@ title: Manage users
The following topics are for the basic management of users: how to create, modify, delete or deactivate users, and using a recovery email.
[Policies](../../customize/policies/index.md) can be used to further manage how users are authenticated. For example, by default authentik does not require email addresses be unique, but you can use a policy to [enforce unique email addresses](../../customize/policies/working_with_policies/unique_email.md).
[Policies](../../customize/policies/index.md) can be used to further manage how users are authenticated. For example, by default authentik does not require email addresses be unique, but you can use a policy to [enforce unique email addresses](../../customize/policies/expression/unique_email.md).
### Create a user

View File

@ -463,13 +463,25 @@
[[redirects]]
from = "/docs/policies/working_with_policies/unique_email"
to = "/docs/customize/policies/working_with_policies/unique_email"
to = "/docs/customize/policies/expression/unique_email"
status = 302
force = true
[[redirects]]
from = "/docs/customize/policies/working_with_policies/unique_email"
to = "/docs/customize/policies/expression/unique_email"
status = 302
force = true
[[redirects]]
from = "/docs/policies/working_with_policies/whitelist_email"
to = "/docs/customize/policies/working_with_policies/whitelist_email"
to = "/docs/customize/policies/expression/whitelist_email"
status = 302
force = true
[[redirects]]
from = "/docs/customize/policies/working_with_policies/whitelist_email"
to = "/docs/customize/policies/expression/whitelist_email"
status = 302
force = true

View File

@ -297,6 +297,7 @@ export default {
"add-secure-apps/flows-stages/stages/invitation/index",
"add-secure-apps/flows-stages/stages/password/index",
"add-secure-apps/flows-stages/stages/prompt/index",
"add-secure-apps/flows-stages/stages/redirect/index",
"add-secure-apps/flows-stages/stages/source/index",
"add-secure-apps/flows-stages/stages/user_delete",
"add-secure-apps/flows-stages/stages/user_login/index",
@ -352,19 +353,20 @@ export default {
id: "customize/policies/index",
},
items: [
"customize/policies/working_with_policies",
{
type: "category",
label: "Working with Policies",
label: "Expression Policies",
link: {
type: "doc",
id: "customize/policies/working_with_policies/working_with_policies",
id: "customize/policies/expression",
},
items: [
"customize/policies/working_with_policies/unique_email",
"customize/policies/working_with_policies/whitelist_email",
"customize/policies/expression/unique_email",
"customize/policies/expression/whitelist_email",
"customize/policies/expression/managing_flow_context_keys",
],
},
"customize/policies/expression",
],
},
{