diff --git a/authentik/core/api/providers.py b/authentik/core/api/providers.py
index a5095dcde..1251d7415 100644
--- a/authentik/core/api/providers.py
+++ b/authentik/core/api/providers.py
@@ -42,6 +42,7 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
"name",
"authentication_flow",
"authorization_flow",
+ "invalidation_flow",
"property_mappings",
"component",
"assigned_application_slug",
diff --git a/authentik/core/migrations/0028_provider_invalidation_flow.py b/authentik/core/migrations/0028_provider_invalidation_flow.py
new file mode 100644
index 000000000..9fedd6c71
--- /dev/null
+++ b/authentik/core/migrations/0028_provider_invalidation_flow.py
@@ -0,0 +1,26 @@
+# Generated by Django 4.1.7 on 2023-03-22 22:26
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("authentik_flows", "0025_alter_flowstagebinding_evaluate_on_plan_and_more"),
+ ("authentik_core", "0027_alter_user_uuid"),
+ ]
+
+ 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",
+ ),
+ ),
+ ]
diff --git a/authentik/core/models.py b/authentik/core/models.py
index 7d11af3d6..06ab7d1cd 100644
--- a/authentik/core/models.py
+++ b/authentik/core/models.py
@@ -299,11 +299,21 @@ class Provider(SerializerModel):
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)
diff --git a/authentik/flows/challenge.py b/authentik/flows/challenge.py
index eb968ce79..517bdbb27 100644
--- a/authentik/flows/challenge.py
+++ b/authentik/flows/challenge.py
@@ -125,6 +125,12 @@ class AccessDeniedChallenge(WithUserInfoChallenge):
component = CharField(default="ak-stage-access-denied")
+class SessionEndChallenge(WithUserInfoChallenge):
+ """Challenge for ending a session"""
+
+ component = CharField(default="ak-stage-session-end")
+
+
class PermissionDict(TypedDict):
"""Consent Permission"""
diff --git a/authentik/flows/models.py b/authentik/flows/models.py
index 67f7f0a9c..f87f7aa1f 100644
--- a/authentik/flows/models.py
+++ b/authentik/flows/models.py
@@ -105,7 +105,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
diff --git a/authentik/providers/oauth2/urls.py b/authentik/providers/oauth2/urls.py
index f310fa1bc..548cfee35 100644
--- a/authentik/providers/oauth2/urls.py
+++ b/authentik/providers/oauth2/urls.py
@@ -11,6 +11,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
@@ -43,7 +44,7 @@ urlpatterns = [
),
path(
"/end-session/",
- RedirectView.as_view(pattern_name="authentik_core:if-session-end", query_string=True),
+ EndSessionView.as_view(),
name="end-session",
),
path("/jwks/", JWKSView.as_view(), name="jwks"),
diff --git a/authentik/providers/oauth2/views/end_session.py b/authentik/providers/oauth2/views/end_session.py
new file mode 100644
index 000000000..bf5b57e01
--- /dev/null
+++ b/authentik/providers/oauth2/views/end_session.py
@@ -0,0 +1,39 @@
+"""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.challenge import SessionEndChallenge
+from authentik.flows.models import in_memory_stage
+from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
+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"""
+
+ 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 or not self.provider.invalidation_flow:
+ raise Http404
+
+ def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
+ """Dispatch the flow planner for the invalidation flow"""
+ planner = FlowPlanner(self.provider.invalidation_flow)
+ planner.allow_empty_flows = True
+ plan = planner.plan(
+ request,
+ {
+ PLAN_CONTEXT_APPLICATION: self.application,
+ },
+ )
+ plan.insert_stage(in_memory_stage(SessionEndChallenge))
+ request.session[SESSION_KEY_PLAN] = plan
+ return redirect_with_qs(
+ "authentik_core:if-flow",
+ self.request.GET,
+ flow_slug=self.provider.invalidation_flow.slug,
+ )
diff --git a/blueprints/default/flow-default-provider-invalidation.yaml b/blueprints/default/flow-default-provider-invalidation.yaml
new file mode 100644
index 000000000..4d29b8a42
--- /dev/null
+++ b/blueprints/default/flow-default-provider-invalidation.yaml
@@ -0,0 +1,13 @@
+version: 1
+metadata:
+ name: Default - Provider invalidation flow
+entries:
+- attrs:
+ designation: invalidation
+ name: Logout
+ title: You've logged out of %(app)s.
+ authentication: none
+ identifiers:
+ slug: default-provider-invalidation-flow
+ model: authentik_flows.flow
+ id: flow
diff --git a/schema.yml b/schema.yml
index 694eca69c..cb3b9120d 100644
--- a/schema.yml
+++ b/schema.yml
@@ -16394,6 +16394,11 @@ paths:
name: is_backchannel
schema:
type: boolean
+ - in: query
+ name: invalidation_flow
+ schema:
+ type: string
+ format: uuid
- in: query
name: issuer
schema:
@@ -29557,6 +29562,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:
@@ -29583,6 +29589,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:
@@ -32592,6 +32599,11 @@ components:
type: string
format: uuid
description: Flow used when authorizing this provider.
+ invalidation_flow:
+ type: string
+ format: uuid
+ nullable: true
+ description: Flow used ending the session from a provider.
property_mappings:
type: array
items:
@@ -32705,6 +32717,11 @@ components:
type: string
format: uuid
description: Flow used when authorizing this provider.
+ invalidation_flow:
+ type: string
+ format: uuid
+ nullable: true
+ description: Flow used ending the session from a provider.
property_mappings:
type: array
items:
@@ -33627,6 +33644,11 @@ components:
type: string
format: uuid
description: Flow used when authorizing this provider.
+ invalidation_flow:
+ type: string
+ format: uuid
+ nullable: true
+ description: Flow used ending the session from a provider.
property_mappings:
type: array
items:
@@ -33760,6 +33782,11 @@ components:
type: string
format: uuid
description: Flow used when authorizing this provider.
+ invalidation_flow:
+ type: string
+ format: uuid
+ nullable: true
+ description: Flow used ending the session from a provider.
property_mappings:
type: array
items:
@@ -36728,6 +36755,11 @@ components:
type: string
format: uuid
description: Flow used when authorizing this provider.
+ invalidation_flow:
+ type: string
+ format: uuid
+ nullable: true
+ description: Flow used ending the session from a provider.
property_mappings:
type: array
items:
@@ -36986,6 +37018,11 @@ components:
type: string
format: uuid
description: Flow used when authorizing this provider.
+ invalidation_flow:
+ type: string
+ format: uuid
+ nullable: true
+ description: Flow used ending the session from a provider.
property_mappings:
type: array
items:
@@ -37478,6 +37515,11 @@ components:
type: string
format: uuid
description: Flow used when authorizing this provider.
+ invalidation_flow:
+ type: string
+ format: uuid
+ nullable: true
+ description: Flow used ending the session from a provider.
property_mappings:
type: array
items:
@@ -37566,6 +37608,11 @@ components:
type: string
format: uuid
description: Flow used when authorizing this provider.
+ invalidation_flow:
+ type: string
+ format: uuid
+ nullable: true
+ description: Flow used ending the session from a provider.
property_mappings:
type: array
items:
@@ -37657,6 +37704,11 @@ components:
type: string
format: uuid
description: Flow used when authorizing this provider.
+ invalidation_flow:
+ type: string
+ format: uuid
+ nullable: true
+ description: Flow used ending the session from a provider.
property_mappings:
type: array
items:
@@ -38984,6 +39036,11 @@ components:
type: string
format: uuid
description: Flow used when authorizing this provider.
+ invalidation_flow:
+ type: string
+ format: uuid
+ nullable: true
+ description: Flow used ending the session from a provider.
property_mappings:
type: array
items:
@@ -39074,6 +39131,11 @@ components:
type: string
format: uuid
description: Flow used when authorizing this provider.
+ invalidation_flow:
+ type: string
+ format: uuid
+ nullable: true
+ description: Flow used ending the session from a provider.
property_mappings:
type: array
items:
@@ -39242,6 +39304,11 @@ components:
type: string
format: uuid
description: Flow used when authorizing this provider.
+ invalidation_flow:
+ type: string
+ format: uuid
+ nullable: true
+ description: Flow used ending the session from a provider.
property_mappings:
type: array
items:
@@ -39386,6 +39453,11 @@ components:
type: string
format: uuid
description: Flow used when authorizing this provider.
+ invalidation_flow:
+ type: string
+ format: uuid
+ nullable: true
+ description: Flow used ending the session from a provider.
property_mappings:
type: array
items:
@@ -39515,6 +39587,11 @@ components:
type: string
format: uuid
description: Flow used when authorizing this provider.
+ invalidation_flow:
+ type: string
+ format: uuid
+ nullable: true
+ description: Flow used ending the session from a provider.
property_mappings:
type: array
items:
@@ -39602,6 +39679,11 @@ components:
type: string
format: uuid
description: Flow used when authorizing this provider.
+ invalidation_flow:
+ type: string
+ format: uuid
+ nullable: true
+ description: Flow used ending the session from a provider.
property_mappings:
type: array
items:
@@ -39944,6 +40026,11 @@ components:
type: string
format: uuid
description: Flow used when authorizing this provider.
+ invalidation_flow:
+ type: string
+ format: uuid
+ nullable: true
+ description: Flow used ending the session from a provider.
property_mappings:
type: array
items:
@@ -40118,6 +40205,11 @@ components:
type: string
format: uuid
description: Flow used when authorizing this provider.
+ invalidation_flow:
+ type: string
+ format: uuid
+ nullable: true
+ description: Flow used ending the session from a provider.
property_mappings:
type: array
items:
@@ -40814,6 +40906,31 @@ components:
required:
- healthy
- version
+ SessionEndChallenge:
+ type: object
+ description: Challenge for ending a session
+ properties:
+ type:
+ $ref: '#/components/schemas/ChallengeChoices'
+ 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
+ required:
+ - pending_user
+ - pending_user_avatar
+ - type
SessionUser:
type: object
description: |-
diff --git a/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts b/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts
index 000df8cad..105f05d5f 100644
--- a/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts
+++ b/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts
@@ -193,6 +193,41 @@ export class OAuth2ProviderFormPage extends BaseProviderForm {
${msg("Flow used when authorizing this provider.")}
+
+ => {
+ const args: FlowsInstancesListRequest = {
+ ordering: "slug",
+ designation: FlowsInstancesListDesignationEnum.Invalidation,
+ };
+ if (query !== undefined) {
+ args.search = query;
+ }
+ const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList(args);
+ return flows.results;
+ }}
+ .renderElement=${(flow: Flow): string => {
+ return RenderFlowOption(flow);
+ }}
+ .renderDescription=${(flow: Flow): TemplateResult => {
+ return html`${flow.name}`;
+ }}
+ .value=${(flow: Flow | undefined): string | undefined => {
+ return flow?.pk;
+ }}
+ .selected=${(flow: Flow): boolean => {
+ return flow.pk === this.instance?.invalidationFlow;
+ }}
+ >
+
+
+ ${t`Flow used when authorizing this provider.`}
+
+
${msg("Protocol settings")}