providers/saml: migrate to challenge for submit

This commit is contained in:
Jens Langhammer 2021-02-21 14:36:22 +01:00
parent 14962eb6cc
commit ca223fa4df
6 changed files with 92 additions and 42 deletions

View File

@ -1,31 +0,0 @@
{% extends "login/base.html" %}
{% load authentik_utils %}
{% load i18n %}
{% block title %}
{{ title }}
{% endblock %}
{% block card %}
<form method="POST" action="{{ url }}" autosubmit>
{% csrf_token %}
{% for key, value in attrs.items %}
<input type="hidden" name="{{ key }}" value="{{ value }}">
{% endfor %}
<div class="pf-c-form__group pf-u-display-flex pf-u-justify-content-center">
<div class="pf-c-form__group-control">
<span class="pf-c-spinner" role="progressbar" aria-valuetext="Loading...">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
</div>
</div>
<div class="pf-c-form__group pf-m-action">
<div class="pf-c-form__actions">
<button class="pf-c-button pf-m-primary pf-m-block" type="submit">{% trans 'Continue' %}</button>
</div>
</div>
</form>
{% endblock %}

View File

@ -64,7 +64,7 @@ class ChallengeStageView(StageView):
return self.response_class(None, data=data, stage=self) return self.response_class(None, data=data, stage=self)
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: 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 challenge.initial_data["title"] = self.executor.flow.title
if not challenge.is_valid(): if not challenge.is_valid():
LOGGER.warning(challenge.errors) LOGGER.warning(challenge.errors)
@ -78,7 +78,7 @@ class ChallengeStageView(StageView):
return self.challenge_invalid(challenge) return self.challenge_invalid(challenge)
return self.challenge_valid(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""" """Return the challenge that the client should solve"""
raise NotImplementedError raise NotImplementedError

View File

@ -1,15 +1,19 @@
"""authentik SAML IDP Views""" """authentik SAML IDP Views"""
from django.core.validators import URLValidator from django.core.validators import URLValidator
from django.db.models.fields import CharField
from django.http import HttpRequest, HttpResponse 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.http import urlencode
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.fields import DictField
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.models import Application from authentik.core.models import Application
from authentik.events.models import Event, EventAction 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.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.lib.views import bad_request_message
from authentik.providers.saml.models import SAMLBindings, SAMLProvider from authentik.providers.saml.models import SAMLBindings, SAMLProvider
from authentik.providers.saml.processors.assertion import AssertionProcessor from authentik.providers.saml.processors.assertion import AssertionProcessor
@ -27,10 +31,17 @@ REQUEST_KEY_RELAY_STATE = "RelayState"
SESSION_KEY_AUTH_N_REQUEST = "authn_request" 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 # 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, """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).""" (if POST is configured)."""
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
@ -62,14 +73,14 @@ class SAMLFlowFinalView(StageView):
} }
if auth_n_request.relay_state: if auth_n_request.relay_state:
form_attrs[REQUEST_KEY_RELAY_STATE] = auth_n_request.relay_state form_attrs[REQUEST_KEY_RELAY_STATE] = auth_n_request.relay_state
return render( return self.get_challenge(
self.request,
"generic/autosubmit_form.html",
{ {
"url": provider.acs_url, "type": ChallengeTypes.native,
"component": "ak-stage-autosubmit",
"title": _("Redirecting to %(app)s..." % {"app": application.name}), "title": _("Redirecting to %(app)s..." % {"app": application.name}),
"url": provider.acs_url,
"attrs": form_attrs, "attrs": form_attrs,
}, }
) )
if provider.sp_binding == SAMLBindings.REDIRECT: if provider.sp_binding == SAMLBindings.REDIRECT:
url_args = { url_args = {
@ -80,3 +91,10 @@ class SAMLFlowFinalView(StageView):
querystring = urlencode(url_args) querystring = urlencode(url_args)
return redirect(f"{provider.acs_url}?{querystring}") return redirect(f"{provider.acs_url}?{querystring}")
return bad_request_message(request, "Invalid sp_binding specified") 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()

View File

@ -98,6 +98,9 @@ class EmailStageView(ChallengeStageView):
) )
return challenge return challenge
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
return super().challenge_invalid(response)
def challenge_invalid(self, response: ChallengeResponse) -> HttpResponse: def challenge_invalid(self, response: ChallengeResponse) -> HttpResponse:
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context: if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
messages.error(self.request, _("No pending user.")) messages.error(self.request, _("No pending user."))

View File

@ -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`<ak-loading-state></ak-loading-state>`;
}
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
${this.challenge.title}
</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form" >
${Object.entries(this.challenge.attrs).map(([ key, value ]) => {
return html`<input type="hidden" name="${key}" value="${value}">`;
})}
<ak-spinner></ak-spinner>
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${gettext("Continue")}
</button>
</div>
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
</footer>`;
}
}

View File

@ -6,12 +6,14 @@ import "../../elements/stages/identification/IdentificationStage";
import "../../elements/stages/password/PasswordStage"; import "../../elements/stages/password/PasswordStage";
import "../../elements/stages/consent/ConsentStage"; import "../../elements/stages/consent/ConsentStage";
import "../../elements/stages/email/EmailStage"; import "../../elements/stages/email/EmailStage";
import "../../elements/stages/autosubmit/AutosubmitStage";
import { ShellChallenge, Challenge, ChallengeTypes, Flow, RedirectChallenge } from "../../api/Flows"; import { ShellChallenge, Challenge, ChallengeTypes, Flow, RedirectChallenge } from "../../api/Flows";
import { DefaultClient } from "../../api/Client"; import { DefaultClient } from "../../api/Client";
import { IdentificationChallenge } from "../../elements/stages/identification/IdentificationStage"; import { IdentificationChallenge } from "../../elements/stages/identification/IdentificationStage";
import { PasswordChallenge } from "../../elements/stages/password/PasswordStage"; import { PasswordChallenge } from "../../elements/stages/password/PasswordStage";
import { ConsentChallenge } from "../../elements/stages/consent/ConsentStage"; import { ConsentChallenge } from "../../elements/stages/consent/ConsentStage";
import { EmailChallenge } from "../../elements/stages/email/EmailStage"; import { EmailChallenge } from "../../elements/stages/email/EmailStage";
import { AutosubmitChallenge } from "../../elements/stages/autosubmit/AutosubmitStage";
@customElement("ak-flow-executor") @customElement("ak-flow-executor")
export class FlowExecutor extends LitElement { export class FlowExecutor extends LitElement {
@ -116,6 +118,8 @@ export class FlowExecutor extends LitElement {
return html`<ak-stage-consent .host=${this} .challenge=${this.challenge as ConsentChallenge}></ak-stage-consent>`; return html`<ak-stage-consent .host=${this} .challenge=${this.challenge as ConsentChallenge}></ak-stage-consent>`;
case "ak-stage-email": case "ak-stage-email":
return html`<ak-stage-email .host=${this} .challenge=${this.challenge as EmailChallenge}></ak-stage-email>`; return html`<ak-stage-email .host=${this} .challenge=${this.challenge as EmailChallenge}></ak-stage-email>`;
case "ak-stage-autosubmit":
return html`<ak-stage-autosubmit .host=${this} .challenge=${this.challenge as AutosubmitChallenge}></ak-stage-autosubmit>`;
default: default:
break; break;
} }