From 6e24856d4593e06bfd1c2a7b06b471b34013c4a4 Mon Sep 17 00:00:00 2001 From: Jens L Date: Sat, 19 Dec 2020 16:42:39 +0100 Subject: [PATCH] flows: fix redirect when un-authenticated user uses external authentication (#416) * flows: add PLAN_CONTEXT_REDIRECT so final redirect can be set from within flow * sources/*: use PLAN_CONTEXT_REDIRECT * flows: fallback when flow plan is empty --- authentik/flows/planner.py | 1 + authentik/flows/views.py | 17 +++++++++++++---- authentik/sources/oauth/views/callback.py | 9 ++++++++- authentik/sources/saml/processors/response.py | 12 +++++++++++- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/authentik/flows/planner.py b/authentik/flows/planner.py index bcc7bd9d2..17246f80d 100644 --- a/authentik/flows/planner.py +++ b/authentik/flows/planner.py @@ -19,6 +19,7 @@ LOGGER = get_logger() PLAN_CONTEXT_PENDING_USER = "pending_user" PLAN_CONTEXT_SSO = "is_sso" +PLAN_CONTEXT_REDIRECT = "redirect" PLAN_CONTEXT_APPLICATION = "application" diff --git a/authentik/flows/views.py b/authentik/flows/views.py index 597dad7bd..66fa83ae2 100644 --- a/authentik/flows/views.py +++ b/authentik/flows/views.py @@ -21,7 +21,12 @@ from authentik.audit.models import cleanse_dict from authentik.core.models import USER_ATTRIBUTE_DEBUG from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException from authentik.flows.models import ConfigurableStage, Flow, FlowDesignation, Stage -from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan, FlowPlanner +from authentik.flows.planner import ( + PLAN_CONTEXT_PENDING_USER, + PLAN_CONTEXT_REDIRECT, + FlowPlan, + FlowPlanner, +) from authentik.lib.utils.reflection import class_to_path from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs from authentik.policies.http import AccessDeniedResponse @@ -145,9 +150,13 @@ class FlowExecutorView(View): """User Successfully passed all stages""" # Since this is wrapped by the ExecutorShell, the next argument is saved in the session # extract the next param before cancel as that cleans it - next_param = self.request.session.get(SESSION_KEY_GET, {}).get( - NEXT_ARG_NAME, "authentik_core:shell" - ) + next_param = None + if self.plan: + next_param = self.plan.context.get(PLAN_CONTEXT_REDIRECT) + if not next_param: + next_param = self.request.session.get(SESSION_KEY_GET, {}).get( + NEXT_ARG_NAME, "authentik_core:shell" + ) self.cancel() return to_stage_response(self.request, redirect_with_qs(next_param)) diff --git a/authentik/sources/oauth/views/callback.py b/authentik/sources/oauth/views/callback.py index c9874e785..5e75ea404 100644 --- a/authentik/sources/oauth/views/callback.py +++ b/authentik/sources/oauth/views/callback.py @@ -15,10 +15,11 @@ from authentik.core.models import User from authentik.flows.models import Flow, in_memory_stage from authentik.flows.planner import ( PLAN_CONTEXT_PENDING_USER, + PLAN_CONTEXT_REDIRECT, PLAN_CONTEXT_SSO, FlowPlanner, ) -from authentik.flows.views import SESSION_KEY_PLAN +from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN from authentik.lib.utils.urls import redirect_with_qs from authentik.policies.utils import delete_none_keys from authentik.sources.oauth.auth import AuthorizedServiceBackend @@ -135,11 +136,17 @@ class OAuthCallback(OAuthClientMixin, View): def handle_login_flow(self, flow: Flow, **kwargs) -> HttpResponse: """Prepare Authentication Plan, redirect user FlowExecutor""" + # Ensure redirect is carried through when user was trying to + # authorize application + final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get( + NEXT_ARG_NAME, "authentik_core:shell" + ) kwargs.update( { # Since we authenticate the user by their token, they have no backend set PLAN_CONTEXT_AUTHENTICATION_BACKEND: "django.contrib.auth.backends.ModelBackend", PLAN_CONTEXT_SSO: True, + PLAN_CONTEXT_REDIRECT: final_redirect, } ) # We run the Flow planner here so we can pass the Pending user in the context diff --git a/authentik/sources/saml/processors/response.py b/authentik/sources/saml/processors/response.py index 5aecf7df4..568d22df0 100644 --- a/authentik/sources/saml/processors/response.py +++ b/authentik/sources/saml/processors/response.py @@ -13,10 +13,11 @@ from authentik.core.models import User from authentik.flows.models import Flow from authentik.flows.planner import ( PLAN_CONTEXT_PENDING_USER, + PLAN_CONTEXT_REDIRECT, PLAN_CONTEXT_SSO, FlowPlanner, ) -from authentik.flows.views import SESSION_KEY_PLAN +from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN from authentik.lib.utils.urls import redirect_with_qs from authentik.policies.utils import delete_none_keys from authentik.sources.saml.exceptions import ( @@ -54,11 +55,14 @@ class ResponseProcessor: _root: Any _root_xml: str + _http_request: HttpRequest + def __init__(self, source: SAMLSource): self._source = source def parse(self, request: HttpRequest): """Check if `request` contains SAML Response data, parse and validate it.""" + self._http_request = request # First off, check if we have any SAML Data at all. raw_response = request.POST.get("SAMLResponse", None) if not raw_response: @@ -187,6 +191,11 @@ class ResponseProcessor: name_id_filter = self._get_name_id_filter() matching_users = User.objects.filter(**name_id_filter) + # Ensure redirect is carried through when user was trying to + # authorize application + final_redirect = self._http_request.session.get(SESSION_KEY_GET, {}).get( + NEXT_ARG_NAME, "authentik_core:shell" + ) if matching_users.exists(): # User exists already, switch to authentication flow return self._flow_response( @@ -195,6 +204,7 @@ class ResponseProcessor: **{ PLAN_CONTEXT_PENDING_USER: matching_users.first(), PLAN_CONTEXT_AUTHENTICATION_BACKEND: DEFAULT_BACKEND, + PLAN_CONTEXT_REDIRECT: final_redirect, }, ) return self._flow_response(