diff --git a/authentik/core/templates/generic/autosubmit_form.html b/authentik/core/templates/generic/autosubmit_form.html deleted file mode 100644 index b7254b437..000000000 --- a/authentik/core/templates/generic/autosubmit_form.html +++ /dev/null @@ -1,31 +0,0 @@ -{% extends "login/base.html" %} - -{% load authentik_utils %} -{% load i18n %} - -{% block title %} -{{ title }} -{% endblock %} - -{% block card %} -
-{% endblock %} diff --git a/authentik/flows/stage.py b/authentik/flows/stage.py index 38cd05c56..ade60ba89 100644 --- a/authentik/flows/stage.py +++ b/authentik/flows/stage.py @@ -64,7 +64,7 @@ class ChallengeStageView(StageView): return self.response_class(None, data=data, stage=self) def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: - challenge = self.get_challenge() + challenge = self.get_challenge(*args, **kwargs) challenge.initial_data["title"] = self.executor.flow.title if not challenge.is_valid(): LOGGER.warning(challenge.errors) @@ -78,7 +78,7 @@ class ChallengeStageView(StageView): return self.challenge_invalid(challenge) return self.challenge_valid(challenge) - def get_challenge(self) -> Challenge: + def get_challenge(self, *args, **kwargs) -> Challenge: """Return the challenge that the client should solve""" raise NotImplementedError diff --git a/authentik/providers/saml/views/flows.py b/authentik/providers/saml/views/flows.py index eb0b08d63..7ba68ec43 100644 --- a/authentik/providers/saml/views/flows.py +++ b/authentik/providers/saml/views/flows.py @@ -1,15 +1,19 @@ """authentik SAML IDP Views""" from django.core.validators import URLValidator +from django.db.models.fields import CharField from django.http import HttpRequest, HttpResponse -from django.shortcuts import get_object_or_404, redirect, render +from django.http.response import HttpResponseBadRequest +from django.shortcuts import get_object_or_404, redirect from django.utils.http import urlencode from django.utils.translation import gettext_lazy as _ +from rest_framework.fields import DictField from structlog.stdlib import get_logger from authentik.core.models import Application from authentik.events.models import Event, EventAction +from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes from authentik.flows.planner import PLAN_CONTEXT_APPLICATION -from authentik.flows.stage import StageView +from authentik.flows.stage import ChallengeStageView from authentik.lib.views import bad_request_message from authentik.providers.saml.models import SAMLBindings, SAMLProvider from authentik.providers.saml.processors.assertion import AssertionProcessor @@ -27,10 +31,17 @@ REQUEST_KEY_RELAY_STATE = "RelayState" SESSION_KEY_AUTH_N_REQUEST = "authn_request" +class AutosubmitChallenge(Challenge): + """Autosubmit challenge used to send and navigate a POST request""" + + url = CharField() + attrs = DictField(CharField()) + + # This View doesn't have a URL on purpose, as its called by the FlowExecutor -class SAMLFlowFinalView(StageView): +class SAMLFlowFinalView(ChallengeStageView): """View used by FlowExecutor after all stages have passed. Logs the authorization, - and redirects to the SP (if REDIRECT is configured) or shows and auto-submit for + and redirects to the SP (if REDIRECT is configured) or shows an auto-submit element (if POST is configured).""" def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: @@ -62,14 +73,14 @@ class SAMLFlowFinalView(StageView): } if auth_n_request.relay_state: form_attrs[REQUEST_KEY_RELAY_STATE] = auth_n_request.relay_state - return render( - self.request, - "generic/autosubmit_form.html", + return self.get_challenge( { - "url": provider.acs_url, + "type": ChallengeTypes.native, + "component": "ak-stage-autosubmit", "title": _("Redirecting to %(app)s..." % {"app": application.name}), + "url": provider.acs_url, "attrs": form_attrs, - }, + } ) if provider.sp_binding == SAMLBindings.REDIRECT: url_args = { @@ -80,3 +91,10 @@ class SAMLFlowFinalView(StageView): querystring = urlencode(url_args) return redirect(f"{provider.acs_url}?{querystring}") return bad_request_message(request, "Invalid sp_binding specified") + + def get_challenge(self, *args, **kwargs) -> Challenge: + return Challenge(data=kwargs) + + def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: + # We'll never get here since the challenge redirects to the SP + return HttpResponseBadRequest() diff --git a/authentik/stages/email/stage.py b/authentik/stages/email/stage.py index f6cea9422..7c398ccb0 100644 --- a/authentik/stages/email/stage.py +++ b/authentik/stages/email/stage.py @@ -98,6 +98,9 @@ class EmailStageView(ChallengeStageView): ) return challenge + def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: + return super().challenge_invalid(response) + def challenge_invalid(self, response: ChallengeResponse) -> HttpResponse: if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context: messages.error(self.request, _("No pending user.")) diff --git a/web/src/elements/stages/autosubmit/AutosubmitStage.ts b/web/src/elements/stages/autosubmit/AutosubmitStage.ts new file mode 100644 index 000000000..c3d524d77 --- /dev/null +++ b/web/src/elements/stages/autosubmit/AutosubmitStage.ts @@ -0,0 +1,56 @@ +import { gettext } from "django"; +import { CSSResult, customElement, html, property, TemplateResult } from "lit-element"; +import { WithUserInfoChallenge } from "../../../api/Flows"; +import { COMMON_STYLES } from "../../../common/styles"; +import { BaseStage } from "../base"; +import "../../Spinner"; + +export interface AutosubmitChallenge extends WithUserInfoChallenge { + url: string; + attrs: { [key: string]: string }; +} + +@customElement("ak-stage-autosubmit") +export class AutosubmitStage extends BaseStage { + + @property({ attribute: false }) + challenge?: AutosubmitChallenge; + + static get styles(): CSSResult[] { + return COMMON_STYLES; + } + + updated(): void { + this.shadowRoot?.querySelectorAll("form").forEach((form) => {form.submit()}); + } + + render(): TemplateResult { + if (!this.challenge) { + return html`