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 %} -
- {% csrf_token %} - {% for key, value in attrs.items %} - - {% endfor %} -
-
- - - - - -
-
-
-
- -
-
-
-{% 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``; + } + return html`
+

+ ${this.challenge.title} +

+
+
+
+ ${Object.entries(this.challenge.attrs).map(([ key, value ]) => { + return html``; + })} + + +
+ +
+
+
+ `; + } + +} diff --git a/web/src/pages/generic/FlowExecutor.ts b/web/src/pages/generic/FlowExecutor.ts index e9c969ede..60dfee1a1 100644 --- a/web/src/pages/generic/FlowExecutor.ts +++ b/web/src/pages/generic/FlowExecutor.ts @@ -6,12 +6,14 @@ import "../../elements/stages/identification/IdentificationStage"; import "../../elements/stages/password/PasswordStage"; import "../../elements/stages/consent/ConsentStage"; import "../../elements/stages/email/EmailStage"; +import "../../elements/stages/autosubmit/AutosubmitStage"; import { ShellChallenge, Challenge, ChallengeTypes, Flow, RedirectChallenge } from "../../api/Flows"; import { DefaultClient } from "../../api/Client"; import { IdentificationChallenge } from "../../elements/stages/identification/IdentificationStage"; import { PasswordChallenge } from "../../elements/stages/password/PasswordStage"; import { ConsentChallenge } from "../../elements/stages/consent/ConsentStage"; import { EmailChallenge } from "../../elements/stages/email/EmailStage"; +import { AutosubmitChallenge } from "../../elements/stages/autosubmit/AutosubmitStage"; @customElement("ak-flow-executor") export class FlowExecutor extends LitElement { @@ -116,6 +118,8 @@ export class FlowExecutor extends LitElement { return html``; case "ak-stage-email": return html``; + case "ak-stage-autosubmit": + return html``; default: break; }