def handle_login_flow( self, source: SAMLSource, *stages_to_append, **kwargs ) -> HttpResponse: """Prepare Authentication Plan, redirect user FlowExecutor""" # Ensure redirect is carried through when user was trying to # authorize application final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get( NEXT_ARG_NAME, "authentik_core:if-admin" ) kwargs.update( { PLAN_CONTEXT_SSO: True, PLAN_CONTEXT_SOURCE: source, PLAN_CONTEXT_REDIRECT: final_redirect, } ) # We run the Flow planner here so we can pass the Pending user in the context planner = FlowPlanner(source.pre_authentication_flow) planner.allow_empty_flows = True plan = planner.plan(self.request, kwargs) for stage in stages_to_append: plan.append(stage) self.request.session[SESSION_KEY_PLAN] = plan return redirect_with_qs( "authentik_core:if-flow", self.request.GET, flow_slug=source.pre_authentication_flow.slug, )
def execute(self, request: Request, slug: str): """Execute flow for current user""" # Because we pre-plan the flow here, and not in the planner, we need to manually clear # the history of the inspector request.session[SESSION_KEY_HISTORY] = [] flow: Flow = self.get_object() planner = FlowPlanner(flow) planner.use_cache = False try: plan = planner.plan(self.request, {PLAN_CONTEXT_PENDING_USER: request.user}) self.request.session[SESSION_KEY_PLAN] = plan except FlowNonApplicableException as exc: return bad_request_message( request, _( "Flow not applicable to current user/request: %(messages)s" % {"messages": str(exc)} ), ) return Response( { "link": request._request.build_absolute_uri( reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) ) } )
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:if-admin") 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_SOURCE: self.source, PLAN_CONTEXT_REDIRECT: final_redirect, }) if not flow: return HttpResponseBadRequest() # We run the Flow planner here so we can pass the Pending user in the context planner = FlowPlanner(flow) plan = planner.plan(self.request, kwargs) for stage in self.get_stages_to_append(flow): plan.append(stage) self.request.session[SESSION_KEY_PLAN] = plan return redirect_with_qs( "authentik_core:if-flow", self.request.GET, flow_slug=flow.slug, )
def test_stageview_user_identifier(self): """Test PLAN_CONTEXT_PENDING_USER_IDENTIFIER""" flow = Flow.objects.create( name="test-default-context", slug="test-default-context", designation=FlowDesignation.AUTHENTICATION, ) FlowStageBinding.objects.create( target=flow, stage=DummyStage.objects.create(name="dummy"), order=0) ident = "test-identifier" user = User.objects.create(username="******") request = self.request_factory.get( reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), ) request.user = user planner = FlowPlanner(flow) plan = planner.plan( request, default_context={PLAN_CONTEXT_PENDING_USER_IDENTIFIER: ident}) executor = FlowExecutorView() executor.plan = plan executor.flow = flow stage_view = StageView(executor) self.assertEqual( ident, stage_view.get_pending_user(for_display=True).username)
def get(self, request: HttpRequest, application_slug: str) -> HttpResponse: """Verify the SAML Request, and if valid initiate the FlowPlanner for the application""" # Call the method handler, which checks the SAML # Request and returns a HTTP Response on error method_response = self.check_saml_request() if method_response: return method_response # Regardless, we start the planner and return to it planner = FlowPlanner(self.provider.authorization_flow) planner.allow_empty_flows = True plan = planner.plan( request, { PLAN_CONTEXT_SSO: True, PLAN_CONTEXT_APPLICATION: self.application, PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.") % { "application": self.application.name }, PLAN_CONTEXT_CONSENT_PERMISSIONS: [], }, ) plan.append(in_memory_stage(SAMLFlowFinalView)) request.session[SESSION_KEY_PLAN] = plan return redirect_with_qs( "authentik_core:if-flow", request.GET, flow_slug=self.provider.authorization_flow.slug, )
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: """Start FlowPLanner, return to flow executor shell""" # After we've checked permissions, and the user has access, check if we need # to re-authenticate the user if self.params.max_age: current_age: timedelta = ( timezone.now() - Event.objects.filter( action=EventAction.LOGIN, user=get_user(self.request.user) ) .latest("created") .created ) if current_age.total_seconds() > self.params.max_age: return self.handle_no_permission() # If prompt=login, we need to re-authenticate the user regardless if ( PROMPT_LOGIN in self.params.prompt and SESSION_NEEDS_LOGIN not in self.request.session ): self.request.session[SESSION_NEEDS_LOGIN] = True return self.handle_no_permission() # Regardless, we start the planner and return to it planner = FlowPlanner(self.provider.authorization_flow) # planner.use_cache = False planner.allow_empty_flows = True scope_descriptions = UserInfoView().get_scope_descriptions(self.params.scope) plan: FlowPlan = planner.plan( self.request, { PLAN_CONTEXT_SSO: True, PLAN_CONTEXT_APPLICATION: self.application, # OAuth2 related params PLAN_CONTEXT_PARAMS: self.params, # Consent related params PLAN_CONTEXT_CONSENT_HEADER: _( "You're about to sign into %(application)s." ) % {"application": self.application.name}, PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions, }, ) # OpenID clients can specify a `prompt` parameter, and if its set to consent we # need to inject a consent stage if PROMPT_CONSNET in self.params.prompt: if not any(isinstance(x, ConsentStageView) for x in plan.stages): # Plan does not have any consent stage, so we add an in-memory one stage = ConsentStage( name="OAuth2 Provider In-memory consent stage", mode=ConsentMode.ALWAYS_REQUIRE, ) plan.append(stage) plan.append(in_memory_stage(OAuthFulfillmentStage)) self.request.session[SESSION_KEY_PLAN] = plan return redirect_with_qs( "authentik_core:if-flow", self.request.GET, flow_slug=self.provider.authorization_flow.slug, )
def restart_flow(self, keep_context=False) -> HttpResponse: """Restart the currently active flow, optionally keeping the current context""" planner = FlowPlanner(self.flow) default_context = None if keep_context: default_context = self.plan.context plan = planner.plan(self.request, default_context) self.request.session[SESSION_KEY_PLAN] = plan kwargs = self.kwargs kwargs.update({"flow_slug": self.flow.slug}) return redirect_with_qs("authentik_api:flow-executor", self.request.GET, **kwargs)
def _initiate_plan(self) -> FlowPlan: planner = FlowPlanner(self.flow) plan = planner.plan(self.request) self.request.session[SESSION_KEY_PLAN] = plan try: # Call the has_stages getter to check that # there are no issues with the class we might've gotten # from the cache. If there are errors, just delete all cached flows _ = plan.has_stages except Exception: # pylint: disable=broad-except keys = cache.keys("flow_*") cache.delete_many(keys) return self._initiate_plan() return plan
def test_planner_cache(self): """Test planner cache""" flow = Flow.objects.create( name="test-cache", slug="test-cache", designation=FlowDesignation.AUTHENTICATION, ) FlowStageBinding.objects.create( target=flow, stage=DummyStage.objects.create(name="dummy"), order=0 ) request = self.request_factory.get( reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), ) request.user = get_anonymous_user() planner = FlowPlanner(flow) planner.plan(request) self.assertEqual( CACHE_MOCK.set.call_count, 1 ) # Ensure plan is written to cache planner = FlowPlanner(flow) planner.plan(request) self.assertEqual( CACHE_MOCK.set.call_count, 1 ) # Ensure nothing is written to cache self.assertEqual(CACHE_MOCK.get.call_count, 2) # Get is called twice
def test_non_applicable_plan(self): """Test that empty plan raises exception""" flow = Flow.objects.create( name="test-empty", slug="test-empty", designation=FlowDesignation.AUTHENTICATION, ) request = self.request_factory.get( reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), ) request.user = get_anonymous_user() with self.assertRaises(FlowNonApplicableException): planner = FlowPlanner(flow) planner.plan(request)
def test_planner_reevaluate_actual(self): """Test planner with re-evaluate""" flow = Flow.objects.create( name="test-default-context", slug="test-default-context", designation=FlowDesignation.AUTHENTICATION, ) false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2) binding = FlowStageBinding.objects.create( target=flow, stage=DummyStage.objects.create(name="dummy1"), order=0 ) binding2 = FlowStageBinding.objects.create( target=flow, stage=DummyStage.objects.create(name="dummy2"), order=1, re_evaluate_policies=True, ) PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0) request = self.request_factory.get( reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), ) request.user = get_anonymous_user() middleware = SessionMiddleware(dummy_get_response) middleware.process_request(request) request.session.save() # Here we patch the dummy policy to evaluate to true so the stage is included with patch( "authentik.policies.dummy.models.DummyPolicy.passes", POLICY_RETURN_TRUE ): planner = FlowPlanner(flow) plan = planner.plan(request) self.assertEqual(plan.stages[0], binding.stage) self.assertEqual(plan.stages[1], binding2.stage) self.assertIsInstance(plan.markers[0], StageMarker) self.assertIsInstance(plan.markers[1], ReevaluateMarker)
def _flow_response(self, request: HttpRequest, flow: Flow, **kwargs) -> HttpResponse: kwargs[PLAN_CONTEXT_SSO] = True kwargs[PLAN_CONTEXT_SOURCE] = self._source request.session[SESSION_KEY_PLAN] = FlowPlanner(flow).plan( request, kwargs) return redirect_with_qs( "authentik_core:if-flow", request.GET, flow_slug=flow.slug, )
def test_planner_default_context(self): """Test planner with default_context""" flow = Flow.objects.create( name="test-default-context", slug="test-default-context", designation=FlowDesignation.AUTHENTICATION, ) FlowStageBinding.objects.create( target=flow, stage=DummyStage.objects.create(name="dummy"), order=0 ) user = User.objects.create(username="******") request = self.request_factory.get( reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), ) request.user = user planner = FlowPlanner(flow) planner.plan(request, default_context={PLAN_CONTEXT_PENDING_USER: user}) key = cache_key(flow, user) self.assertTrue(cache.get(key) is not None)
def test_planner_marker_reevaluate(self): """Test that the planner creates the proper marker""" flow = Flow.objects.create( name="test-default-context", slug="test-default-context", designation=FlowDesignation.AUTHENTICATION, ) FlowStageBinding.objects.create( target=flow, stage=DummyStage.objects.create(name="dummy1"), order=0, re_evaluate_policies=True, ) request = self.request_factory.get( reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), ) request.user = get_anonymous_user() planner = FlowPlanner(flow) plan = planner.plan(request) self.assertIsInstance(plan.markers[0], ReevaluateMarker)
def get(self, request: HttpRequest, stage_uuid: str) -> HttpResponse: """Initiate planner for selected change flow and redirect to flow executor, or raise Http404 if no configure_flow has been set.""" try: stage: Stage = Stage.objects.get_subclass(pk=stage_uuid) except Stage.DoesNotExist as exc: raise Http404 from exc if not isinstance(stage, ConfigurableStage): LOGGER.debug("Stage does not inherit ConfigurableStage", stage=stage) raise Http404 if not stage.configure_flow: LOGGER.debug("Stage has no configure_flow set", stage=stage) raise Http404 plan = FlowPlanner(stage.configure_flow).plan( request, {PLAN_CONTEXT_PENDING_USER: request.user}) request.session[SESSION_KEY_PLAN] = plan return redirect_with_qs( "authentik_core:if-flow", self.request.GET, flow_slug=stage.configure_flow.slug, )
def test_inner(): planner = FlowPlanner(self.flow) planner.use_cache = False planner.plan(self.request, {PLAN_CONTEXT_PENDING_USER: self.user})
def _initiate_plan(self) -> FlowPlan: planner = FlowPlanner(self.flow) plan = planner.plan(self.request) self.request.session[SESSION_KEY_PLAN] = plan return plan