stages: source stage (#8330)
* stages: source stage Signed-off-by: Jens Langhammer <jens@goauthentik.io> * include stage name in dummy stage Signed-off-by: Jens Langhammer <jens@goauthentik.io> * use data instead of instance for login button Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make mostly work Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix ident stage Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make it work Signed-off-by: Jens Langhammer <jens@goauthentik.io> * pass more data Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix flow inspector not always loading Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix dark theme for stepper Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix inspector styling Signed-off-by: Jens Langhammer <jens@goauthentik.io> * don't skip source stage unless returning Signed-off-by: Jens Langhammer <jens@goauthentik.io> * auto open flow inspector when debug Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix lint Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fixup Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix lint Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix validation Signed-off-by: Jens Langhammer <jens@goauthentik.io> * include raw saml response in flow context Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add some tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * move Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add docs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * Apply suggestions from code review Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Signed-off-by: Jens L. <jens@beryju.org> * fix import Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io> Signed-off-by: Jens L. <jens@beryju.org> Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
This commit is contained in:
@ -617,6 +617,9 @@ class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
|
||||
"""Get serializer for this model"""
|
||||
raise NotImplementedError
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"User-source connection (user={self.user.username}, source={self.source.slug})"
|
||||
|
||||
class Meta:
|
||||
unique_together = (("user", "source"),)
|
||||
|
||||
|
@ -16,8 +16,9 @@ from authentik.core.models import Source, SourceUserMatchingModes, User, UserSou
|
||||
from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION, PostUserEnrollmentStage
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.exceptions import FlowNonApplicableException
|
||||
from authentik.flows.models import Flow, Stage, in_memory_stage
|
||||
from authentik.flows.models import Flow, FlowToken, Stage, in_memory_stage
|
||||
from authentik.flows.planner import (
|
||||
PLAN_CONTEXT_IS_RESTORED,
|
||||
PLAN_CONTEXT_PENDING_USER,
|
||||
PLAN_CONTEXT_REDIRECT,
|
||||
PLAN_CONTEXT_SOURCE,
|
||||
@ -35,6 +36,8 @@ from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
from authentik.stages.user_write.stage import PLAN_CONTEXT_USER_PATH
|
||||
|
||||
SESSION_KEY_OVERRIDE_FLOW_TOKEN = "authentik/flows/source_override_flow_token" # nosec
|
||||
|
||||
|
||||
class Action(Enum):
|
||||
"""Actions that can be decided based on the request
|
||||
@ -222,22 +225,43 @@ class SourceFlowManager:
|
||||
**kwargs,
|
||||
) -> HttpResponse:
|
||||
"""Prepare Authentication Plan, redirect user FlowExecutor"""
|
||||
# Ensure redirect is carried through when user was trying to
|
||||
# authorize application
|
||||
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
||||
NEXT_ARG_NAME, "authentik_core:if-user"
|
||||
)
|
||||
kwargs.update(
|
||||
{
|
||||
# Since we authenticate the user by their token, they have no backend set
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_INBUILT,
|
||||
PLAN_CONTEXT_SSO: True,
|
||||
PLAN_CONTEXT_SOURCE: self.source,
|
||||
PLAN_CONTEXT_REDIRECT: final_redirect,
|
||||
PLAN_CONTEXT_SOURCES_CONNECTION: connection,
|
||||
}
|
||||
)
|
||||
kwargs.update(self.policy_context)
|
||||
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
|
||||
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
|
||||
self._logger.info("Replacing source flow with overridden flow", flow=token.flow.slug)
|
||||
plan = token.plan
|
||||
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
|
||||
plan.context.update(kwargs)
|
||||
for stage in self.get_stages_to_append(flow):
|
||||
plan.append_stage(stage)
|
||||
if stages:
|
||||
for stage in stages:
|
||||
plan.append_stage(stage)
|
||||
self.request.session[SESSION_KEY_PLAN] = plan
|
||||
flow_slug = token.flow.slug
|
||||
token.delete()
|
||||
return redirect_with_qs(
|
||||
"authentik_core:if-flow",
|
||||
self.request.GET,
|
||||
flow_slug=flow_slug,
|
||||
)
|
||||
# Ensure redirect is carried through when user was trying to
|
||||
# authorize application
|
||||
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
||||
NEXT_ARG_NAME, "authentik_core:if-user"
|
||||
)
|
||||
if PLAN_CONTEXT_REDIRECT not in kwargs:
|
||||
kwargs[PLAN_CONTEXT_REDIRECT] = final_redirect
|
||||
|
||||
if not flow:
|
||||
return bad_request_message(
|
||||
self.request,
|
||||
|
@ -15,6 +15,7 @@ CELERY_BEAT_SCHEDULE = {
|
||||
TENANT_APPS = [
|
||||
"authentik.enterprise.audit",
|
||||
"authentik.enterprise.providers.rac",
|
||||
"authentik.enterprise.stages.source",
|
||||
]
|
||||
|
||||
MIDDLEWARE = ["authentik.enterprise.middleware.EnterpriseMiddleware"]
|
||||
|
0
authentik/enterprise/stages/__init__.py
Normal file
0
authentik/enterprise/stages/__init__.py
Normal file
0
authentik/enterprise/stages/source/__init__.py
Normal file
0
authentik/enterprise/stages/source/__init__.py
Normal file
38
authentik/enterprise/stages/source/api.py
Normal file
38
authentik/enterprise/stages/source/api.py
Normal file
@ -0,0 +1,38 @@
|
||||
"""Source Stage API Views"""
|
||||
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.models import Source
|
||||
from authentik.enterprise.api import EnterpriseRequiredMixin
|
||||
from authentik.enterprise.stages.source.models import SourceStage
|
||||
from authentik.flows.api.stages import StageSerializer
|
||||
|
||||
|
||||
class SourceStageSerializer(EnterpriseRequiredMixin, StageSerializer):
|
||||
"""SourceStage Serializer"""
|
||||
|
||||
def validate_source(self, _source: Source) -> Source:
|
||||
"""Ensure configured source supports web-based login"""
|
||||
source = Source.objects.filter(pk=_source.pk).select_subclasses().first()
|
||||
if not source:
|
||||
raise ValidationError("Invalid source")
|
||||
login_button = source.ui_login_button(self.context["request"])
|
||||
if not login_button:
|
||||
raise ValidationError("Invalid source selected, only web-based sources are supported.")
|
||||
return source
|
||||
|
||||
class Meta:
|
||||
model = SourceStage
|
||||
fields = StageSerializer.Meta.fields + ["source", "resume_timeout"]
|
||||
|
||||
|
||||
class SourceStageViewSet(UsedByMixin, ModelViewSet):
|
||||
"""SourceStage Viewset"""
|
||||
|
||||
queryset = SourceStage.objects.all()
|
||||
serializer_class = SourceStageSerializer
|
||||
filterset_fields = "__all__"
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
12
authentik/enterprise/stages/source/apps.py
Normal file
12
authentik/enterprise/stages/source/apps.py
Normal file
@ -0,0 +1,12 @@
|
||||
"""authentik stage app config"""
|
||||
|
||||
from authentik.enterprise.apps import EnterpriseConfig
|
||||
|
||||
|
||||
class AuthentikEnterpriseStageSourceConfig(EnterpriseConfig):
|
||||
"""authentik source stage config"""
|
||||
|
||||
name = "authentik.enterprise.stages.source"
|
||||
label = "authentik_stages_source"
|
||||
verbose_name = "authentik Enterprise.Stages.Source"
|
||||
default = True
|
@ -0,0 +1,53 @@
|
||||
# Generated by Django 5.0.2 on 2024-02-25 20:44
|
||||
|
||||
import authentik.lib.utils.time
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0033_alter_user_options"),
|
||||
("authentik_flows", "0027_auto_20231028_1424"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="SourceStage",
|
||||
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",
|
||||
),
|
||||
),
|
||||
(
|
||||
"resume_timeout",
|
||||
models.TextField(
|
||||
default="minutes=10",
|
||||
help_text="Amount of time a user can take to return from the source to continue the flow (Format: hours=-1;minutes=-2;seconds=-3)",
|
||||
validators=[authentik.lib.utils.time.timedelta_string_validator],
|
||||
),
|
||||
),
|
||||
(
|
||||
"source",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="authentik_core.source"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Source Stage",
|
||||
"verbose_name_plural": "Source Stages",
|
||||
},
|
||||
bases=("authentik_flows.stage",),
|
||||
),
|
||||
]
|
45
authentik/enterprise/stages/source/models.py
Normal file
45
authentik/enterprise/stages/source/models.py
Normal file
@ -0,0 +1,45 @@
|
||||
"""Source stage models"""
|
||||
|
||||
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 Stage
|
||||
from authentik.lib.utils.time import timedelta_string_validator
|
||||
|
||||
|
||||
class SourceStage(Stage):
|
||||
"""Suspend the current flow execution and send the user to a source,
|
||||
after which this flow execution is resumed."""
|
||||
|
||||
source = models.ForeignKey("authentik_core.Source", on_delete=models.CASCADE)
|
||||
|
||||
resume_timeout = models.TextField(
|
||||
default="minutes=10",
|
||||
validators=[timedelta_string_validator],
|
||||
help_text=_(
|
||||
"Amount of time a user can take to return from the source to continue the flow "
|
||||
"(Format: hours=-1;minutes=-2;seconds=-3)"
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[BaseSerializer]:
|
||||
from authentik.enterprise.stages.source.api import SourceStageSerializer
|
||||
|
||||
return SourceStageSerializer
|
||||
|
||||
@property
|
||||
def view(self) -> type[View]:
|
||||
from authentik.enterprise.stages.source.stage import SourceStageView
|
||||
|
||||
return SourceStageView
|
||||
|
||||
@property
|
||||
def component(self) -> str:
|
||||
return "ak-stage-source-form"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Source Stage")
|
||||
verbose_name_plural = _("Source Stages")
|
79
authentik/enterprise/stages/source/stage.py
Normal file
79
authentik/enterprise/stages/source/stage.py
Normal file
@ -0,0 +1,79 @@
|
||||
"""Source stage logic"""
|
||||
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.text import slugify
|
||||
from django.utils.timezone import now
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
from authentik.core.models import Source, User
|
||||
from authentik.core.sources.flow_manager import SESSION_KEY_OVERRIDE_FLOW_TOKEN
|
||||
from authentik.core.types import UILoginButton
|
||||
from authentik.enterprise.stages.source.models import SourceStage
|
||||
from authentik.flows.challenge import Challenge, ChallengeResponse
|
||||
from authentik.flows.models import FlowToken
|
||||
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
|
||||
PLAN_CONTEXT_RESUME_TOKEN = "resume_token" # nosec
|
||||
|
||||
|
||||
class SourceStageView(ChallengeStageView):
|
||||
"""Suspend the current flow execution and send the user to a source,
|
||||
after which this flow execution is resumed."""
|
||||
|
||||
login_button: UILoginButton
|
||||
|
||||
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
current_stage: SourceStage = self.executor.current_stage
|
||||
source: Source = (
|
||||
Source.objects.filter(pk=current_stage.source_id).select_subclasses().first()
|
||||
)
|
||||
if not source:
|
||||
self.logger.warning("Source does not exist")
|
||||
return self.executor.stage_invalid("Source does not exist")
|
||||
self.login_button = source.ui_login_button(self.request)
|
||||
if not self.login_button:
|
||||
self.logger.warning("Source does not have a UI login button")
|
||||
return self.executor.stage_invalid("Invalid source")
|
||||
restore_token = self.executor.plan.context.get(PLAN_CONTEXT_IS_RESTORED)
|
||||
override_token = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
|
||||
if restore_token and override_token and restore_token.pk == override_token.pk:
|
||||
del self.request.session[SESSION_KEY_OVERRIDE_FLOW_TOKEN]
|
||||
return self.executor.stage_ok()
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_challenge(self, *args, **kwargs) -> Challenge:
|
||||
resume_token = self.create_flow_token()
|
||||
self.request.session[SESSION_KEY_OVERRIDE_FLOW_TOKEN] = resume_token
|
||||
return self.login_button.challenge
|
||||
|
||||
def create_flow_token(self) -> FlowToken:
|
||||
"""Save the current flow state in a token that can be used to resume this flow"""
|
||||
pending_user: User = self.get_pending_user()
|
||||
if pending_user.is_anonymous:
|
||||
pending_user = get_anonymous_user()
|
||||
current_stage: SourceStage = self.executor.current_stage
|
||||
identifier = slugify(f"ak-source-stage-{current_stage.name}-{str(uuid4())}")
|
||||
# Don't check for validity here, we only care if the token exists
|
||||
tokens = FlowToken.objects.filter(identifier=identifier)
|
||||
valid_delta = timedelta_from_string(current_stage.resume_timeout)
|
||||
if not tokens.exists():
|
||||
return FlowToken.objects.create(
|
||||
expires=now() + valid_delta,
|
||||
user=pending_user,
|
||||
identifier=identifier,
|
||||
flow=self.executor.flow,
|
||||
_plan=FlowToken.pickle(self.executor.plan),
|
||||
)
|
||||
token = tokens.first()
|
||||
# Check if token is expired and rotate key if so
|
||||
if token.is_expired:
|
||||
token.expire_action()
|
||||
return token
|
||||
|
||||
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
|
||||
return self.executor.stage_ok()
|
99
authentik/enterprise/stages/source/tests.py
Normal file
99
authentik/enterprise/stages/source/tests.py
Normal file
@ -0,0 +1,99 @@
|
||||
"""Source stage tests"""
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.core.tests.utils import create_test_flow, create_test_user
|
||||
from authentik.enterprise.stages.source.models import SourceStage
|
||||
from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken
|
||||
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, FlowPlan
|
||||
from authentik.flows.tests import FlowTestCase
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.sources.saml.models import SAMLSource
|
||||
from authentik.stages.identification.models import IdentificationStage, UserFields
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
from authentik.stages.password.models import PasswordStage
|
||||
from authentik.stages.user_login.models import UserLoginStage
|
||||
|
||||
|
||||
class TestSourceStage(FlowTestCase):
|
||||
"""Source stage tests"""
|
||||
|
||||
def setUp(self):
|
||||
self.source = SAMLSource.objects.create(
|
||||
slug=generate_id(),
|
||||
issuer="authentik",
|
||||
allow_idp_initiated=True,
|
||||
pre_authentication_flow=create_test_flow(),
|
||||
)
|
||||
|
||||
def test_source_success(self):
|
||||
"""Test"""
|
||||
user = create_test_user()
|
||||
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
|
||||
stage = SourceStage.objects.create(name=generate_id(), source=self.source)
|
||||
FlowStageBinding.objects.create(
|
||||
target=flow,
|
||||
stage=IdentificationStage.objects.create(
|
||||
name=generate_id(),
|
||||
user_fields=[UserFields.USERNAME],
|
||||
),
|
||||
order=0,
|
||||
)
|
||||
FlowStageBinding.objects.create(
|
||||
target=flow,
|
||||
stage=PasswordStage.objects.create(name=generate_id(), backends=[BACKEND_INBUILT]),
|
||||
order=5,
|
||||
)
|
||||
FlowStageBinding.objects.create(target=flow, stage=stage, order=10)
|
||||
FlowStageBinding.objects.create(
|
||||
target=flow,
|
||||
stage=UserLoginStage.objects.create(
|
||||
name=generate_id(),
|
||||
),
|
||||
order=15,
|
||||
)
|
||||
|
||||
# Get user identification stage
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertStageResponse(response, flow, component="ak-stage-identification")
|
||||
# Send username
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
data={"uid_field": user.username},
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertStageResponse(response, flow, component="ak-stage-password")
|
||||
# Send password
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
data={"password": user.username},
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertStageRedirects(
|
||||
response,
|
||||
reverse("authentik_sources_saml:login", kwargs={"source_slug": self.source.slug}),
|
||||
)
|
||||
|
||||
# Hijack flow plan so we don't have to emulate the source
|
||||
flow_token = FlowToken.objects.filter(
|
||||
identifier__startswith=f"ak-source-stage-{stage.name.lower()}"
|
||||
).first()
|
||||
self.assertIsNotNone(flow_token)
|
||||
session = self.client.session
|
||||
plan: FlowPlan = session[SESSION_KEY_PLAN]
|
||||
plan.context[PLAN_CONTEXT_IS_RESTORED] = flow_token
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
||||
# Pretend we've just returned from the source
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), follow=True
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
|
5
authentik/enterprise/stages/source/urls.py
Normal file
5
authentik/enterprise/stages/source/urls.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""API URLs"""
|
||||
|
||||
from authentik.enterprise.stages.source.api import SourceStageViewSet
|
||||
|
||||
api_urlpatterns = [("stages/source", SourceStageViewSet)]
|
@ -13,6 +13,7 @@ from structlog.stdlib import get_logger
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
||||
from authentik.core.types import UserSettingSerializer
|
||||
from authentik.enterprise.apps import EnterpriseConfig
|
||||
from authentik.flows.api.flows import FlowSetSerializer
|
||||
from authentik.flows.models import ConfigurableStage, Stage
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
@ -75,6 +76,7 @@ class StageViewSet(
|
||||
"description": subclass.__doc__,
|
||||
"component": subclass().component,
|
||||
"model_name": subclass._meta.model_name,
|
||||
"requires_enterprise": isinstance(subclass._meta.app_config, EnterpriseConfig),
|
||||
}
|
||||
)
|
||||
data = sorted(data, key=lambda x: x["name"])
|
||||
|
@ -469,7 +469,7 @@ class FlowExecutorView(APIView):
|
||||
|
||||
|
||||
class CancelView(View):
|
||||
"""View which canels the currently active plan"""
|
||||
"""View which cancels the currently active plan"""
|
||||
|
||||
def get(self, request: HttpRequest) -> HttpResponse:
|
||||
"""View which canels the currently active plan"""
|
||||
|
@ -47,7 +47,7 @@ class SourceType:
|
||||
def login_challenge(self, source: OAuthSource, request: HttpRequest) -> Challenge:
|
||||
"""Allow types to return custom challenges"""
|
||||
return RedirectChallenge(
|
||||
instance={
|
||||
data={
|
||||
"type": ChallengeTypes.REDIRECT.value,
|
||||
"to": reverse(
|
||||
"authentik_sources_oauth:oauth-client-login",
|
||||
|
@ -54,7 +54,7 @@ class OAuthCallback(OAuthClientMixin, View):
|
||||
raw_profile=exc.doc,
|
||||
).from_http(self.request)
|
||||
return self.handle_login_failure("Could not retrieve profile.")
|
||||
identifier = self.get_user_id(raw_info)
|
||||
identifier = self.get_user_id(info=raw_info)
|
||||
if identifier is None:
|
||||
return self.handle_login_failure("Could not determine id.")
|
||||
# Get or create access record
|
||||
@ -67,6 +67,7 @@ class OAuthCallback(OAuthClientMixin, View):
|
||||
)
|
||||
sfm.policy_context = {"oauth_userinfo": raw_info}
|
||||
return sfm.get_flow(
|
||||
raw_info=raw_info,
|
||||
access_token=self.token.get("access_token"),
|
||||
)
|
||||
|
||||
@ -116,6 +117,7 @@ class OAuthSourceFlowManager(SourceFlowManager):
|
||||
self,
|
||||
connection: UserOAuthSourceConnection,
|
||||
access_token: str | None = None,
|
||||
**_,
|
||||
) -> UserOAuthSourceConnection:
|
||||
"""Set the access_token on the connection"""
|
||||
connection.access_token = access_token
|
||||
|
@ -190,7 +190,7 @@ class SAMLSource(Source):
|
||||
def ui_login_button(self, request: HttpRequest) -> UILoginButton:
|
||||
return UILoginButton(
|
||||
challenge=RedirectChallenge(
|
||||
instance={
|
||||
data={
|
||||
"type": ChallengeTypes.REDIRECT.value,
|
||||
"to": reverse(
|
||||
"authentik_sources_saml:login",
|
||||
|
@ -234,12 +234,14 @@ class ResponseProcessor:
|
||||
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_TRANSIENT:
|
||||
return self._handle_name_id_transient()
|
||||
|
||||
return SAMLSourceFlowManager(
|
||||
flow_manager = SAMLSourceFlowManager(
|
||||
self._source,
|
||||
self._http_request,
|
||||
name_id.text,
|
||||
delete_none_values(self.get_attributes()),
|
||||
)
|
||||
flow_manager.policy_context["saml_response"] = self._root
|
||||
return flow_manager
|
||||
|
||||
|
||||
class SAMLSourceFlowManager(SourceFlowManager):
|
||||
|
@ -12,6 +12,7 @@ class DummyChallenge(Challenge):
|
||||
"""Dummy challenge"""
|
||||
|
||||
component = CharField(default="ak-stage-dummy")
|
||||
name = CharField()
|
||||
|
||||
|
||||
class DummyChallengeResponse(ChallengeResponse):
|
||||
@ -35,5 +36,6 @@ class DummyStageView(ChallengeStageView):
|
||||
data={
|
||||
"type": ChallengeTypes.NATIVE.value,
|
||||
"title": self.executor.current_stage.name,
|
||||
"name": self.executor.current_stage.name,
|
||||
}
|
||||
)
|
||||
|
@ -237,7 +237,9 @@ class IdentificationStageView(ChallengeStageView):
|
||||
ui_login_button = source.ui_login_button(self.request)
|
||||
if ui_login_button:
|
||||
button = asdict(ui_login_button)
|
||||
button["challenge"] = ui_login_button.challenge.data
|
||||
source_challenge = ui_login_button.challenge
|
||||
source_challenge.is_valid()
|
||||
button["challenge"] = source_challenge.data
|
||||
ui_sources.append(button)
|
||||
challenge.initial_data["sources"] = ui_sources
|
||||
return challenge
|
||||
|
@ -2594,6 +2594,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"model",
|
||||
"identifiers"
|
||||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
"const": "authentik_stages_source.sourcestage"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"absent",
|
||||
"present",
|
||||
"created",
|
||||
"must_created"
|
||||
],
|
||||
"default": "present"
|
||||
},
|
||||
"conditions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"attrs": {
|
||||
"$ref": "#/$defs/model_authentik_stages_source.sourcestage"
|
||||
},
|
||||
"identifiers": {
|
||||
"$ref": "#/$defs/model_authentik_stages_source.sourcestage"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@ -3257,6 +3294,7 @@
|
||||
"authentik.enterprise",
|
||||
"authentik.enterprise.audit",
|
||||
"authentik.enterprise.providers.rac",
|
||||
"authentik.enterprise.stages.source",
|
||||
"authentik.events"
|
||||
],
|
||||
"title": "App",
|
||||
@ -3338,6 +3376,7 @@
|
||||
"authentik_providers_rac.racprovider",
|
||||
"authentik_providers_rac.endpoint",
|
||||
"authentik_providers_rac.racpropertymapping",
|
||||
"authentik_stages_source.sourcestage",
|
||||
"authentik_events.event",
|
||||
"authentik_events.notificationtransport",
|
||||
"authentik_events.notification",
|
||||
@ -8018,6 +8057,109 @@
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"model_authentik_stages_source.sourcestage": {
|
||||
"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"
|
||||
},
|
||||
"source": {
|
||||
"type": "integer",
|
||||
"title": "Source"
|
||||
},
|
||||
"resume_timeout": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Resume timeout",
|
||||
"description": "Amount of time a user can take to return from the source to continue the flow (Format: hours=-1;minutes=-2;seconds=-3)"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"model_authentik_events.event": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
399
schema.yml
399
schema.yml
@ -18512,6 +18512,7 @@ paths:
|
||||
- authentik_stages_password.passwordstage
|
||||
- authentik_stages_prompt.prompt
|
||||
- authentik_stages_prompt.promptstage
|
||||
- authentik_stages_source.sourcestage
|
||||
- authentik_stages_user_delete.userdeletestage
|
||||
- authentik_stages_user_login.userloginstage
|
||||
- authentik_stages_user_logout.userlogoutstage
|
||||
@ -18587,6 +18588,7 @@ paths:
|
||||
* `authentik_providers_rac.racprovider` - RAC Provider
|
||||
* `authentik_providers_rac.endpoint` - RAC Endpoint
|
||||
* `authentik_providers_rac.racpropertymapping` - RAC Property Mapping
|
||||
* `authentik_stages_source.sourcestage` - Source Stage
|
||||
* `authentik_events.event` - Event
|
||||
* `authentik_events.notificationtransport` - Notification Transport
|
||||
* `authentik_events.notification` - Notification
|
||||
@ -18800,6 +18802,7 @@ paths:
|
||||
- authentik_stages_password.passwordstage
|
||||
- authentik_stages_prompt.prompt
|
||||
- authentik_stages_prompt.promptstage
|
||||
- authentik_stages_source.sourcestage
|
||||
- authentik_stages_user_delete.userdeletestage
|
||||
- authentik_stages_user_login.userloginstage
|
||||
- authentik_stages_user_logout.userlogoutstage
|
||||
@ -18875,6 +18878,7 @@ paths:
|
||||
* `authentik_providers_rac.racprovider` - RAC Provider
|
||||
* `authentik_providers_rac.endpoint` - RAC Endpoint
|
||||
* `authentik_providers_rac.racpropertymapping` - RAC Property Mapping
|
||||
* `authentik_stages_source.sourcestage` - Source Stage
|
||||
* `authentik_events.event` - Event
|
||||
* `authentik_events.notificationtransport` - Notification Transport
|
||||
* `authentik_events.notification` - Notification
|
||||
@ -27872,6 +27876,289 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/stages/source/:
|
||||
get:
|
||||
operationId: stages_source_list
|
||||
description: SourceStage 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
|
||||
- in: query
|
||||
name: resume_timeout
|
||||
schema:
|
||||
type: string
|
||||
- name: search
|
||||
required: false
|
||||
in: query
|
||||
description: A search term.
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: source
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: stage_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
tags:
|
||||
- stages
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PaginatedSourceStageList'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
post:
|
||||
operationId: stages_source_create
|
||||
description: SourceStage Viewset
|
||||
tags:
|
||||
- stages
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SourceStageRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'201':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SourceStage'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/stages/source/{stage_uuid}/:
|
||||
get:
|
||||
operationId: stages_source_retrieve
|
||||
description: SourceStage Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: stage_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this Source Stage.
|
||||
required: true
|
||||
tags:
|
||||
- stages
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SourceStage'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
put:
|
||||
operationId: stages_source_update
|
||||
description: SourceStage Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: stage_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this Source Stage.
|
||||
required: true
|
||||
tags:
|
||||
- stages
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SourceStageRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SourceStage'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
patch:
|
||||
operationId: stages_source_partial_update
|
||||
description: SourceStage Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: stage_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this Source Stage.
|
||||
required: true
|
||||
tags:
|
||||
- stages
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PatchedSourceStageRequest'
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SourceStage'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
delete:
|
||||
operationId: stages_source_destroy
|
||||
description: SourceStage Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: stage_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this Source 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/source/{stage_uuid}/used_by/:
|
||||
get:
|
||||
operationId: stages_source_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 Source 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/user_delete/:
|
||||
get:
|
||||
operationId: stages_user_delete_list
|
||||
@ -29642,6 +29929,7 @@ components:
|
||||
- authentik.enterprise
|
||||
- authentik.enterprise.audit
|
||||
- authentik.enterprise.providers.rac
|
||||
- authentik.enterprise.stages.source
|
||||
- authentik.events
|
||||
type: string
|
||||
description: |-
|
||||
@ -29696,6 +29984,7 @@ components:
|
||||
* `authentik.enterprise` - authentik Enterprise
|
||||
* `authentik.enterprise.audit` - authentik Enterprise.Audit
|
||||
* `authentik.enterprise.providers.rac` - authentik Enterprise.Providers.RAC
|
||||
* `authentik.enterprise.stages.source` - authentik Enterprise.Stages.Source
|
||||
* `authentik.events` - authentik Events
|
||||
AppleChallengeResponseRequest:
|
||||
type: object
|
||||
@ -32057,7 +32346,10 @@ components:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ErrorDetail'
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- type
|
||||
DummyChallengeResponseRequest:
|
||||
type: object
|
||||
@ -32739,6 +33031,7 @@ components:
|
||||
* `authentik.enterprise` - authentik Enterprise
|
||||
* `authentik.enterprise.audit` - authentik Enterprise.Audit
|
||||
* `authentik.enterprise.providers.rac` - authentik Enterprise.Providers.RAC
|
||||
* `authentik.enterprise.stages.source` - authentik Enterprise.Stages.Source
|
||||
* `authentik.events` - authentik Events
|
||||
model:
|
||||
allOf:
|
||||
@ -32816,6 +33109,7 @@ components:
|
||||
* `authentik_providers_rac.racprovider` - RAC Provider
|
||||
* `authentik_providers_rac.endpoint` - RAC Endpoint
|
||||
* `authentik_providers_rac.racpropertymapping` - RAC Property Mapping
|
||||
* `authentik_stages_source.sourcestage` - Source Stage
|
||||
* `authentik_events.event` - Event
|
||||
* `authentik_events.notificationtransport` - Notification Transport
|
||||
* `authentik_events.notification` - Notification
|
||||
@ -32938,6 +33232,7 @@ components:
|
||||
* `authentik.enterprise` - authentik Enterprise
|
||||
* `authentik.enterprise.audit` - authentik Enterprise.Audit
|
||||
* `authentik.enterprise.providers.rac` - authentik Enterprise.Providers.RAC
|
||||
* `authentik.enterprise.stages.source` - authentik Enterprise.Stages.Source
|
||||
* `authentik.events` - authentik Events
|
||||
model:
|
||||
allOf:
|
||||
@ -33015,6 +33310,7 @@ components:
|
||||
* `authentik_providers_rac.racprovider` - RAC Provider
|
||||
* `authentik_providers_rac.endpoint` - RAC Endpoint
|
||||
* `authentik_providers_rac.racpropertymapping` - RAC Property Mapping
|
||||
* `authentik_stages_source.sourcestage` - Source Stage
|
||||
* `authentik_events.event` - Event
|
||||
* `authentik_events.notificationtransport` - Notification Transport
|
||||
* `authentik_events.notification` - Notification
|
||||
@ -35330,6 +35626,7 @@ components:
|
||||
- authentik_providers_rac.racprovider
|
||||
- authentik_providers_rac.endpoint
|
||||
- authentik_providers_rac.racpropertymapping
|
||||
- authentik_stages_source.sourcestage
|
||||
- authentik_events.event
|
||||
- authentik_events.notificationtransport
|
||||
- authentik_events.notification
|
||||
@ -35406,6 +35703,7 @@ components:
|
||||
* `authentik_providers_rac.racprovider` - RAC Provider
|
||||
* `authentik_providers_rac.endpoint` - RAC Endpoint
|
||||
* `authentik_providers_rac.racpropertymapping` - RAC Property Mapping
|
||||
* `authentik_stages_source.sourcestage` - Source Stage
|
||||
* `authentik_events.event` - Event
|
||||
* `authentik_events.notificationtransport` - Notification Transport
|
||||
* `authentik_events.notification` - Notification
|
||||
@ -37340,6 +37638,18 @@ components:
|
||||
required:
|
||||
- pagination
|
||||
- results
|
||||
PaginatedSourceStageList:
|
||||
type: object
|
||||
properties:
|
||||
pagination:
|
||||
$ref: '#/components/schemas/Pagination'
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/SourceStage'
|
||||
required:
|
||||
- pagination
|
||||
- results
|
||||
PaginatedStageList:
|
||||
type: object
|
||||
properties:
|
||||
@ -38583,6 +38893,7 @@ components:
|
||||
* `authentik.enterprise` - authentik Enterprise
|
||||
* `authentik.enterprise.audit` - authentik Enterprise.Audit
|
||||
* `authentik.enterprise.providers.rac` - authentik Enterprise.Providers.RAC
|
||||
* `authentik.enterprise.stages.source` - authentik Enterprise.Stages.Source
|
||||
* `authentik.events` - authentik Events
|
||||
model:
|
||||
allOf:
|
||||
@ -38660,6 +38971,7 @@ components:
|
||||
* `authentik_providers_rac.racprovider` - RAC Provider
|
||||
* `authentik_providers_rac.endpoint` - RAC Endpoint
|
||||
* `authentik_providers_rac.racpropertymapping` - RAC Property Mapping
|
||||
* `authentik_stages_source.sourcestage` - Source Stage
|
||||
* `authentik_events.event` - Event
|
||||
* `authentik_events.notificationtransport` - Notification Transport
|
||||
* `authentik_events.notification` - Notification
|
||||
@ -40242,6 +40554,25 @@ components:
|
||||
impersonation:
|
||||
type: boolean
|
||||
description: Globally enable/disable impersonation.
|
||||
PatchedSourceStageRequest:
|
||||
type: object
|
||||
description: SourceStage Serializer
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
flow_set:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/FlowSetRequest'
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
resume_timeout:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: 'Amount of time a user can take to return from the source to
|
||||
continue the flow (Format: hours=-1;minutes=-2;seconds=-3)'
|
||||
PatchedStaticDeviceRequest:
|
||||
type: object
|
||||
description: Serializer for static authenticator devices
|
||||
@ -43586,6 +43917,74 @@ components:
|
||||
required:
|
||||
- name
|
||||
- slug
|
||||
SourceStage:
|
||||
type: object
|
||||
description: SourceStage 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'
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
resume_timeout:
|
||||
type: string
|
||||
description: 'Amount of time a user can take to return from the source to
|
||||
continue the flow (Format: hours=-1;minutes=-2;seconds=-3)'
|
||||
required:
|
||||
- component
|
||||
- meta_model_name
|
||||
- name
|
||||
- pk
|
||||
- source
|
||||
- verbose_name
|
||||
- verbose_name_plural
|
||||
SourceStageRequest:
|
||||
type: object
|
||||
description: SourceStage Serializer
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
flow_set:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/FlowSetRequest'
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
resume_timeout:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: 'Amount of time a user can take to return from the source to
|
||||
continue the flow (Format: hours=-1;minutes=-2;seconds=-3)'
|
||||
required:
|
||||
- name
|
||||
- source
|
||||
SourceType:
|
||||
type: object
|
||||
description: Serializer for SourceType
|
||||
|
@ -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/source/SourceStageForm";
|
||||
import "@goauthentik/admin/stages/user_delete/UserDeleteStageForm";
|
||||
import "@goauthentik/admin/stages/user_login/UserLoginStageForm";
|
||||
import "@goauthentik/admin/stages/user_logout/UserLogoutStageForm";
|
||||
|
@ -1,3 +1,4 @@
|
||||
import "@goauthentik/admin/common/ak-license-notice";
|
||||
import { StageBindingForm } from "@goauthentik/admin/flows/StageBindingForm";
|
||||
import "@goauthentik/admin/stages/authenticator_duo/AuthenticatorDuoStageForm";
|
||||
import "@goauthentik/admin/stages/authenticator_sms/AuthenticatorSMSStageForm";
|
||||
@ -14,12 +15,14 @@ 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/source/SourceStageForm";
|
||||
import "@goauthentik/admin/stages/user_delete/UserDeleteStageForm";
|
||||
import "@goauthentik/admin/stages/user_login/UserLoginStageForm";
|
||||
import "@goauthentik/admin/stages/user_logout/UserLogoutStageForm";
|
||||
import "@goauthentik/admin/stages/user_write/UserWriteStageForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
|
||||
import "@goauthentik/elements/forms/ProxyForm";
|
||||
import "@goauthentik/elements/wizard/FormWizardPage";
|
||||
import { FormWizardPage } from "@goauthentik/elements/wizard/FormWizardPage";
|
||||
@ -28,7 +31,7 @@ import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
|
||||
import { CSSResult, TemplateResult, html } from "lit";
|
||||
import { CSSResult, TemplateResult, html, nothing } from "lit";
|
||||
import { property } from "lit/decorators.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
@ -39,7 +42,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
import { FlowStageBinding, Stage, StagesApi, TypeCreate } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-stage-wizard-initial")
|
||||
export class InitialStageWizardPage extends WizardPage {
|
||||
export class InitialStageWizardPage extends WithLicenseSummary(WizardPage) {
|
||||
@property({ attribute: false })
|
||||
stageTypes: TypeCreate[] = [];
|
||||
sidebarLabel = () => msg("Select type");
|
||||
@ -62,6 +65,7 @@ export class InitialStageWizardPage extends WizardPage {
|
||||
render(): TemplateResult {
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
${this.stageTypes.map((type) => {
|
||||
const requiresEnterprise = type.requiresEnterprise && !this.hasEnterpriseLicense;
|
||||
return html`<div class="pf-c-radio">
|
||||
<input
|
||||
class="pf-c-radio__input"
|
||||
@ -82,11 +86,15 @@ export class InitialStageWizardPage extends WizardPage {
|
||||
);
|
||||
this.host.isValid = true;
|
||||
}}
|
||||
?disabled=${requiresEnterprise}
|
||||
/>
|
||||
<label class="pf-c-radio__label" for=${`${type.component}-${type.modelName}`}
|
||||
>${type.name}</label
|
||||
>
|
||||
<span class="pf-c-radio__description">${type.description}</span>
|
||||
<span class="pf-c-radio__description">${type.description}${
|
||||
requiresEnterprise ? html`<ak-license-notice></ak-license-notice>` : nothing
|
||||
}</span>
|
||||
</span>
|
||||
</div>`;
|
||||
})}
|
||||
</form>`;
|
||||
|
99
web/src/admin/stages/source/SourceStageForm.ts
Normal file
99
web/src/admin/stages/source/SourceStageForm.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/SearchSelect/index";
|
||||
import "@goauthentik/elements/utils/TimeDeltaHelp";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
Source,
|
||||
SourceStage,
|
||||
SourcesAllListRequest,
|
||||
SourcesApi,
|
||||
StagesApi,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-stage-source-form")
|
||||
export class SourceStageForm extends BaseStageForm<SourceStage> {
|
||||
loadInstance(pk: string): Promise<SourceStage> {
|
||||
return new StagesApi(DEFAULT_CONFIG).stagesSourceRetrieve({
|
||||
stageUuid: pk,
|
||||
});
|
||||
}
|
||||
|
||||
async send(data: SourceStage): Promise<SourceStage> {
|
||||
if (this.instance) {
|
||||
return new StagesApi(DEFAULT_CONFIG).stagesSourceUpdate({
|
||||
stageUuid: this.instance.pk || "",
|
||||
sourceStageRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new StagesApi(DEFAULT_CONFIG).stagesSourceCreate({
|
||||
sourceStageRequest: data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`
|
||||
<span> ${msg("TODO.")} </span>
|
||||
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.name || "")}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Source")} ?required=${true} name="source">
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Source[]> => {
|
||||
const args: SourcesAllListRequest = {
|
||||
ordering: "name",
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const users = await new SourcesApi(DEFAULT_CONFIG).sourcesAllList(args);
|
||||
return users.results;
|
||||
}}
|
||||
.renderElement=${(source: Source): string => {
|
||||
return source.name;
|
||||
}}
|
||||
.renderDescription=${(source: Source): TemplateResult => {
|
||||
return html`${source.verboseName}`;
|
||||
}}
|
||||
.value=${(source: Source | undefined): string | undefined => {
|
||||
return source?.pk;
|
||||
}}
|
||||
.selected=${(source: Source): boolean => {
|
||||
return source.pk === this.instance?.source;
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Resume timeout")}
|
||||
?required=${true}
|
||||
name="resumeTimeout"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.resumeTimeout || "minutes=10")}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Amount of time a user can take to return from the source to continue the flow.",
|
||||
)}
|
||||
</p>
|
||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>
|
||||
</ak-form-element-horizontal>
|
||||
`;
|
||||
}
|
||||
}
|
@ -329,3 +329,7 @@ input[type="date"]::-webkit-calendar-picker-indicator {
|
||||
.pf-c-tree-view__content:focus-within {
|
||||
--pf-c-tree-view__node--hover--BackgroundColor: var(--ak-dark-background-light-ish);
|
||||
}
|
||||
/* stepper */
|
||||
.pf-c-progress-stepper__step-title {
|
||||
--pf-c-progress-stepper__step-title--Color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import {
|
||||
CapabilitiesEnum,
|
||||
ChallengeChoices,
|
||||
ChallengeTypes,
|
||||
ContextualFlowInfo,
|
||||
@ -162,7 +163,7 @@ export class FlowExecutor extends Interface implements StageHost {
|
||||
super();
|
||||
this.ws = new WebsocketClient();
|
||||
if (window.location.search.includes("inspector")) {
|
||||
this.inspectorOpen = !this.inspectorOpen;
|
||||
this.inspectorOpen = true;
|
||||
}
|
||||
this.addEventListener(EVENT_FLOW_INSPECTOR_TOGGLE, () => {
|
||||
this.inspectorOpen = !this.inspectorOpen;
|
||||
@ -213,6 +214,9 @@ export class FlowExecutor extends Interface implements StageHost {
|
||||
|
||||
async firstUpdated(): Promise<void> {
|
||||
configureSentry();
|
||||
if (this.config?.capabilities.includes(CapabilitiesEnum.CanDebug)) {
|
||||
this.inspectorOpen = true;
|
||||
}
|
||||
this.loading = true;
|
||||
try {
|
||||
const challenge = await new FlowsApi(DEFAULT_CONFIG).flowsExecutorGet({
|
||||
|
@ -37,6 +37,10 @@ export class FlowInspector extends AKElement {
|
||||
PFDescriptionList,
|
||||
PFProgressStepper,
|
||||
css`
|
||||
.pf-c-drawer__body {
|
||||
min-height: 100vh;
|
||||
max-height: 100vh;
|
||||
}
|
||||
code.break {
|
||||
word-break: break-all;
|
||||
}
|
||||
@ -45,9 +49,6 @@ export class FlowInspector extends AKElement {
|
||||
overflow-x: hidden;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
.pf-c-notification-drawer__body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
@ -113,6 +114,7 @@ export class FlowInspector extends AKElement {
|
||||
return this.renderAccessDenied();
|
||||
}
|
||||
if (!this.state) {
|
||||
this.advanceHandler();
|
||||
return html`<ak-empty-state ?loading="${true}" header=${msg("Loading")}>
|
||||
</ak-empty-state>`;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import "@goauthentik/elements/EmptyState";
|
||||
import "@goauthentik/flow/FormStatic";
|
||||
import { BaseStage } from "@goauthentik/flow/stages/base";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
@ -36,6 +36,7 @@ export class DummyStage extends BaseStage<DummyChallenge, DummyChallengeResponse
|
||||
this.submitForm(e);
|
||||
}}
|
||||
>
|
||||
<p>${msg(str`Stage name: ${this.challenge.name}`)}</p>
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
${msg("Continue")}
|
||||
|
@ -8457,6 +8457,9 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
||||
<trans-unit id="s6ecfc18dbfeedd76">
|
||||
<source>Select one of the options below to continue.</source>
|
||||
<target>Sélectionner une des options suivantes pour continuer.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4fe832d815563329">
|
||||
<source>TODO.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -53,6 +53,10 @@ Flows are an ordered sequence of stages. These flows can be used to define how a
|
||||
|
||||
A stage represents a single verification or logic step. They are used to authenticate users, enroll users, and more. These stages can optionally be applied to a flow via policies.
|
||||
|
||||
#### Dynamic in-memory stage
|
||||
|
||||
Certain use cases within authentik add steps that are run as part of a flow. These steps are a special type of stage called the "Dynamic in-memory" stage, as they are added to flows dynamically when required, only exist in memory, and are thus not configurable by administrators.
|
||||
|
||||
See [Flows](../flow/index.md)
|
||||
|
||||
### Property Mappings
|
||||
|
49
website/docs/flow/stages/source/index.md
Normal file
49
website/docs/flow/stages/source/index.md
Normal file
@ -0,0 +1,49 @@
|
||||
---
|
||||
title: Source stage
|
||||
---
|
||||
|
||||
<span class="badge badge--primary">Enterprise</span>
|
||||
<span class="badge badge--info">authentik 2024.4+</span>
|
||||
|
||||
---
|
||||
|
||||
The source stage injects an [OAuth](../../../../integrations/sources/oauth/) or [SAML](../../../../integrations/sources/saml/) Source into the flow execution. This allows for additional user verification, or to dynamically access different sources for different user identifiers (username, email address, etc).
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant u as User
|
||||
participant ak as authentik
|
||||
participant eidp as External IDP
|
||||
|
||||
u->>ak: User initiates flow
|
||||
ak->>u: User reaches Source Stage
|
||||
|
||||
u->>eidp: User is redirected to external IDP
|
||||
eidp->>ak: User has authenticated with external IDP
|
||||
|
||||
alt User is connected to external IDP (auth)
|
||||
ak->>u: Source's authentication flow is started
|
||||
u->>ak: User finishes source's authentication flow
|
||||
else User has not been connected to external IDP (enroll)
|
||||
ak->>u: Source's enrollment flow is started
|
||||
u->>ak: User finishes source's enrollment flow
|
||||
end
|
||||
|
||||
ak->>u: Execution of the previous flow is resumed
|
||||
```
|
||||
|
||||
### Considerations
|
||||
|
||||
It is very important that the configured source's authentication and enrollment flows (when set; they can be left unselected to prevent authentication or enrollment with the source) do **not** have a [User login stage](../user_login/index.md) bound to them.
|
||||
|
||||
This is because the Source stage works by appending a [dynamic in-memory](../../../core/terminology.md#dynamic-in-memory-stage) stage to the source's flow, so having a [User login stage](../user_login/index.md) bound will cause the source's flow to not resume the original flow it was started from, and instead directly authenticating the pending user.
|
||||
|
||||
### Options
|
||||
|
||||
#### `source`
|
||||
|
||||
The source the user is redirected to. Must be a web-based source, such as [OAuth](../../../../integrations/sources/oauth/) or [SAML](../../../../integrations/sources/saml/). Sources like [LDAP](../../../../integrations/sources/ldap/) are _not_ compatible.
|
||||
|
||||
#### `resume_timeout`
|
||||
|
||||
Because the execution of the current flow is suspended before the user is redirected to the configured source, this option configures how long the suspended flow is saved. If this timeout is exceeded, upon return from the configured source, the suspended flow will restart from the beginning.
|
@ -204,6 +204,7 @@ const docsSidebar = {
|
||||
"flow/stages/invitation/index",
|
||||
"flow/stages/password/index",
|
||||
"flow/stages/prompt/index",
|
||||
"flow/stages/source/index",
|
||||
"flow/stages/user_delete",
|
||||
"flow/stages/user_login/index",
|
||||
"flow/stages/user_logout",
|
||||
|
Reference in New Issue
Block a user