flows: provider invalidation (#5048)
* add initial Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add web stage for session end Signed-off-by: Jens Langhammer <jens@goauthentik.io> * migrate saml and tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * cleanup Signed-off-by: Jens Langhammer <jens@goauthentik.io> * group flow settings when providers have multiple flows Signed-off-by: Jens Langhammer <jens@goauthentik.io> * adjust name for default provider invalidation Signed-off-by: Jens Langhammer <jens@goauthentik.io> * re-make migrations Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add invalidation_flow to saml importer Signed-off-by: Jens Langhammer <jens@goauthentik.io> * re-do migrations again Signed-off-by: Jens Langhammer <jens@goauthentik.io> * update web stuff to get rid of old libraries Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make unbind flow for ldap configurable Signed-off-by: Jens Langhammer <jens@goauthentik.io> * unrelated: fix flow inspector Signed-off-by: Jens Langhammer <jens@goauthentik.io> * handle invalidation_flow as optional, as it should be Signed-off-by: Jens Langhammer <jens@goauthentik.io> * also fix ldap outpost Signed-off-by: Jens Langhammer <jens@goauthentik.io> * don't generate URL in client Signed-off-by: Jens Langhammer <jens@goauthentik.io> * actually make it work??? Signed-off-by: Jens Langhammer <jens@goauthentik.io> * format Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix migration breaking things...? Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start fixing tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix fallback Signed-off-by: Jens Langhammer <jens@goauthentik.io> * re-migrate Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix duplicate flow setting Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add migration Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix race condition with brand Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix oauth test Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix SAML tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add to wizard, fix required Signed-off-by: Jens Langhammer <jens@goauthentik.io> * update docs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make required, start release notes Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@ -29,9 +29,7 @@ def check_blueprint_v1_file(BlueprintInstance: type, db_alias, path: Path):
|
||||
if version != 1:
|
||||
return
|
||||
blueprint_file.seek(0)
|
||||
instance: BlueprintInstance = (
|
||||
BlueprintInstance.objects.using(db_alias).filter(path=path).first()
|
||||
)
|
||||
instance = BlueprintInstance.objects.using(db_alias).filter(path=path).first()
|
||||
rel_path = path.relative_to(Path(CONFIG.get("blueprints_dir")))
|
||||
meta = None
|
||||
if metadata:
|
||||
|
@ -38,6 +38,7 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"name",
|
||||
"authentication_flow",
|
||||
"authorization_flow",
|
||||
"invalidation_flow",
|
||||
"property_mappings",
|
||||
"component",
|
||||
"assigned_application_slug",
|
||||
@ -50,6 +51,7 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
||||
]
|
||||
extra_kwargs = {
|
||||
"authorization_flow": {"required": True, "allow_null": False},
|
||||
"invalidation_flow": {"required": True, "allow_null": False},
|
||||
}
|
||||
|
||||
|
||||
|
55
authentik/core/migrations/0040_provider_invalidation_flow.py
Normal file
55
authentik/core/migrations/0040_provider_invalidation_flow.py
Normal file
@ -0,0 +1,55 @@
|
||||
# Generated by Django 5.0.9 on 2024-10-02 11:35
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
|
||||
def migrate_invalidation_flow_default(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
from authentik.flows.models import FlowDesignation, FlowAuthenticationRequirement
|
||||
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
Flow = apps.get_model("authentik_flows", "Flow")
|
||||
Provider = apps.get_model("authentik_core", "Provider")
|
||||
|
||||
# So this flow is managed via a blueprint, bue we're in a migration so we don't want to rely on that
|
||||
# since the blueprint is just an empty flow we can just create it here
|
||||
# and let it be managed by the blueprint later
|
||||
flow, _ = Flow.objects.using(db_alias).update_or_create(
|
||||
slug="default-provider-invalidation-flow",
|
||||
defaults={
|
||||
"name": "Logged out of application",
|
||||
"title": "You've logged out of %(app)s.",
|
||||
"authentication": FlowAuthenticationRequirement.NONE,
|
||||
"designation": FlowDesignation.INVALIDATION,
|
||||
},
|
||||
)
|
||||
Provider.objects.using(db_alias).filter(invalidation_flow=None).update(invalidation_flow=flow)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0039_source_group_matching_mode_alter_group_name_and_more"),
|
||||
("authentik_flows", "0027_auto_20231028_1424"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="provider",
|
||||
name="invalidation_flow",
|
||||
field=models.ForeignKey(
|
||||
default=None,
|
||||
help_text="Flow used ending the session from a provider.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||
related_name="provider_invalidation",
|
||||
to="authentik_flows.flow",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(migrate_invalidation_flow_default),
|
||||
]
|
@ -391,14 +391,23 @@ class Provider(SerializerModel):
|
||||
),
|
||||
related_name="provider_authentication",
|
||||
)
|
||||
|
||||
authorization_flow = models.ForeignKey(
|
||||
"authentik_flows.Flow",
|
||||
# Set to cascade even though null is allowed, since most providers
|
||||
# still require an authorization flow set
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
help_text=_("Flow used when authorizing this provider."),
|
||||
related_name="provider_authorization",
|
||||
)
|
||||
invalidation_flow = models.ForeignKey(
|
||||
"authentik_flows.Flow",
|
||||
on_delete=models.SET_DEFAULT,
|
||||
default=None,
|
||||
null=True,
|
||||
help_text=_("Flow used ending the session from a provider."),
|
||||
related_name="provider_invalidation",
|
||||
)
|
||||
|
||||
property_mappings = models.ManyToManyField("PropertyMapping", default=None, blank=True)
|
||||
|
||||
|
@ -1,43 +0,0 @@
|
||||
{% extends 'login/base_full.html' %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}
|
||||
{% trans 'End session' %} - {{ brand.branding_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block card_title %}
|
||||
{% blocktrans with application=application.name %}
|
||||
You've logged out of {{ application }}.
|
||||
{% endblocktrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<form method="POST" class="pf-c-form">
|
||||
<p>
|
||||
{% blocktrans with application=application.name branding_title=brand.branding_title %}
|
||||
You've logged out of {{ application }}. You can go back to the overview to launch another application, or log out of your {{ branding_title }} account.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<a id="ak-back-home" href="{% url 'authentik_core:root-redirect' %}" class="pf-c-button pf-m-primary">
|
||||
{% trans 'Go back to overview' %}
|
||||
</a>
|
||||
|
||||
<a id="logout" href="{% url 'authentik_flows:default-invalidation' %}" class="pf-c-button pf-m-secondary">
|
||||
{% blocktrans with branding_title=brand.branding_title %}
|
||||
Log out of {{ branding_title }}
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
|
||||
{% if application.get_launch_url %}
|
||||
<a href="{{ application.get_launch_url }}" class="pf-c-button pf-m-secondary">
|
||||
{% blocktrans with application=application.name %}
|
||||
Log back into {{ application }}
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
</form>
|
||||
{% endblock %}
|
@ -134,6 +134,7 @@ class TestApplicationsAPI(APITestCase):
|
||||
"assigned_application_name": "allowed",
|
||||
"assigned_application_slug": "allowed",
|
||||
"authentication_flow": None,
|
||||
"invalidation_flow": None,
|
||||
"authorization_flow": str(self.provider.authorization_flow.pk),
|
||||
"component": "ak-provider-oauth2-form",
|
||||
"meta_model_name": "authentik_providers_oauth2.oauth2provider",
|
||||
@ -186,6 +187,7 @@ class TestApplicationsAPI(APITestCase):
|
||||
"assigned_application_name": "allowed",
|
||||
"assigned_application_slug": "allowed",
|
||||
"authentication_flow": None,
|
||||
"invalidation_flow": None,
|
||||
"authorization_flow": str(self.provider.authorization_flow.pk),
|
||||
"component": "ak-provider-oauth2-form",
|
||||
"meta_model_name": "authentik_providers_oauth2.oauth2provider",
|
||||
|
@ -19,7 +19,6 @@ class TestTransactionalApplicationsAPI(APITestCase):
|
||||
"""Test transactional Application + provider creation"""
|
||||
self.client.force_login(self.user)
|
||||
uid = generate_id()
|
||||
authorization_flow = create_test_flow()
|
||||
response = self.client.put(
|
||||
reverse("authentik_api:core-transactional-application"),
|
||||
data={
|
||||
@ -30,7 +29,8 @@ class TestTransactionalApplicationsAPI(APITestCase):
|
||||
"provider_model": "authentik_providers_oauth2.oauth2provider",
|
||||
"provider": {
|
||||
"name": uid,
|
||||
"authorization_flow": str(authorization_flow.pk),
|
||||
"authorization_flow": str(create_test_flow().pk),
|
||||
"invalidation_flow": str(create_test_flow().pk),
|
||||
},
|
||||
},
|
||||
)
|
||||
@ -56,10 +56,16 @@ class TestTransactionalApplicationsAPI(APITestCase):
|
||||
"provider": {
|
||||
"name": uid,
|
||||
"authorization_flow": "",
|
||||
"invalidation_flow": "",
|
||||
},
|
||||
},
|
||||
)
|
||||
self.assertJSONEqual(
|
||||
response.content.decode(),
|
||||
{"provider": {"authorization_flow": ["This field may not be null."]}},
|
||||
{
|
||||
"provider": {
|
||||
"authorization_flow": ["This field may not be null."],
|
||||
"invalidation_flow": ["This field may not be null."],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@ -24,7 +24,6 @@ from authentik.core.views.interface import (
|
||||
InterfaceView,
|
||||
RootRedirectView,
|
||||
)
|
||||
from authentik.core.views.session import EndSessionView
|
||||
from authentik.flows.views.interface import FlowInterfaceView
|
||||
from authentik.root.asgi_middleware import SessionMiddleware
|
||||
from authentik.root.messages.consumer import MessageConsumer
|
||||
@ -60,11 +59,6 @@ urlpatterns = [
|
||||
ensure_csrf_cookie(FlowInterfaceView.as_view()),
|
||||
name="if-flow",
|
||||
),
|
||||
path(
|
||||
"if/session-end/<slug:application_slug>/",
|
||||
ensure_csrf_cookie(EndSessionView.as_view()),
|
||||
name="if-session-end",
|
||||
),
|
||||
# Fallback for WS
|
||||
path("ws/outpost/<uuid:pk>/", InterfaceView.as_view(template_name="if/admin.html")),
|
||||
path(
|
||||
|
@ -1,23 +0,0 @@
|
||||
"""authentik Session Views"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.views.generic.base import TemplateView
|
||||
|
||||
from authentik.core.models import Application
|
||||
from authentik.policies.views import PolicyAccessView
|
||||
|
||||
|
||||
class EndSessionView(TemplateView, PolicyAccessView):
|
||||
"""Allow the client to end the Session"""
|
||||
|
||||
template_name = "if/end_session.html"
|
||||
|
||||
def resolve_provider_application(self):
|
||||
self.application = get_object_or_404(Application, slug=self.kwargs["application_slug"])
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["application"] = self.application
|
||||
return context
|
@ -68,6 +68,7 @@ class TestEndpointsAPI(APITestCase):
|
||||
"name": self.provider.name,
|
||||
"authentication_flow": None,
|
||||
"authorization_flow": None,
|
||||
"invalidation_flow": None,
|
||||
"property_mappings": [],
|
||||
"connection_expiry": "hours=8",
|
||||
"delete_token_on_disconnect": False,
|
||||
@ -120,6 +121,7 @@ class TestEndpointsAPI(APITestCase):
|
||||
"name": self.provider.name,
|
||||
"authentication_flow": None,
|
||||
"authorization_flow": None,
|
||||
"invalidation_flow": None,
|
||||
"property_mappings": [],
|
||||
"component": "ak-provider-rac-form",
|
||||
"assigned_application_slug": self.app.slug,
|
||||
@ -149,6 +151,7 @@ class TestEndpointsAPI(APITestCase):
|
||||
"name": self.provider.name,
|
||||
"authentication_flow": None,
|
||||
"authorization_flow": None,
|
||||
"invalidation_flow": None,
|
||||
"property_mappings": [],
|
||||
"component": "ak-provider-rac-form",
|
||||
"assigned_application_slug": self.app.slug,
|
||||
|
@ -110,9 +110,22 @@ class FlowErrorChallenge(Challenge):
|
||||
class AccessDeniedChallenge(WithUserInfoChallenge):
|
||||
"""Challenge when a flow's active stage calls `stage_invalid()`."""
|
||||
|
||||
error_message = CharField(required=False)
|
||||
component = CharField(default="ak-stage-access-denied")
|
||||
|
||||
error_message = CharField(required=False)
|
||||
|
||||
|
||||
class SessionEndChallenge(WithUserInfoChallenge):
|
||||
"""Challenge for ending a session"""
|
||||
|
||||
component = CharField(default="ak-stage-session-end")
|
||||
|
||||
application_name = CharField(required=False)
|
||||
application_launch_url = CharField(required=False)
|
||||
|
||||
invalidation_flow_url = CharField(required=False)
|
||||
brand_name = CharField(required=True)
|
||||
|
||||
|
||||
class PermissionDict(TypedDict):
|
||||
"""Consent Permission"""
|
||||
|
@ -6,20 +6,18 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
|
||||
def set_oobe_flow_authentication(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
from guardian.conf import settings as guardian_settings
|
||||
|
||||
Flow = apps.get_model("authentik_flows", "Flow")
|
||||
User = apps.get_model("authentik_core", "User")
|
||||
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
users = User.objects.using(db_alias).exclude(username="akadmin")
|
||||
try:
|
||||
users = users.exclude(pk=get_anonymous_user().pk)
|
||||
|
||||
except Exception: # nosec
|
||||
pass
|
||||
|
||||
users = (
|
||||
User.objects.using(db_alias)
|
||||
.exclude(username="akadmin")
|
||||
.exclude(username=guardian_settings.ANONYMOUS_USER_NAME)
|
||||
)
|
||||
if users.exists():
|
||||
Flow.objects.using(db_alias).filter(slug="initial-setup").update(
|
||||
authentication="require_superuser"
|
||||
|
@ -107,7 +107,9 @@ class Stage(SerializerModel):
|
||||
|
||||
|
||||
def in_memory_stage(view: type["StageView"], **kwargs) -> Stage:
|
||||
"""Creates an in-memory stage instance, based on a `view` as view."""
|
||||
"""Creates an in-memory stage instance, based on a `view` as view.
|
||||
Any key-word arguments are set as attributes on the stage object,
|
||||
accessible via `self.executor.current_stage`."""
|
||||
stage = Stage()
|
||||
# Because we can't pickle a locally generated function,
|
||||
# we set the view as a separate property and reference a generic function
|
||||
|
@ -13,7 +13,7 @@ from rest_framework.request import Request
|
||||
from sentry_sdk import start_span
|
||||
from structlog.stdlib import BoundLogger, get_logger
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.core.models import Application, User
|
||||
from authentik.flows.challenge import (
|
||||
AccessDeniedChallenge,
|
||||
Challenge,
|
||||
@ -21,6 +21,7 @@ from authentik.flows.challenge import (
|
||||
ContextualFlowInfo,
|
||||
HttpChallengeResponse,
|
||||
RedirectChallenge,
|
||||
SessionEndChallenge,
|
||||
WithUserInfoChallenge,
|
||||
)
|
||||
from authentik.flows.exceptions import StageInvalidException
|
||||
@ -230,7 +231,7 @@ class ChallengeStageView(StageView):
|
||||
return HttpChallengeResponse(challenge_response)
|
||||
|
||||
|
||||
class AccessDeniedChallengeView(ChallengeStageView):
|
||||
class AccessDeniedStage(ChallengeStageView):
|
||||
"""Used internally by FlowExecutor's stage_invalid()"""
|
||||
|
||||
error_message: str | None
|
||||
@ -268,3 +269,31 @@ class RedirectStage(ChallengeStageView):
|
||||
|
||||
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
|
||||
return HttpChallengeResponse(self.get_challenge())
|
||||
|
||||
|
||||
class SessionEndStage(ChallengeStageView):
|
||||
"""Stage inserted when a flow is used as invalidation flow. By default shows actions
|
||||
that the user is likely to take after signing out of a provider."""
|
||||
|
||||
def get_challenge(self, *args, **kwargs) -> Challenge:
|
||||
application: Application | None = self.executor.plan.context.get(PLAN_CONTEXT_APPLICATION)
|
||||
data = {
|
||||
"component": "ak-stage-session-end",
|
||||
"brand_name": self.request.brand.branding_title,
|
||||
}
|
||||
if application:
|
||||
data["application_name"] = application.name
|
||||
data["application_launch_url"] = application.get_launch_url(self.get_pending_user())
|
||||
if self.request.brand.flow_invalidation:
|
||||
data["invalidation_flow_url"] = reverse(
|
||||
"authentik_core:if-flow",
|
||||
kwargs={
|
||||
"flow_slug": self.request.brand.flow_invalidation.slug,
|
||||
},
|
||||
)
|
||||
return SessionEndChallenge(data=data)
|
||||
|
||||
# This can never be reached since this challenge is created on demand and only the
|
||||
# .get() method is called
|
||||
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: # pragma: no cover
|
||||
return self.executor.cancel()
|
||||
|
@ -54,7 +54,7 @@ from authentik.flows.planner import (
|
||||
FlowPlan,
|
||||
FlowPlanner,
|
||||
)
|
||||
from authentik.flows.stage import AccessDeniedChallengeView, StageView
|
||||
from authentik.flows.stage import AccessDeniedStage, StageView
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.lib.utils.reflection import all_subclasses, class_to_path
|
||||
@ -441,7 +441,7 @@ class FlowExecutorView(APIView):
|
||||
)
|
||||
return self.restart_flow(keep_context)
|
||||
self.cancel()
|
||||
challenge_view = AccessDeniedChallengeView(self, error_message)
|
||||
challenge_view = AccessDeniedStage(self, error_message)
|
||||
challenge_view.request = self.request
|
||||
return to_stage_response(self.request, challenge_view.get(self.request))
|
||||
|
||||
|
@ -87,6 +87,7 @@ class LDAPOutpostConfigSerializer(ModelSerializer):
|
||||
|
||||
application_slug = SerializerMethodField()
|
||||
bind_flow_slug = CharField(source="authorization_flow.slug")
|
||||
unbind_flow_slug = SerializerMethodField()
|
||||
|
||||
def get_application_slug(self, instance: LDAPProvider) -> str:
|
||||
"""Prioritise backchannel slug over direct application slug"""
|
||||
@ -94,6 +95,16 @@ class LDAPOutpostConfigSerializer(ModelSerializer):
|
||||
return instance.backchannel_application.slug
|
||||
return instance.application.slug
|
||||
|
||||
def get_unbind_flow_slug(self, instance: LDAPProvider) -> str | None:
|
||||
"""Get slug for unbind flow, defaulting to brand's default flow."""
|
||||
flow = instance.invalidation_flow
|
||||
if not flow and "request" in self.context:
|
||||
request = self.context.get("request")
|
||||
flow = request.brand.flow_invalidation
|
||||
if not flow:
|
||||
return None
|
||||
return flow.slug
|
||||
|
||||
class Meta:
|
||||
model = LDAPProvider
|
||||
fields = [
|
||||
@ -101,6 +112,7 @@ class LDAPOutpostConfigSerializer(ModelSerializer):
|
||||
"name",
|
||||
"base_dn",
|
||||
"bind_flow_slug",
|
||||
"unbind_flow_slug",
|
||||
"application_slug",
|
||||
"certificate",
|
||||
"tls_server_name",
|
||||
|
@ -12,6 +12,7 @@ from authentik.providers.oauth2.api.tokens import (
|
||||
)
|
||||
from authentik.providers.oauth2.views.authorize import AuthorizationFlowInitView
|
||||
from authentik.providers.oauth2.views.device_backchannel import DeviceView
|
||||
from authentik.providers.oauth2.views.end_session import EndSessionView
|
||||
from authentik.providers.oauth2.views.introspection import TokenIntrospectionView
|
||||
from authentik.providers.oauth2.views.jwks import JWKSView
|
||||
from authentik.providers.oauth2.views.provider import ProviderInfoView
|
||||
@ -44,7 +45,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"<slug:application_slug>/end-session/",
|
||||
RedirectView.as_view(pattern_name="authentik_core:if-session-end", query_string=True),
|
||||
EndSessionView.as_view(),
|
||||
name="end-session",
|
||||
),
|
||||
path("<slug:application_slug>/jwks/", JWKSView.as_view(), name="jwks"),
|
||||
|
45
authentik/providers/oauth2/views/end_session.py
Normal file
45
authentik/providers/oauth2/views/end_session.py
Normal file
@ -0,0 +1,45 @@
|
||||
"""oauth2 provider end_session Views"""
|
||||
|
||||
from django.http import Http404, HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from authentik.core.models import Application
|
||||
from authentik.flows.models import Flow, in_memory_stage
|
||||
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
|
||||
from authentik.flows.stage import SessionEndStage
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.lib.utils.urls import redirect_with_qs
|
||||
from authentik.policies.views import PolicyAccessView
|
||||
|
||||
|
||||
class EndSessionView(PolicyAccessView):
|
||||
"""Redirect to application's provider's invalidation flow"""
|
||||
|
||||
flow: Flow
|
||||
|
||||
def resolve_provider_application(self):
|
||||
self.application = get_object_or_404(Application, slug=self.kwargs["application_slug"])
|
||||
self.provider = self.application.get_provider()
|
||||
if not self.provider:
|
||||
raise Http404
|
||||
self.flow = self.provider.invalidation_flow or self.request.brand.flow_invalidation
|
||||
if not self.flow:
|
||||
raise Http404
|
||||
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""Dispatch the flow planner for the invalidation flow"""
|
||||
planner = FlowPlanner(self.flow)
|
||||
planner.allow_empty_flows = True
|
||||
plan = planner.plan(
|
||||
request,
|
||||
{
|
||||
PLAN_CONTEXT_APPLICATION: self.application,
|
||||
},
|
||||
)
|
||||
plan.insert_stage(in_memory_stage(SessionEndStage))
|
||||
request.session[SESSION_KEY_PLAN] = plan
|
||||
return redirect_with_qs(
|
||||
"authentik_core:if-flow",
|
||||
self.request.GET,
|
||||
flow_slug=self.flow.slug,
|
||||
)
|
@ -24,6 +24,7 @@ class ProxyProviderTests(APITestCase):
|
||||
"name": generate_id(),
|
||||
"mode": ProxyMode.PROXY,
|
||||
"authorization_flow": create_test_flow().pk.hex,
|
||||
"invalidation_flow": create_test_flow().pk.hex,
|
||||
"external_host": "http://localhost",
|
||||
"internal_host": "http://localhost",
|
||||
"basic_auth_enabled": True,
|
||||
@ -41,6 +42,7 @@ class ProxyProviderTests(APITestCase):
|
||||
"name": generate_id(),
|
||||
"mode": ProxyMode.PROXY,
|
||||
"authorization_flow": create_test_flow().pk.hex,
|
||||
"invalidation_flow": create_test_flow().pk.hex,
|
||||
"external_host": "http://localhost",
|
||||
"internal_host": "http://localhost",
|
||||
"basic_auth_enabled": True,
|
||||
@ -64,6 +66,7 @@ class ProxyProviderTests(APITestCase):
|
||||
"name": generate_id(),
|
||||
"mode": ProxyMode.PROXY,
|
||||
"authorization_flow": create_test_flow().pk.hex,
|
||||
"invalidation_flow": create_test_flow().pk.hex,
|
||||
"external_host": "http://localhost",
|
||||
},
|
||||
)
|
||||
@ -82,6 +85,7 @@ class ProxyProviderTests(APITestCase):
|
||||
"name": name,
|
||||
"mode": ProxyMode.PROXY,
|
||||
"authorization_flow": create_test_flow().pk.hex,
|
||||
"invalidation_flow": create_test_flow().pk.hex,
|
||||
"external_host": "http://localhost",
|
||||
"internal_host": "http://localhost",
|
||||
},
|
||||
@ -99,6 +103,7 @@ class ProxyProviderTests(APITestCase):
|
||||
"name": name,
|
||||
"mode": ProxyMode.PROXY,
|
||||
"authorization_flow": create_test_flow().pk.hex,
|
||||
"invalidation_flow": create_test_flow().pk.hex,
|
||||
"external_host": "http://localhost",
|
||||
"internal_host": "http://localhost",
|
||||
},
|
||||
@ -114,6 +119,7 @@ class ProxyProviderTests(APITestCase):
|
||||
"name": name,
|
||||
"mode": ProxyMode.PROXY,
|
||||
"authorization_flow": create_test_flow().pk.hex,
|
||||
"invalidation_flow": create_test_flow().pk.hex,
|
||||
"external_host": "http://localhost",
|
||||
"internal_host": "http://localhost",
|
||||
},
|
||||
|
@ -188,6 +188,9 @@ class SAMLProviderImportSerializer(PassiveSerializer):
|
||||
authorization_flow = PrimaryKeyRelatedField(
|
||||
queryset=Flow.objects.filter(designation=FlowDesignation.AUTHORIZATION),
|
||||
)
|
||||
invalidation_flow = PrimaryKeyRelatedField(
|
||||
queryset=Flow.objects.filter(designation=FlowDesignation.INVALIDATION),
|
||||
)
|
||||
file = FileField()
|
||||
|
||||
|
||||
@ -277,7 +280,9 @@ class SAMLProviderViewSet(UsedByMixin, ModelViewSet):
|
||||
try:
|
||||
metadata = ServiceProviderMetadataParser().parse(file.read().decode())
|
||||
metadata.to_provider(
|
||||
data.validated_data["name"], data.validated_data["authorization_flow"]
|
||||
data.validated_data["name"],
|
||||
data.validated_data["authorization_flow"],
|
||||
data.validated_data["invalidation_flow"],
|
||||
)
|
||||
except ValueError as exc: # pragma: no cover
|
||||
LOGGER.warning(str(exc))
|
||||
|
@ -49,12 +49,13 @@ class ServiceProviderMetadata:
|
||||
|
||||
signing_keypair: CertificateKeyPair | None = None
|
||||
|
||||
def to_provider(self, name: str, authorization_flow: Flow) -> SAMLProvider:
|
||||
def to_provider(
|
||||
self, name: str, authorization_flow: Flow, invalidation_flow: Flow
|
||||
) -> SAMLProvider:
|
||||
"""Create a SAMLProvider instance from the details. `name` is required,
|
||||
as depending on the metadata CertificateKeypairs might have to be created."""
|
||||
provider = SAMLProvider.objects.create(
|
||||
name=name,
|
||||
authorization_flow=authorization_flow,
|
||||
name=name, authorization_flow=authorization_flow, invalidation_flow=invalidation_flow
|
||||
)
|
||||
provider.issuer = self.entity_id
|
||||
provider.sp_binding = self.acs_binding
|
||||
|
@ -47,11 +47,12 @@ class TestSAMLProviderAPI(APITestCase):
|
||||
data={
|
||||
"name": generate_id(),
|
||||
"authorization_flow": create_test_flow().pk,
|
||||
"invalidation_flow": create_test_flow().pk,
|
||||
"acs_url": "http://localhost",
|
||||
"signing_kp": cert.pk,
|
||||
},
|
||||
)
|
||||
self.assertEqual(400, response.status_code)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
response.content,
|
||||
{
|
||||
@ -68,12 +69,13 @@ class TestSAMLProviderAPI(APITestCase):
|
||||
data={
|
||||
"name": generate_id(),
|
||||
"authorization_flow": create_test_flow().pk,
|
||||
"invalidation_flow": create_test_flow().pk,
|
||||
"acs_url": "http://localhost",
|
||||
"signing_kp": cert.pk,
|
||||
"sign_assertion": True,
|
||||
},
|
||||
)
|
||||
self.assertEqual(201, response.status_code)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
def test_metadata(self):
|
||||
"""Test metadata export (normal)"""
|
||||
@ -131,6 +133,7 @@ class TestSAMLProviderAPI(APITestCase):
|
||||
"file": metadata,
|
||||
"name": generate_id(),
|
||||
"authorization_flow": create_test_flow(FlowDesignation.AUTHORIZATION).pk,
|
||||
"invalidation_flow": create_test_flow(FlowDesignation.INVALIDATION).pk,
|
||||
},
|
||||
format="multipart",
|
||||
)
|
||||
|
@ -82,7 +82,7 @@ class TestServiceProviderMetadataParser(TestCase):
|
||||
def test_simple(self):
|
||||
"""Test simple metadata without Signing"""
|
||||
metadata = ServiceProviderMetadataParser().parse(load_fixture("fixtures/simple.xml"))
|
||||
provider = metadata.to_provider("test", self.flow)
|
||||
provider = metadata.to_provider("test", self.flow, self.flow)
|
||||
self.assertEqual(provider.acs_url, "http://localhost:8080/saml/acs")
|
||||
self.assertEqual(provider.issuer, "http://localhost:8080/saml/metadata")
|
||||
self.assertEqual(provider.sp_binding, SAMLBindings.POST)
|
||||
@ -95,7 +95,7 @@ class TestServiceProviderMetadataParser(TestCase):
|
||||
"""Test Metadata with signing cert"""
|
||||
create_test_cert()
|
||||
metadata = ServiceProviderMetadataParser().parse(load_fixture("fixtures/cert.xml"))
|
||||
provider = metadata.to_provider("test", self.flow)
|
||||
provider = metadata.to_provider("test", self.flow, self.flow)
|
||||
self.assertEqual(provider.acs_url, "http://localhost:8080/apps/user_saml/saml/acs")
|
||||
self.assertEqual(provider.issuer, "http://localhost:8080/apps/user_saml/saml/metadata")
|
||||
self.assertEqual(provider.sp_binding, SAMLBindings.POST)
|
||||
|
@ -1,8 +1,8 @@
|
||||
"""SLO Views"""
|
||||
|
||||
from django.http import HttpRequest
|
||||
from django.http import Http404, HttpRequest
|
||||
from django.http.response import HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.clickjacking import xframe_options_sameorigin
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
@ -10,6 +10,11 @@ from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import Application
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.models import Flow, in_memory_stage
|
||||
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
|
||||
from authentik.flows.stage import SessionEndStage
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.lib.utils.urls import redirect_with_qs
|
||||
from authentik.lib.views import bad_request_message
|
||||
from authentik.policies.views import PolicyAccessView
|
||||
from authentik.providers.saml.exceptions import CannotHandleAssertion
|
||||
@ -28,11 +33,16 @@ class SAMLSLOView(PolicyAccessView):
|
||||
""" "SAML SLO Base View, which plans a flow and injects our final stage.
|
||||
Calls get/post handler."""
|
||||
|
||||
flow: Flow
|
||||
|
||||
def resolve_provider_application(self):
|
||||
self.application = get_object_or_404(Application, slug=self.kwargs["application_slug"])
|
||||
self.provider: SAMLProvider = get_object_or_404(
|
||||
SAMLProvider, pk=self.application.provider_id
|
||||
)
|
||||
self.flow = self.provider.invalidation_flow or self.request.brand.flow_invalidation
|
||||
if not self.flow:
|
||||
raise Http404
|
||||
|
||||
def check_saml_request(self) -> HttpRequest | None:
|
||||
"""Handler to verify the SAML Request. Must be implemented by a subclass"""
|
||||
@ -45,9 +55,20 @@ class SAMLSLOView(PolicyAccessView):
|
||||
method_response = self.check_saml_request()
|
||||
if method_response:
|
||||
return method_response
|
||||
return redirect(
|
||||
"authentik_core:if-session-end",
|
||||
application_slug=self.kwargs["application_slug"],
|
||||
planner = FlowPlanner(self.flow)
|
||||
planner.allow_empty_flows = True
|
||||
plan = planner.plan(
|
||||
request,
|
||||
{
|
||||
PLAN_CONTEXT_APPLICATION: self.application,
|
||||
},
|
||||
)
|
||||
plan.insert_stage(in_memory_stage(SessionEndStage))
|
||||
request.session[SESSION_KEY_PLAN] = plan
|
||||
return redirect_with_qs(
|
||||
"authentik_core:if-flow",
|
||||
self.request.GET,
|
||||
flow_slug=self.flow.slug,
|
||||
)
|
||||
|
||||
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
|
||||
|
13
blueprints/default/flow-default-provider-invalidation.yaml
Normal file
13
blueprints/default/flow-default-provider-invalidation.yaml
Normal file
@ -0,0 +1,13 @@
|
||||
version: 1
|
||||
metadata:
|
||||
name: Default - Provider invalidation flow
|
||||
entries:
|
||||
- attrs:
|
||||
designation: invalidation
|
||||
name: Logged out of application
|
||||
title: You've logged out of %(app)s.
|
||||
authentication: none
|
||||
identifiers:
|
||||
slug: default-provider-invalidation-flow
|
||||
model: authentik_flows.flow
|
||||
id: flow
|
@ -5117,6 +5117,12 @@
|
||||
"title": "Authorization flow",
|
||||
"description": "Flow used when authorizing this provider."
|
||||
},
|
||||
"invalidation_flow": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Invalidation flow",
|
||||
"description": "Flow used ending the session from a provider."
|
||||
},
|
||||
"property_mappings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@ -5287,6 +5293,12 @@
|
||||
"title": "Authorization flow",
|
||||
"description": "Flow used when authorizing this provider."
|
||||
},
|
||||
"invalidation_flow": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Invalidation flow",
|
||||
"description": "Flow used ending the session from a provider."
|
||||
},
|
||||
"property_mappings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@ -5428,6 +5440,12 @@
|
||||
"title": "Authorization flow",
|
||||
"description": "Flow used when authorizing this provider."
|
||||
},
|
||||
"invalidation_flow": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Invalidation flow",
|
||||
"description": "Flow used ending the session from a provider."
|
||||
},
|
||||
"property_mappings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@ -5563,6 +5581,12 @@
|
||||
"title": "Authorization flow",
|
||||
"description": "Flow used when authorizing this provider."
|
||||
},
|
||||
"invalidation_flow": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Invalidation flow",
|
||||
"description": "Flow used ending the session from a provider."
|
||||
},
|
||||
"property_mappings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@ -5688,6 +5712,12 @@
|
||||
"title": "Authorization flow",
|
||||
"description": "Flow used when authorizing this provider."
|
||||
},
|
||||
"invalidation_flow": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Invalidation flow",
|
||||
"description": "Flow used ending the session from a provider."
|
||||
},
|
||||
"property_mappings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@ -12761,6 +12791,12 @@
|
||||
"title": "Authorization flow",
|
||||
"description": "Flow used when authorizing this provider."
|
||||
},
|
||||
"invalidation_flow": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Invalidation flow",
|
||||
"description": "Flow used ending the session from a provider."
|
||||
},
|
||||
"property_mappings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
@ -8,11 +8,17 @@ import (
|
||||
)
|
||||
|
||||
func (db *DirectBinder) Unbind(username string, req *bind.Request) (ldap.LDAPResultCode, error) {
|
||||
flowSlug := db.si.GetInvalidationFlowSlug()
|
||||
if flowSlug == nil {
|
||||
req.Log().Debug("Provider does not have a logout flow configured")
|
||||
db.si.SetFlags(req.BindDN, nil)
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
flags := db.si.GetFlags(req.BindDN)
|
||||
if flags == nil || flags.Session == nil {
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
fe := flow.NewFlowExecutor(req.Context(), db.si.GetInvalidationFlowSlug(), db.si.GetAPIClient().GetConfig(), log.Fields{
|
||||
fe := flow.NewFlowExecutor(req.Context(), *flowSlug, db.si.GetAPIClient().GetConfig(), log.Fields{
|
||||
"boundDN": req.BindDN,
|
||||
"client": req.RemoteAddr(),
|
||||
"requestId": req.ID(),
|
||||
@ -22,7 +28,7 @@ func (db *DirectBinder) Unbind(username string, req *bind.Request) (ldap.LDAPRes
|
||||
fe.Params.Add("goauthentik.io/outpost/ldap", "true")
|
||||
_, err := fe.Execute()
|
||||
if err != nil {
|
||||
db.log.WithError(err).Warning("failed to logout user")
|
||||
req.Log().WithError(err).Warning("failed to logout user")
|
||||
}
|
||||
db.si.SetFlags(req.BindDN, nil)
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
|
@ -26,7 +26,7 @@ type ProviderInstance struct {
|
||||
|
||||
appSlug string
|
||||
authenticationFlowSlug string
|
||||
invalidationFlowSlug string
|
||||
invalidationFlowSlug *string
|
||||
s *LDAPServer
|
||||
log *log.Entry
|
||||
|
||||
@ -99,7 +99,7 @@ func (pi *ProviderInstance) GetAuthenticationFlowSlug() string {
|
||||
return pi.authenticationFlowSlug
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GetInvalidationFlowSlug() string {
|
||||
func (pi *ProviderInstance) GetInvalidationFlowSlug() *string {
|
||||
return pi.invalidationFlowSlug
|
||||
}
|
||||
|
||||
|
@ -29,16 +29,6 @@ func (ls *LDAPServer) getCurrentProvider(pk int32) *ProviderInstance {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ls *LDAPServer) getInvalidationFlow() string {
|
||||
req, _, err := ls.ac.Client.CoreApi.CoreBrandsCurrentRetrieve(context.Background()).Execute()
|
||||
if err != nil {
|
||||
ls.log.WithError(err).Warning("failed to fetch brand config")
|
||||
return ""
|
||||
}
|
||||
flow := req.GetFlowInvalidation()
|
||||
return flow
|
||||
}
|
||||
|
||||
func (ls *LDAPServer) Refresh() error {
|
||||
apiProviders, err := ak.Paginator(ls.ac.Client.OutpostsApi.OutpostsLdapList(context.Background()), ak.PaginatorOptions{
|
||||
PageSize: 100,
|
||||
@ -51,7 +41,6 @@ func (ls *LDAPServer) Refresh() error {
|
||||
return errors.New("no ldap provider defined")
|
||||
}
|
||||
providers := make([]*ProviderInstance, len(apiProviders))
|
||||
invalidationFlow := ls.getInvalidationFlow()
|
||||
for idx, provider := range apiProviders {
|
||||
userDN := strings.ToLower(fmt.Sprintf("ou=%s,%s", constants.OUUsers, *provider.BaseDn))
|
||||
groupDN := strings.ToLower(fmt.Sprintf("ou=%s,%s", constants.OUGroups, *provider.BaseDn))
|
||||
@ -75,7 +64,7 @@ func (ls *LDAPServer) Refresh() error {
|
||||
UserDN: userDN,
|
||||
appSlug: provider.ApplicationSlug,
|
||||
authenticationFlowSlug: provider.BindFlowSlug,
|
||||
invalidationFlowSlug: invalidationFlow,
|
||||
invalidationFlowSlug: provider.UnbindFlowSlug.Get(),
|
||||
boundUsersMutex: usersMutex,
|
||||
boundUsers: users,
|
||||
s: ls,
|
||||
|
@ -12,7 +12,7 @@ type LDAPServerInstance interface {
|
||||
GetOutpostName() string
|
||||
|
||||
GetAuthenticationFlowSlug() string
|
||||
GetInvalidationFlowSlug() string
|
||||
GetInvalidationFlowSlug() *string
|
||||
GetAppSlug() string
|
||||
GetProviderID() int32
|
||||
|
||||
|
142
schema.yml
142
schema.yml
@ -20755,6 +20755,11 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: invalidation_flow
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: is_backchannel
|
||||
schema:
|
||||
@ -37547,6 +37552,7 @@ components:
|
||||
- $ref: '#/components/schemas/PlexAuthenticationChallenge'
|
||||
- $ref: '#/components/schemas/PromptChallenge'
|
||||
- $ref: '#/components/schemas/RedirectChallenge'
|
||||
- $ref: '#/components/schemas/SessionEndChallenge'
|
||||
- $ref: '#/components/schemas/ShellChallenge'
|
||||
- $ref: '#/components/schemas/UserLoginChallenge'
|
||||
discriminator:
|
||||
@ -37573,6 +37579,7 @@ components:
|
||||
ak-source-plex: '#/components/schemas/PlexAuthenticationChallenge'
|
||||
ak-stage-prompt: '#/components/schemas/PromptChallenge'
|
||||
xak-flow-redirect: '#/components/schemas/RedirectChallenge'
|
||||
ak-stage-session-end: '#/components/schemas/SessionEndChallenge'
|
||||
xak-flow-shell: '#/components/schemas/ShellChallenge'
|
||||
ak-stage-user-login: '#/components/schemas/UserLoginChallenge'
|
||||
ClientTypeEnum:
|
||||
@ -40923,6 +40930,11 @@ components:
|
||||
description: DN under which objects are accessible.
|
||||
bind_flow_slug:
|
||||
type: string
|
||||
unbind_flow_slug:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Get slug for unbind flow, defaulting to brand's default flow.
|
||||
readOnly: true
|
||||
application_slug:
|
||||
type: string
|
||||
description: Prioritise backchannel slug over direct application slug
|
||||
@ -40964,6 +40976,7 @@ components:
|
||||
- bind_flow_slug
|
||||
- name
|
||||
- pk
|
||||
- unbind_flow_slug
|
||||
LDAPProvider:
|
||||
type: object
|
||||
description: LDAPProvider Serializer
|
||||
@ -40984,6 +40997,10 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used when authorizing this provider.
|
||||
invalidation_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used ending the session from a provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
@ -41068,6 +41085,7 @@ components:
|
||||
- assigned_backchannel_application_slug
|
||||
- authorization_flow
|
||||
- component
|
||||
- invalidation_flow
|
||||
- meta_model_name
|
||||
- name
|
||||
- outpost_set
|
||||
@ -41091,6 +41109,10 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used when authorizing this provider.
|
||||
invalidation_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used ending the session from a provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
@ -41134,6 +41156,7 @@ components:
|
||||
if it contains a semicolon.
|
||||
required:
|
||||
- authorization_flow
|
||||
- invalidation_flow
|
||||
- name
|
||||
LDAPSource:
|
||||
type: object
|
||||
@ -42282,6 +42305,10 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used when authorizing this provider.
|
||||
invalidation_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used ending the session from a provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
@ -42379,6 +42406,7 @@ components:
|
||||
- assigned_backchannel_application_slug
|
||||
- authorization_flow
|
||||
- component
|
||||
- invalidation_flow
|
||||
- meta_model_name
|
||||
- name
|
||||
- pk
|
||||
@ -42401,6 +42429,10 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used when authorizing this provider.
|
||||
invalidation_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used ending the session from a provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
@ -42465,6 +42497,7 @@ components:
|
||||
title: Any JWT signed by the JWK of the selected source can be used to authenticate.
|
||||
required:
|
||||
- authorization_flow
|
||||
- invalidation_flow
|
||||
- name
|
||||
OAuth2ProviderSetupURLs:
|
||||
type: object
|
||||
@ -45857,6 +45890,10 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used when authorizing this provider.
|
||||
invalidation_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used ending the session from a provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
@ -46177,6 +46214,10 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used when authorizing this provider.
|
||||
invalidation_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used ending the session from a provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
@ -46701,6 +46742,10 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used when authorizing this provider.
|
||||
invalidation_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used ending the session from a provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
@ -46806,6 +46851,10 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used when authorizing this provider.
|
||||
invalidation_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used ending the session from a provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
@ -46856,6 +46905,10 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used when authorizing this provider.
|
||||
invalidation_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used ending the session from a provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
@ -46947,6 +47000,10 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used when authorizing this provider.
|
||||
invalidation_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used ending the session from a provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
@ -48465,6 +48522,10 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used when authorizing this provider.
|
||||
invalidation_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used ending the session from a provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
@ -48509,6 +48570,7 @@ components:
|
||||
- assigned_backchannel_application_slug
|
||||
- authorization_flow
|
||||
- component
|
||||
- invalidation_flow
|
||||
- meta_model_name
|
||||
- name
|
||||
- pk
|
||||
@ -48548,6 +48610,10 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used when authorizing this provider.
|
||||
invalidation_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used ending the session from a provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
@ -48555,6 +48621,7 @@ components:
|
||||
format: uuid
|
||||
required:
|
||||
- authorization_flow
|
||||
- invalidation_flow
|
||||
- name
|
||||
ProviderTypeEnum:
|
||||
enum:
|
||||
@ -48695,6 +48762,10 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used when authorizing this provider.
|
||||
invalidation_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used ending the session from a provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
@ -48811,6 +48882,7 @@ components:
|
||||
- client_id
|
||||
- component
|
||||
- external_host
|
||||
- invalidation_flow
|
||||
- meta_model_name
|
||||
- name
|
||||
- outpost_set
|
||||
@ -48835,6 +48907,10 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used when authorizing this provider.
|
||||
invalidation_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used ending the session from a provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
@ -48905,6 +48981,7 @@ components:
|
||||
required:
|
||||
- authorization_flow
|
||||
- external_host
|
||||
- invalidation_flow
|
||||
- name
|
||||
RACPropertyMapping:
|
||||
type: object
|
||||
@ -48998,6 +49075,10 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used when authorizing this provider.
|
||||
invalidation_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used ending the session from a provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
@ -49055,6 +49136,7 @@ components:
|
||||
- assigned_backchannel_application_slug
|
||||
- authorization_flow
|
||||
- component
|
||||
- invalidation_flow
|
||||
- meta_model_name
|
||||
- name
|
||||
- outpost_set
|
||||
@ -49078,6 +49160,10 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used when authorizing this provider.
|
||||
invalidation_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used ending the session from a provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
@ -49094,6 +49180,7 @@ components:
|
||||
description: When set to true, connection tokens will be deleted upon disconnect.
|
||||
required:
|
||||
- authorization_flow
|
||||
- invalidation_flow
|
||||
- name
|
||||
RadiusCheckAccess:
|
||||
type: object
|
||||
@ -49159,6 +49246,10 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used when authorizing this provider.
|
||||
invalidation_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used ending the session from a provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
@ -49223,6 +49314,7 @@ components:
|
||||
- assigned_backchannel_application_slug
|
||||
- authorization_flow
|
||||
- component
|
||||
- invalidation_flow
|
||||
- meta_model_name
|
||||
- name
|
||||
- outpost_set
|
||||
@ -49313,6 +49405,10 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used when authorizing this provider.
|
||||
invalidation_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used ending the session from a provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
@ -49337,6 +49433,7 @@ components:
|
||||
if it contains a semicolon.
|
||||
required:
|
||||
- authorization_flow
|
||||
- invalidation_flow
|
||||
- name
|
||||
RedirectChallenge:
|
||||
type: object
|
||||
@ -49647,6 +49744,10 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used when authorizing this provider.
|
||||
invalidation_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used ending the session from a provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
@ -49785,6 +49886,7 @@ components:
|
||||
- assigned_backchannel_application_slug
|
||||
- authorization_flow
|
||||
- component
|
||||
- invalidation_flow
|
||||
- meta_model_name
|
||||
- name
|
||||
- pk
|
||||
@ -49806,12 +49908,16 @@ components:
|
||||
authorization_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
invalidation_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
required:
|
||||
- authorization_flow
|
||||
- file
|
||||
- invalidation_flow
|
||||
- name
|
||||
SAMLProviderRequest:
|
||||
type: object
|
||||
@ -49830,6 +49936,10 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used when authorizing this provider.
|
||||
invalidation_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used ending the session from a provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
@ -49912,6 +50022,7 @@ components:
|
||||
required:
|
||||
- acs_url
|
||||
- authorization_flow
|
||||
- invalidation_flow
|
||||
- name
|
||||
SAMLSource:
|
||||
type: object
|
||||
@ -50952,6 +51063,37 @@ components:
|
||||
required:
|
||||
- healthy
|
||||
- version
|
||||
SessionEndChallenge:
|
||||
type: object
|
||||
description: Challenge for ending a session
|
||||
properties:
|
||||
flow_info:
|
||||
$ref: '#/components/schemas/ContextualFlowInfo'
|
||||
component:
|
||||
type: string
|
||||
default: ak-stage-session-end
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ErrorDetail'
|
||||
pending_user:
|
||||
type: string
|
||||
pending_user_avatar:
|
||||
type: string
|
||||
application_name:
|
||||
type: string
|
||||
application_launch_url:
|
||||
type: string
|
||||
invalidation_flow_url:
|
||||
type: string
|
||||
brand_name:
|
||||
type: string
|
||||
required:
|
||||
- brand_name
|
||||
- pending_user
|
||||
- pending_user_avatar
|
||||
SessionUser:
|
||||
type: object
|
||||
description: |-
|
||||
|
@ -181,9 +181,15 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
||||
@apply_blueprint(
|
||||
"default/flow-default-authentication-flow.yaml",
|
||||
"default/flow-default-invalidation-flow.yaml",
|
||||
"default/default-brand.yaml",
|
||||
)
|
||||
@apply_blueprint(
|
||||
"default/flow-default-provider-authorization-implicit-consent.yaml",
|
||||
"default/flow-default-provider-invalidation.yaml",
|
||||
)
|
||||
@apply_blueprint(
|
||||
"system/providers-oauth2.yaml",
|
||||
)
|
||||
@apply_blueprint("default/flow-default-provider-authorization-implicit-consent.yaml")
|
||||
@apply_blueprint("system/providers-oauth2.yaml")
|
||||
@reconcile_app("authentik_crypto")
|
||||
def test_authorization_logout(self):
|
||||
"""test OpenID Provider flow with logout"""
|
||||
@ -192,6 +198,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
)
|
||||
invalidation_flow = Flow.objects.get(slug="default-provider-invalidation-flow")
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
client_type=ClientTypes.CONFIDENTIAL,
|
||||
@ -200,6 +207,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
||||
signing_key=create_test_cert(),
|
||||
redirect_uris="http://localhost:3000/login/generic_oauth",
|
||||
authorization_flow=authorization_flow,
|
||||
invalidation_flow=invalidation_flow,
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
ScopeMapping.objects.filter(
|
||||
@ -242,11 +250,13 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
||||
self.driver.get("http://localhost:3000/logout")
|
||||
self.wait_for_url(
|
||||
self.url(
|
||||
"authentik_core:if-session-end",
|
||||
application_slug=self.app_slug,
|
||||
"authentik_core:if-flow",
|
||||
flow_slug=invalidation_flow.slug,
|
||||
)
|
||||
)
|
||||
self.driver.find_element(By.ID, "logout").click()
|
||||
flow_executor = self.get_shadow_root("ak-flow-executor")
|
||||
session_end_stage = self.get_shadow_root("ak-stage-session-end", flow_executor)
|
||||
session_end_stage.find_element(By.ID, "logout").click()
|
||||
|
||||
@retry()
|
||||
@apply_blueprint(
|
||||
|
@ -65,6 +65,7 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
)
|
||||
@apply_blueprint(
|
||||
"default/flow-default-provider-authorization-implicit-consent.yaml",
|
||||
"default/flow-default-provider-invalidation.yaml",
|
||||
)
|
||||
@apply_blueprint(
|
||||
"system/providers-oauth2.yaml",
|
||||
@ -82,6 +83,7 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
authorization_flow=Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
),
|
||||
invalidation_flow=Flow.objects.get(slug="default-provider-invalidation-flow"),
|
||||
internal_host=f"http://{self.host}",
|
||||
external_host="http://localhost:9000",
|
||||
)
|
||||
@ -120,8 +122,10 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
|
||||
self.driver.get("http://localhost:9000/outpost.goauthentik.io/sign_out")
|
||||
sleep(2)
|
||||
full_body_text = self.driver.find_element(By.CSS_SELECTOR, ".pf-c-title.pf-m-3xl").text
|
||||
self.assertIn("You've logged out of", full_body_text)
|
||||
flow_executor = self.get_shadow_root("ak-flow-executor")
|
||||
session_end_stage = self.get_shadow_root("ak-stage-session-end", flow_executor)
|
||||
title = session_end_stage.find_element(By.CSS_SELECTOR, ".pf-c-title.pf-m-3xl").text
|
||||
self.assertIn("You've logged out of", title)
|
||||
|
||||
@retry()
|
||||
@apply_blueprint(
|
||||
@ -130,6 +134,7 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
)
|
||||
@apply_blueprint(
|
||||
"default/flow-default-provider-authorization-implicit-consent.yaml",
|
||||
"default/flow-default-provider-invalidation.yaml",
|
||||
)
|
||||
@apply_blueprint(
|
||||
"system/providers-oauth2.yaml",
|
||||
@ -149,6 +154,7 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
authorization_flow=Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
),
|
||||
invalidation_flow=Flow.objects.get(slug="default-provider-invalidation-flow"),
|
||||
internal_host=f"http://{self.host}",
|
||||
external_host="http://localhost:9000",
|
||||
basic_auth_enabled=True,
|
||||
@ -191,8 +197,10 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
|
||||
self.driver.get("http://localhost:9000/outpost.goauthentik.io/sign_out")
|
||||
sleep(2)
|
||||
full_body_text = self.driver.find_element(By.CSS_SELECTOR, ".pf-c-title.pf-m-3xl").text
|
||||
self.assertIn("You've logged out of", full_body_text)
|
||||
flow_executor = self.get_shadow_root("ak-flow-executor")
|
||||
session_end_stage = self.get_shadow_root("ak-stage-session-end", flow_executor)
|
||||
title = session_end_stage.find_element(By.CSS_SELECTOR, ".pf-c-title.pf-m-3xl").text
|
||||
self.assertIn("You've logged out of", title)
|
||||
|
||||
|
||||
# TODO: Fix flaky test
|
||||
|
@ -414,6 +414,7 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
)
|
||||
@apply_blueprint(
|
||||
"default/flow-default-provider-authorization-implicit-consent.yaml",
|
||||
"default/flow-default-provider-invalidation.yaml",
|
||||
)
|
||||
@apply_blueprint(
|
||||
"system/providers-saml.yaml",
|
||||
@ -425,6 +426,7 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
)
|
||||
invalidation_flow = Flow.objects.get(slug="default-provider-invalidation-flow")
|
||||
provider: SAMLProvider = SAMLProvider.objects.create(
|
||||
name="saml-test",
|
||||
acs_url="http://localhost:9009/saml/acs",
|
||||
@ -432,11 +434,12 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
issuer="authentik-e2e",
|
||||
sp_binding=SAMLBindings.POST,
|
||||
authorization_flow=authorization_flow,
|
||||
invalidation_flow=invalidation_flow,
|
||||
signing_kp=create_test_cert(),
|
||||
)
|
||||
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
|
||||
provider.save()
|
||||
app = Application.objects.create(
|
||||
Application.objects.create(
|
||||
name="SAML",
|
||||
slug="authentik-saml",
|
||||
provider=provider,
|
||||
@ -447,9 +450,11 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
self.wait_for_url("http://localhost:9009/")
|
||||
|
||||
self.driver.get("http://localhost:9009/saml/logout")
|
||||
self.wait_for_url(
|
||||
self.url(
|
||||
"authentik_core:if-session-end",
|
||||
application_slug=app.slug,
|
||||
)
|
||||
should_url = self.url(
|
||||
"authentik_core:if-flow",
|
||||
flow_slug=invalidation_flow.slug,
|
||||
)
|
||||
self.wait.until(
|
||||
lambda driver: driver.current_url.startswith(should_url),
|
||||
f"URL {self.driver.current_url} doesn't match expected URL {should_url}",
|
||||
)
|
||||
|
@ -101,6 +101,21 @@ export class ApplicationWizardAuthenticationByOauth extends BaseProviderPanel {
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
name="invalidationFlow"
|
||||
label=${msg("Authorization flow")}
|
||||
.errorMessages=${errors?.invalidationFlow ?? []}
|
||||
?required=${true}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${provider?.invalidationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
|
@ -121,10 +121,9 @@ export class AkTypeProxyApplicationWizardPage extends BaseProviderPanel {
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authorization flow")}
|
||||
?required=${true}
|
||||
required
|
||||
name="authorizationFlow"
|
||||
.errorMessages=${errors?.authorizationFlow ?? []}
|
||||
>
|
||||
@ -137,6 +136,21 @@ export class AkTypeProxyApplicationWizardPage extends BaseProviderPanel {
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
name="invalidationFlow"
|
||||
label=${msg("Authorization flow")}
|
||||
.errorMessages=${errors?.invalidationFlow ?? []}
|
||||
?required=${true}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${this.instance?.invalidationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
${this.renderProxyMode()}
|
||||
|
||||
|
@ -97,7 +97,6 @@ export class ApplicationWizardProviderSamlConfiguration extends BaseProviderPane
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${provider?.authenticationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
@ -105,7 +104,6 @@ export class ApplicationWizardProviderSamlConfiguration extends BaseProviderPane
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authorization flow")}
|
||||
?required=${true}
|
||||
@ -121,6 +119,21 @@ export class ApplicationWizardProviderSamlConfiguration extends BaseProviderPane
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
name="invalidationFlow"
|
||||
label=${msg("Authorization flow")}
|
||||
.errorMessages=${errors?.invalidationFlow ?? []}
|
||||
?required=${true}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${provider?.invalidationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
|
@ -57,19 +57,6 @@ export class LDAPProviderFormPage extends WithBrandConfig(BaseProviderForm<LDAPP
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Bind flow")}
|
||||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-branded-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.authorizationFlow}
|
||||
.brandFlow=${this.brand?.flowAuthentication}
|
||||
required
|
||||
></ak-branded-flow-search>
|
||||
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Bind mode")} name="bindMode">
|
||||
<ak-radio
|
||||
.options=${[
|
||||
@ -143,6 +130,35 @@ export class LDAPProviderFormPage extends WithBrandConfig(BaseProviderForm<LDAPP
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Flow settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Bind flow")}
|
||||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-branded-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.authorizationFlow}
|
||||
.brandFlow=${this.brand?.flowAuthentication}
|
||||
required
|
||||
></ak-branded-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used for users to authenticate.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Unbind flow")} name="invalidationFlow">
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${this.instance?.invalidationFlow}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used for unbinding users.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
|
@ -155,34 +155,7 @@ export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
|
||||
required
|
||||
></ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
name="authenticationFlow"
|
||||
label=${msg("Authentication flow")}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${provider?.authenticationFlow}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when a user access this provider and is not authenticated.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
name="authorizationFlow"
|
||||
label=${msg("Authorization flow")}
|
||||
required
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${provider?.authorizationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-group .expanded=${true}>
|
||||
<ak-form-group expanded>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-radio-input
|
||||
@ -235,6 +208,54 @@ export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Flow settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
name="authenticationFlow"
|
||||
label=${msg("Authentication flow")}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${provider?.authenticationFlow}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Flow used when a user access this provider and is not authenticated.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
name="authorizationFlow"
|
||||
label=${msg("Authorization flow")}
|
||||
?required=${true}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${provider?.authorizationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Invalidation flow")}
|
||||
name="invalidationFlow"
|
||||
required
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${provider?.invalidationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
|
@ -249,7 +249,8 @@ export class ProxyProviderFormPage extends BaseProviderForm<ProxyProvider> {
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
||||
return html`
|
||||
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.name)}"
|
||||
@ -257,34 +258,6 @@ export class ProxyProviderFormPage extends BaseProviderForm<ProxyProvider> {
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authentication flow")}
|
||||
?required=${false}
|
||||
name="authenticationFlow"
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.authenticationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when a user access this provider and is not authenticated.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authorization flow")}
|
||||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${this.instance?.authorizationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<div class="pf-c-card pf-m-selectable pf-m-selected">
|
||||
<div class="pf-c-card__body">${this.renderModeSelector()}</div>
|
||||
@ -418,7 +391,57 @@ ${this.instance?.skipPathRegex}</textarea
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>`;
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Flow settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authentication flow")}
|
||||
?required=${false}
|
||||
name="authenticationFlow"
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.authenticationFlow}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Flow used when a user access this provider and is not authenticated.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authorization flow")}
|
||||
required
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${this.instance?.authorizationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Invalidation flow")}
|
||||
name="invalidationFlow"
|
||||
required
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${this.instance?.invalidationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,34 +89,6 @@ export class SAMLProviderFormPage extends BaseProviderForm<SAMLProvider> {
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authentication flow")}
|
||||
?required=${false}
|
||||
name="authenticationFlow"
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.authenticationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when a user access this provider and is not authenticated.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authorization flow")}
|
||||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${this.instance?.authorizationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
@ -182,6 +154,55 @@ export class SAMLProviderFormPage extends BaseProviderForm<SAMLProvider> {
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Flow settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authentication flow")}
|
||||
?required=${false}
|
||||
name="authenticationFlow"
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.authenticationFlow}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Flow used when a user access this provider and is not authenticated.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authorization flow")}
|
||||
required
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${this.instance?.authorizationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Invalidation flow")}
|
||||
name="invalidationFlow"
|
||||
required
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${this.instance?.invalidationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
|
@ -26,6 +26,7 @@ export class SAMLProviderImportForm extends Form<SAMLProvider> {
|
||||
file: file,
|
||||
name: data.name,
|
||||
authorizationFlow: data.authorizationFlow || "",
|
||||
invalidationFlow: data.invalidationFlow || "",
|
||||
});
|
||||
}
|
||||
|
||||
@ -46,6 +47,19 @@ export class SAMLProviderImportForm extends Form<SAMLProvider> {
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Invalidation flow")}
|
||||
?required=${true}
|
||||
name="invalidationFlow"
|
||||
>
|
||||
<ak-flow-search-no-default
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
required
|
||||
></ak-flow-search-no-default>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal label=${msg("Metadata")} name="metadata">
|
||||
<input type="file" value="" class="pf-c-form-control" />
|
||||
|
@ -408,6 +408,12 @@ export class FlowExecutor extends Interface implements StageHost {
|
||||
.host=${this as StageHost}
|
||||
.challenge=${this.challenge}
|
||||
></ak-flow-provider-oauth2-code-finish>`;
|
||||
case "ak-stage-session-end":
|
||||
await import("@goauthentik/flow/providers/SessionEnd");
|
||||
return html`<ak-stage-session-end
|
||||
.host=${this as StageHost}
|
||||
.challenge=${this.challenge}
|
||||
></ak-stage-session-end>`;
|
||||
// Internal stages
|
||||
case "ak-stage-flow-error":
|
||||
return html`<ak-stage-flow-error
|
||||
|
78
web/src/flow/providers/SessionEnd.ts
Normal file
78
web/src/flow/providers/SessionEnd.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import "@goauthentik/flow/FormStatic";
|
||||
import { BaseStage } from "@goauthentik/flow/stages/base";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, html, nothing } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import { SessionEndChallenge } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-stage-session-end")
|
||||
export class SessionEnd extends BaseStage<SessionEndChallenge, unknown> {
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton];
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.challenge) {
|
||||
return html`<ak-empty-state ?loading="${true}" header=${msg("Loading")}>
|
||||
</ak-empty-state>`;
|
||||
}
|
||||
return html`<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form">
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${msg("Not you?")}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
<p>
|
||||
${msg(
|
||||
str`You've logged out of ${this.challenge.applicationName}. You can go back to the overview to launch another application, or log out of your authentik account.`,
|
||||
)}
|
||||
</p>
|
||||
<a href="/" class="pf-c-button pf-m-primary"> ${msg("Go back to overview")} </a>
|
||||
${this.challenge.invalidationFlowUrl
|
||||
? html`
|
||||
<a
|
||||
href="${this.challenge.invalidationFlowUrl}"
|
||||
class="pf-c-button pf-m-secondary"
|
||||
id="logout"
|
||||
>
|
||||
${msg(str`Log out of ${this.challenge.brandName}`)}
|
||||
</a>
|
||||
`
|
||||
: nothing}
|
||||
${this.challenge.applicationLaunchUrl && this.challenge.applicationName
|
||||
? html`
|
||||
<a
|
||||
href="${this.challenge.applicationLaunchUrl}"
|
||||
class="pf-c-button pf-m-secondary"
|
||||
>
|
||||
${msg(str`Log back into ${this.challenge.applicationName}`)}
|
||||
</a>
|
||||
`
|
||||
: nothing}
|
||||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links"></ul>
|
||||
</footer>`;
|
||||
}
|
||||
}
|
@ -66,17 +66,17 @@ When creating or editing a flow in the UI of the Admin interface, you can set th
|
||||
|
||||
- **Authentication**: this option designates a flow to be used for authentication. The authentication flow should always contain a [**User Login**](../stages/user_login/index.md) stage, which attaches the staged user to the current session.
|
||||
|
||||
- **Authorization**: designates a flow to be used for authorization. The authorization flow `default-provider-authorization-explicit-consent` should always contain a consent stage.
|
||||
- **Authorization**: designates a flow to be used for authorization of an application. Can be used to add additional verification steps before the user is allowed to access an application.
|
||||
|
||||
- **Invalidation**: designates a flow to be used to invalidate a session. This flow should always contain a [**User Logout**](../stages/user_logout.md) stage, which resets the current session.
|
||||
- **Invalidation**: designates a flow to be used to invalidate a session. Both used to invalidate a session from authentik and when the session of an application ends. When used as a global invalidation flow should contain a [**User Logout**](../stages/user_logout.md) stage.
|
||||
|
||||
- **Enrollment**: designates a flow for enrollment. This flow can contain any amount of verification stages, such as [**email**](../stages/email/index.mdx) or [**captcha**](../stages/captcha/index.md). At the end, to create the user, you can use the [**user_write**](../stages/user_write.md) stage, which either updates the currently staged user, or if none exists, creates a new one.
|
||||
- **Enrollment**: designates a flow for enrollment. This flow can contain any amount of verification stages, such as [**Email**](../stages/email/index.mdx) or [**Captcha**](../stages/captcha/index.md). At the end, to create the user, you can use the [**User Write**](../stages/user_write.md) stage, which either updates the currently staged user, or if none exists, creates a new one.
|
||||
|
||||
- **Unenrollment**: designates a flow for unenrollment. This flow can contain any amount of verification stages, such as [**email**](../stages/email/index.mdx) or [**captcha**](../stages/captcha/index.md). As a final stage, to delete the account, use the [**user_delete**](../stages/user_delete.md) stage.
|
||||
- **Unenrollment**: designates a flow for unenrollment. This flow can contain any amount of verification stages, such as [**email**](../stages/email/index.mdx) or [**Captcha**](../stages/captcha/index.md). As a final stage, to delete the account, use the [**user_delete**](../stages/user_delete.md) stage.
|
||||
|
||||
- **Recovery**: designates a flow for recovery. This flow normally contains an [**identification**](../stages/identification/index.md) stage to find the user. It can also contain any amount of verification stages, such as [**email**](../stages/email/index.mdx) or [**captcha**](../stages/captcha/index.md). Afterwards, use the [**prompt**](../stages/prompt/index.md) stage to ask the user for a new password and the [**user_write**](../stages/user_write.md) stage to update the password.
|
||||
- **Recovery**: designates a flow for recovery. This flow normally contains an [**Identification**](../stages/identification/index.md) stage to find the user. It can also contain any amount of verification stages, such as [**Email**](../stages/email/index.mdx) or [**captcha**](../stages/captcha/index.md). Afterwards, use the [**Prompt**](../stages/prompt/index.md) stage to ask the user for a new password and the [**User Write**](../stages/user_write.md) stage to update the password.
|
||||
|
||||
- **Stage configuration**: designates a flow for general setup. This designation doesn't have any constraints in what you can do. For example, by default this designation is used to configure Factors, like change a password and setup TOTP.
|
||||
- **Stage configuration**: designates a flow for general setup. This designation doesn't have any constraints in what you can do. For example, by default this designation is used to configure authenticators, like change a password and setup TOTP.
|
||||
|
||||
**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.
|
||||
|
||||
|
54
website/docs/releases/2024/v2024.10.md
Normal file
54
website/docs/releases/2024/v2024.10.md
Normal file
@ -0,0 +1,54 @@
|
||||
---
|
||||
title: Release 2024.10
|
||||
slug: "/releases/2024.10"
|
||||
---
|
||||
|
||||
:::::note
|
||||
2024.10 has not been released yet! We're publishing these release notes as a preview of what's to come, and for our awesome beta testers trying out release candidates.
|
||||
|
||||
To try out the release candidate, replace your Docker image tag with the latest release candidate number, such as 2024.10.0-rc1. You can find the latest one in [the latest releases on GitHub](https://github.com/goauthentik/authentik/releases). If you don't find any, it means we haven't released one yet.
|
||||
:::::
|
||||
|
||||
<!-- ## Breaking changes -->
|
||||
|
||||
## New features
|
||||
|
||||
- **Invalidation flows for providers**
|
||||
|
||||
The sign-out experience when the session in an application ends can be configured now. Previously where this was always a static page, any flow can be used now. This can be used for additional validation, or redirecting the user to a custom URL.
|
||||
|
||||
## Upgrading
|
||||
|
||||
This release does not introduce any new requirements. You can follow the upgrade instructions below; for more detailed information about upgrading authentik, refer to our [Upgrade documentation](../../install-config/upgrade.mdx).
|
||||
|
||||
:::warning
|
||||
When you upgrade, be aware that the version of the authentik instance and of any outposts must be the same. We recommended that you always upgrade any outposts at the same time you upgrade your authentik instance.
|
||||
:::
|
||||
|
||||
### Docker Compose
|
||||
|
||||
To upgrade, download the new docker-compose file and update the Docker stack with the new version, using these commands:
|
||||
|
||||
```shell
|
||||
wget -O docker-compose.yml https://goauthentik.io/version/2024.10/docker-compose.yml
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
The `-O` flag retains the downloaded file's name, overwriting any existing local file with the same name.
|
||||
|
||||
### Kubernetes
|
||||
|
||||
Upgrade the Helm Chart to the new version, using the following commands:
|
||||
|
||||
```shell
|
||||
helm repo update
|
||||
helm upgrade authentik authentik/authentik -f values.yaml --version ^2024.10
|
||||
```
|
||||
|
||||
## Minor changes/fixes
|
||||
|
||||
<!-- _Insert the output of `make gen-changelog` here_ -->
|
||||
|
||||
## API Changes
|
||||
|
||||
<!-- _Insert output of `make gen-diff` here_ -->
|
Reference in New Issue
Block a user