Merge branch 'master' into next

This commit is contained in:
Jens Langhammer 2021-03-24 09:22:41 +01:00
commit e12d99ba63
19 changed files with 266 additions and 150 deletions

12
Pipfile.lock generated
View File

@ -116,18 +116,18 @@
}, },
"boto3": { "boto3": {
"hashes": [ "hashes": [
"sha256:1ddd597e3d8b7553432f84b32b9519cc90aad91c4dc3873725375163c9f98353", "sha256:1e6e06b2f1eee5a76acdde1e7b4f57c93c1bf2905341207d74f2a140ce060cd8",
"sha256:8f33cb3d2fc42b0547a5560a6d7397aa93336f50899386762b2450682c0e992b" "sha256:40e84a5f7888924db74a2710dbe48d066b51fe1f5549efaffe90e6efe813f37b"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.17.34" "version": "==1.17.35"
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
"sha256:749bdb151e340329f1b25600bfe9d223e930f8ba26bd74b71478ca5781f2feaf", "sha256:9119ffb231145ffadd55391c9356dcdb18e3de65c3a7c82844634e949f0ca5a0",
"sha256:c4fe4fea1d6a3934dd8c670ee83b128f935a64078786fe8afb8a662446304926" "sha256:e34bbb7d7de154c2ff2a73ae0691c601a69c5bda887374c8a6a23072380b07a4"
], ],
"version": "==1.20.34" "version": "==1.20.35"
}, },
"cachetools": { "cachetools": {
"hashes": [ "hashes": [

View File

@ -1,34 +0,0 @@
{% extends "login/base_full.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>
<script>
document.querySelector("form").submit();
</script>
{% endblock %}

View File

@ -26,7 +26,7 @@ class UILoginButtonSerializer(Serializer):
name = CharField() name = CharField()
url = CharField() url = CharField()
icon_url = CharField() icon_url = CharField(required=False)
def create(self, validated_data: dict) -> Model: def create(self, validated_data: dict) -> Model:
return Model() return Model()

View File

@ -77,7 +77,7 @@ class Stage(SerializerModel):
def in_memory_stage(view: Type["StageView"]) -> Stage: def in_memory_stage(view: Type["StageView"]) -> Stage:
"""Creates an in-memory stage instance, based on a `_type` as view.""" """Creates an in-memory stage instance, based on a `view` as view."""
stage = Stage() stage = Stage()
# Because we can't pickle a locally generated function, # Because we can't pickle a locally generated function,
# we set the view as a separate property and reference a generic function # we set the view as a separate property and reference a generic function

View File

@ -81,6 +81,9 @@ class TestAuthNRequest(TestCase):
self.source = SAMLSource.objects.create( self.source = SAMLSource.objects.create(
slug="provider", slug="provider",
issuer="authentik", issuer="authentik",
pre_authentication_flow=Flow.objects.get(
slug="default-source-pre-authentication"
),
signing_kp=cert, signing_kp=cert,
) )
self.factory = RequestFactory() self.factory = RequestFactory()

View File

@ -37,6 +37,9 @@ class TestSchema(TestCase):
slug="provider", slug="provider",
issuer="authentik", issuer="authentik",
signing_kp=cert, signing_kp=cert,
pre_authentication_flow=Flow.objects.get(
slug="default-source-pre-authentication"
),
) )
self.factory = RequestFactory() self.factory = RequestFactory()

View File

@ -136,7 +136,9 @@ class OAuthCallback(OAuthClientMixin, View):
messages.error(self.request, _("Authentication Failed.")) messages.error(self.request, _("Authentication Failed."))
return redirect(self.get_error_redirect(source, reason)) return redirect(self.get_error_redirect(source, reason))
def handle_login_flow(self, flow: Flow, **kwargs) -> HttpResponse: def handle_login_flow(
self, flow: Flow, *stages_to_append, **kwargs
) -> HttpResponse:
"""Prepare Authentication Plan, redirect user FlowExecutor""" """Prepare Authentication Plan, redirect user FlowExecutor"""
# Ensure redirect is carried through when user was trying to # Ensure redirect is carried through when user was trying to
# authorize application # authorize application
@ -157,6 +159,8 @@ class OAuthCallback(OAuthClientMixin, View):
# We run the Flow planner here so we can pass the Pending user in the context # We run the Flow planner here so we can pass the Pending user in the context
planner = FlowPlanner(flow) planner = FlowPlanner(flow)
plan = planner.plan(self.request, kwargs) plan = planner.plan(self.request, kwargs)
for stage in stages_to_append:
plan.append(stage)
self.request.session[SESSION_KEY_PLAN] = plan self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs( return redirect_with_qs(
"authentik_core:if-flow", "authentik_core:if-flow",
@ -224,27 +228,18 @@ class OAuthCallback(OAuthClientMixin, View):
% {"source": self.source.name} % {"source": self.source.name}
), ),
) )
# Because we inject a stage into the planned flow, we can't use `self.handle_login_flow`
context = {
# 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_PROMPT: delete_none_keys(
self.get_user_enroll_context(source, access, info)
),
PLAN_CONTEXT_SOURCES_OAUTH_ACCESS: access,
}
# We run the Flow planner here so we can pass the Pending user in the context # We run the Flow planner here so we can pass the Pending user in the context
if not source.enrollment_flow: if not source.enrollment_flow:
LOGGER.warning("source has no enrollment flow", source=source) LOGGER.warning("source has no enrollment flow", source=source)
return HttpResponseBadRequest() return HttpResponseBadRequest()
planner = FlowPlanner(source.enrollment_flow) return self.handle_login_flow(
plan = planner.plan(self.request, context) source.enrollment_flow,
plan.append(in_memory_stage(PostUserEnrollmentStage)) in_memory_stage(PostUserEnrollmentStage),
self.request.session[SESSION_KEY_PLAN] = plan **{
return redirect_with_qs( PLAN_CONTEXT_PROMPT: delete_none_keys(
"authentik_core:if-flow", self.get_user_enroll_context(source, access, info)
self.request.GET, ),
flow_slug=source.enrollment_flow.slug, PLAN_CONTEXT_SOURCES_OAUTH_ACCESS: access,
},
) )

View File

@ -18,6 +18,7 @@ class SAMLSourceSerializer(SourceSerializer):
model = SAMLSource model = SAMLSource
fields = SourceSerializer.Meta.fields + [ fields = SourceSerializer.Meta.fields + [
"pre_authentication_flow",
"issuer", "issuer",
"sso_url", "sso_url",
"slo_url", "slo_url",

View File

@ -14,6 +14,9 @@ class SAMLSourceForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields["pre_authentication_flow"].queryset = Flow.objects.filter(
designation=FlowDesignation.AUTHENTICATION
)
self.fields["authentication_flow"].queryset = Flow.objects.filter( self.fields["authentication_flow"].queryset = Flow.objects.filter(
designation=FlowDesignation.AUTHENTICATION designation=FlowDesignation.AUTHENTICATION
) )
@ -32,6 +35,7 @@ class SAMLSourceForm(forms.ModelForm):
"name", "name",
"slug", "slug",
"enabled", "enabled",
"pre_authentication_flow",
"authentication_flow", "authentication_flow",
"enrollment_flow", "enrollment_flow",
"issuer", "issuer",

View File

@ -0,0 +1,53 @@
# Generated by Django 3.1.7 on 2021-03-23 22:09
import django.db.models.deletion
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from authentik.flows.models import FlowDesignation
def create_default_pre_authentication_flow(
apps: Apps, schema_editor: BaseDatabaseSchemaEditor
):
Flow = apps.get_model("authentik_flows", "Flow")
SAMLSource = apps.get_model("authentik_sources_saml", "samlsource")
db_alias = schema_editor.connection.alias
# Empty flow for providers where consent is implicitly given
flow, _ = Flow.objects.using(db_alias).update_or_create(
slug="default-source-pre-authentication",
designation=FlowDesignation.AUTHENTICATION,
defaults={"name": "Pre-Authentication", "title": ""},
)
for source in SAMLSource.objects.using(db_alias).all():
source.pre_authentication_flow = flow
source.save()
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0016_auto_20201202_1307"),
("authentik_sources_saml", "0009_auto_20210301_0949"),
]
operations = [
migrations.AddField(
model_name="samlsource",
name="pre_authentication_flow",
field=models.ForeignKey(
default=None,
null=True,
help_text="Flow used before authentication.",
on_delete=django.db.models.deletion.CASCADE,
related_name="source_pre_authentication",
to="authentik_flows.flow",
),
preserve_default=False,
),
migrations.RunPython(create_default_pre_authentication_flow),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 3.1.7 on 2021-03-24 07:36
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0016_auto_20201202_1307"),
("authentik_sources_saml", "0010_samlsource_pre_authentication_flow"),
]
operations = [
migrations.AlterField(
model_name="samlsource",
name="pre_authentication_flow",
field=models.ForeignKey(
help_text="Flow used before authentication.",
on_delete=django.db.models.deletion.CASCADE,
related_name="source_pre_authentication",
to="authentik_flows.flow",
),
),
]

View File

@ -11,6 +11,7 @@ from rest_framework.serializers import Serializer
from authentik.core.models import Source from authentik.core.models import Source
from authentik.core.types import UILoginButton from authentik.core.types import UILoginButton
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow
from authentik.lib.utils.time import timedelta_string_validator from authentik.lib.utils.time import timedelta_string_validator
from authentik.sources.saml.processors.constants import ( from authentik.sources.saml.processors.constants import (
DSA_SHA1, DSA_SHA1,
@ -51,6 +52,13 @@ class SAMLNameIDPolicy(models.TextChoices):
class SAMLSource(Source): class SAMLSource(Source):
"""Authenticate using an external SAML Identity Provider.""" """Authenticate using an external SAML Identity Provider."""
pre_authentication_flow = models.ForeignKey(
Flow,
on_delete=models.CASCADE,
help_text=_("Flow used before authentication."),
related_name="source_pre_authentication",
)
issuer = models.TextField( issuer = models.TextField(
blank=True, blank=True,
default=None, default=None,

View File

@ -1,26 +0,0 @@
{% extends "login/base_full.html" %}
{% load authentik_utils %}
{% load i18n %}
{% block title %}
{% trans 'Authorize Application' %}
{% endblock %}
{% block card %}
<form class="pf-c-form" method="POST" action="{{ request_url }}">
{% csrf_token %}
<input type="hidden" name="SAMLRequest" value="{{ request }}" />
<input type="hidden" name="RelayState" value="{{ relay_state }}" />
<div class="login-group">
<h3>
{% blocktrans with source=source.name %}
You're about to sign-in via {{ source }}.
{% endblocktrans %}
</h3>
</div>
<div class="pf-c-form__group pf-m-action">
<button class="pf-c-button pf-m-primary pf-m-block" type="submit">{% trans "Continue" %}</button>
</div>
</form>
{% endblock %}

View File

@ -4,6 +4,7 @@ from django.test import RequestFactory, TestCase
from lxml import etree # nosec from lxml import etree # nosec
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow
from authentik.sources.saml.models import SAMLSource from authentik.sources.saml.models import SAMLSource
from authentik.sources.saml.processors.metadata import MetadataProcessor from authentik.sources.saml.processors.metadata import MetadataProcessor
@ -20,6 +21,9 @@ class TestMetadataProcessor(TestCase):
slug="provider", slug="provider",
issuer="authentik", issuer="authentik",
signing_kp=CertificateKeyPair.objects.first(), signing_kp=CertificateKeyPair.objects.first(),
pre_authentication_flow=Flow.objects.get(
slug="default-source-pre-authentication"
),
) )
request = self.factory.get("/") request = self.factory.get("/")
xml = MetadataProcessor(source, request).build_entity_descriptor() xml = MetadataProcessor(source, request).build_entity_descriptor()

View File

@ -2,7 +2,8 @@
from django.contrib.auth import logout from django.contrib.auth import logout
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import Http404, HttpRequest, HttpResponse from django.http import Http404, 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.decorators import method_decorator from django.utils.decorators import method_decorator
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 _
@ -10,8 +11,20 @@ from django.views import View
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from xmlsec import VerificationError from xmlsec import VerificationError
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
from authentik.flows.models import in_memory_stage
from authentik.flows.planner import (
PLAN_CONTEXT_REDIRECT,
PLAN_CONTEXT_SOURCE,
PLAN_CONTEXT_SSO,
FlowPlanner,
)
from authentik.flows.stage import ChallengeStageView
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.lib.views import bad_request_message from authentik.lib.views import bad_request_message
from authentik.providers.saml.utils.encoding import nice64 from authentik.providers.saml.utils.encoding import nice64
from authentik.providers.saml.views.flows import AutosubmitChallenge
from authentik.sources.saml.exceptions import ( from authentik.sources.saml.exceptions import (
MissingSAMLResponse, MissingSAMLResponse,
UnsupportedNameIDFormat, UnsupportedNameIDFormat,
@ -20,11 +33,68 @@ from authentik.sources.saml.models import SAMLBindingTypes, SAMLSource
from authentik.sources.saml.processors.metadata import MetadataProcessor from authentik.sources.saml.processors.metadata import MetadataProcessor
from authentik.sources.saml.processors.request import RequestProcessor from authentik.sources.saml.processors.request import RequestProcessor
from authentik.sources.saml.processors.response import ResponseProcessor from authentik.sources.saml.processors.response import ResponseProcessor
from authentik.stages.consent.stage import (
PLAN_CONTEXT_CONSENT_HEADER,
PLAN_CONTEXT_CONSENT_TITLE,
ConsentStageView,
)
PLAN_CONTEXT_TITLE = "title"
PLAN_CONTEXT_URL = "url"
PLAN_CONTEXT_ATTRS = "attrs"
class AutosubmitStageView(ChallengeStageView):
"""Wrapper stage to create an autosubmit challenge from plan context variables"""
def get_challenge(self, *args, **kwargs) -> Challenge:
return AutosubmitChallenge(
data={
"type": ChallengeTypes.native.value,
"component": "ak-stage-autosubmit",
"title": self.executor.plan.context.get(PLAN_CONTEXT_TITLE, ""),
"url": self.executor.plan.context.get(PLAN_CONTEXT_URL, ""),
"attrs": self.executor.plan.context.get(PLAN_CONTEXT_ATTRS, ""),
},
)
# Since `ak-stage-autosubmit` redirects off site, we don't have anything to check
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
return HttpResponseBadRequest()
class InitiateView(View): class InitiateView(View):
"""Get the Form with SAML Request, which sends us to the IDP""" """Get the Form with SAML Request, which sends us to the IDP"""
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 get(self, request: HttpRequest, source_slug: str) -> HttpResponse: def get(self, request: HttpRequest, source_slug: str) -> HttpResponse:
"""Replies with an XHTML SSO Request.""" """Replies with an XHTML SSO Request."""
source: SAMLSource = get_object_or_404(SAMLSource, slug=source_slug) source: SAMLSource = get_object_or_404(SAMLSource, slug=source_slug)
@ -38,29 +108,29 @@ class InitiateView(View):
return redirect(f"{source.sso_url}?{url_args}") return redirect(f"{source.sso_url}?{url_args}")
# As POST Binding we show a form # As POST Binding we show a form
saml_request = nice64(auth_n_req.build_auth_n()) saml_request = nice64(auth_n_req.build_auth_n())
injected_stages = []
plan_kwargs = {
PLAN_CONTEXT_TITLE: _("Redirecting to %(app)s..." % {"app": source.name}),
PLAN_CONTEXT_CONSENT_TITLE: _(
"Redirecting to %(app)s..." % {"app": source.name}
),
PLAN_CONTEXT_ATTRS: {
"SAMLRequest": saml_request,
"RelayState": relay_state,
},
PLAN_CONTEXT_URL: source.sso_url,
}
# For just POST we add a consent stage,
# otherwise we default to POST_AUTO, with direct redirect
if source.binding_type == SAMLBindingTypes.POST: if source.binding_type == SAMLBindingTypes.POST:
return render( injected_stages.append(in_memory_stage(ConsentStageView))
request, plan_kwargs[PLAN_CONTEXT_CONSENT_HEADER] = f"Continue to {source.name}"
"saml/sp/login.html", injected_stages.append(in_memory_stage(AutosubmitStageView))
{ return self.handle_login_flow(
"request_url": source.sso_url, source,
"request": saml_request, *injected_stages,
"relay_state": relay_state, **plan_kwargs,
"source": source, )
},
)
# Or an auto-submit form
if source.binding_type == SAMLBindingTypes.POST_AUTO:
return render(
request,
"generic/autosubmit_form_full.html",
{
"title": _("Redirecting to %(app)s..." % {"app": source.name}),
"attrs": {"SAMLRequest": saml_request, "RelayState": relay_state},
"url": source.sso_url,
},
)
raise Http404
@method_decorator(csrf_exempt, name="dispatch") @method_decorator(csrf_exempt, name="dispatch")

View File

@ -1,15 +0,0 @@
{% load i18n %}
<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
{% trans 'WebAuthn' %}
</h1>
</header>
<div class="pf-c-login__main-body">
{% block card %}
<div class="pf-c-form">
<ak-stage-webauthn-auth>
</ak-stage-webauthn-auth>
</div>
{% endblock %}
</div>

View File

@ -1,16 +0,0 @@
{% load i18n %}
<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
{% trans 'Configure WebAuthn' %}
</h1>
</header>
<div class="pf-c-login__main-body">
{% block card %}
<div class="pf-c-form">
<ak-stage-webauthn-register>
</ak-stage-webauthn-register>
</div>
{% endblock %}
</div>

View File

@ -15,6 +15,7 @@ from authentik.flows.stage import ChallengeStageView
from authentik.lib.utils.time import timedelta_from_string from authentik.lib.utils.time import timedelta_from_string
from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent
PLAN_CONTEXT_CONSENT_TITLE = "consent_title"
PLAN_CONTEXT_CONSENT_HEADER = "consent_header" PLAN_CONTEXT_CONSENT_HEADER = "consent_header"
PLAN_CONTEXT_CONSENT_PERMISSIONS = "consent_permissions" PLAN_CONTEXT_CONSENT_PERMISSIONS = "consent_permissions"
@ -42,6 +43,10 @@ class ConsentStageView(ChallengeStageView):
"component": "ak-stage-consent", "component": "ak-stage-consent",
} }
) )
if PLAN_CONTEXT_CONSENT_TITLE in self.executor.plan.context:
challenge.initial_data["title"] = self.executor.plan.context[
PLAN_CONTEXT_CONSENT_TITLE
]
if PLAN_CONTEXT_CONSENT_HEADER in self.executor.plan.context: if PLAN_CONTEXT_CONSENT_HEADER in self.executor.plan.context:
challenge.initial_data["header_text"] = self.executor.plan.context[ challenge.initial_data["header_text"] = self.executor.plan.context[
PLAN_CONTEXT_CONSENT_HEADER PLAN_CONTEXT_CONSENT_HEADER
@ -50,16 +55,15 @@ class ConsentStageView(ChallengeStageView):
challenge.initial_data["permissions"] = self.executor.plan.context[ challenge.initial_data["permissions"] = self.executor.plan.context[
PLAN_CONTEXT_CONSENT_PERMISSIONS PLAN_CONTEXT_CONSENT_PERMISSIONS
] ]
# If there's a pending user, update the `username` field
# this field is only used by password managers.
# If there's no user set, an error is raised later.
if user := self.get_pending_user():
challenge.initial_data["pending_user"] = user.username
challenge.initial_data["pending_user_avatar"] = user.avatar
return challenge return challenge
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
current_stage: ConsentStage = self.executor.current_stage current_stage: ConsentStage = self.executor.current_stage
# Make this StageView work when injected, in which case `current_stage` is an instance
# of the base class, and we don't save any consent, as it is assumed to be a one-time
# prompt
if not isinstance(current_stage, ConsentStage):
return super().get(request, *args, **kwargs)
# For always require, we always return the challenge # For always require, we always return the challenge
if current_stage.mode == ConsentMode.ALWAYS_REQUIRE: if current_stage.mode == ConsentMode.ALWAYS_REQUIRE:
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
@ -85,6 +89,11 @@ class ConsentStageView(ChallengeStageView):
if PLAN_CONTEXT_APPLICATION not in self.executor.plan.context: if PLAN_CONTEXT_APPLICATION not in self.executor.plan.context:
return self.executor.stage_ok() return self.executor.stage_ok()
application = self.executor.plan.context[PLAN_CONTEXT_APPLICATION] application = self.executor.plan.context[PLAN_CONTEXT_APPLICATION]
# Make this StageView work when injected, in which case `current_stage` is an instance
# of the base class, and we don't save any consent, as it is assumed to be a one-time
# prompt
if not isinstance(current_stage, ConsentStage):
return self.executor.stage_ok()
# Since we only get here when no consent exists, we can create it without update # Since we only get here when no consent exists, we can create it without update
if current_stage.mode == ConsentMode.PERMANENT: if current_stage.mode == ConsentMode.PERMANENT:
UserConsent.objects.create( UserConsent.objects.create(

View File

@ -98,12 +98,18 @@ class TestSourceSAML(SeleniumTestCase):
@apply_migration("authentik_flows", "0008_default_flows") @apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0009_source_flows") @apply_migration("authentik_flows", "0009_source_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp") @apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@apply_migration(
"authentik_sources_saml", "0010_samlsource_pre_authentication_flow"
)
@object_manager @object_manager
def test_idp_redirect(self): def test_idp_redirect(self):
"""test SAML Source With redirect binding""" """test SAML Source With redirect binding"""
# Bootstrap all needed objects # Bootstrap all needed objects
authentication_flow = Flow.objects.get(slug="default-source-authentication") authentication_flow = Flow.objects.get(slug="default-source-authentication")
enrollment_flow = Flow.objects.get(slug="default-source-enrollment") enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
pre_authentication_flow = Flow.objects.get(
slug="default-source-pre-authentication"
)
keypair = CertificateKeyPair.objects.create( keypair = CertificateKeyPair.objects.create(
name="test-idp-cert", name="test-idp-cert",
certificate_data=IDP_CERT, certificate_data=IDP_CERT,
@ -115,6 +121,7 @@ class TestSourceSAML(SeleniumTestCase):
slug="saml-idp-test", slug="saml-idp-test",
authentication_flow=authentication_flow, authentication_flow=authentication_flow,
enrollment_flow=enrollment_flow, enrollment_flow=enrollment_flow,
pre_authentication_flow=pre_authentication_flow,
issuer="entity-id", issuer="entity-id",
sso_url="http://localhost:8080/simplesaml/saml2/idp/SSOService.php", sso_url="http://localhost:8080/simplesaml/saml2/idp/SSOService.php",
binding_type=SAMLBindingTypes.Redirect, binding_type=SAMLBindingTypes.Redirect,
@ -158,23 +165,30 @@ class TestSourceSAML(SeleniumTestCase):
@apply_migration("authentik_flows", "0008_default_flows") @apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0009_source_flows") @apply_migration("authentik_flows", "0009_source_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp") @apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@apply_migration(
"authentik_sources_saml", "0010_samlsource_pre_authentication_flow"
)
@object_manager @object_manager
def test_idp_post(self): def test_idp_post(self):
"""test SAML Source With post binding""" """test SAML Source With post binding"""
# Bootstrap all needed objects # Bootstrap all needed objects
authentication_flow = Flow.objects.get(slug="default-source-authentication") authentication_flow = Flow.objects.get(slug="default-source-authentication")
enrollment_flow = Flow.objects.get(slug="default-source-enrollment") enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
pre_authentication_flow = Flow.objects.get(
slug="default-source-pre-authentication"
)
keypair = CertificateKeyPair.objects.create( keypair = CertificateKeyPair.objects.create(
name="test-idp-cert", name="test-idp-cert",
certificate_data=IDP_CERT, certificate_data=IDP_CERT,
key_data=IDP_KEY, key_data=IDP_KEY,
) )
SAMLSource.objects.create( source = SAMLSource.objects.create(
name="saml-idp-test", name="saml-idp-test",
slug="saml-idp-test", slug="saml-idp-test",
authentication_flow=authentication_flow, authentication_flow=authentication_flow,
enrollment_flow=enrollment_flow, enrollment_flow=enrollment_flow,
pre_authentication_flow=pre_authentication_flow,
issuer="entity-id", issuer="entity-id",
sso_url="http://localhost:8080/simplesaml/saml2/idp/SSOService.php", sso_url="http://localhost:8080/simplesaml/saml2/idp/SSOService.php",
binding_type=SAMLBindingTypes.POST, binding_type=SAMLBindingTypes.POST,
@ -198,7 +212,18 @@ class TestSourceSAML(SeleniumTestCase):
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link" By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
).click() ).click()
sleep(1) sleep(1)
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
flow_executor = self.get_shadow_root("ak-flow-executor")
consent_stage = self.get_shadow_root("ak-stage-consent", flow_executor)
self.assertIn(
source.name,
consent_stage.find_element(By.CSS_SELECTOR, "#header-text").text,
)
consent_stage.find_element(
By.CSS_SELECTOR,
("[type=submit]"),
).click()
# Now we should be at the IDP, wait for the username field # Now we should be at the IDP, wait for the username field
self.wait.until(ec.presence_of_element_located((By.ID, "username"))) self.wait.until(ec.presence_of_element_located((By.ID, "username")))
@ -220,12 +245,18 @@ class TestSourceSAML(SeleniumTestCase):
@apply_migration("authentik_flows", "0008_default_flows") @apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0009_source_flows") @apply_migration("authentik_flows", "0009_source_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp") @apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@apply_migration(
"authentik_sources_saml", "0010_samlsource_pre_authentication_flow"
)
@object_manager @object_manager
def test_idp_post_auto(self): def test_idp_post_auto(self):
"""test SAML Source With post binding (auto redirect)""" """test SAML Source With post binding (auto redirect)"""
# Bootstrap all needed objects # Bootstrap all needed objects
authentication_flow = Flow.objects.get(slug="default-source-authentication") authentication_flow = Flow.objects.get(slug="default-source-authentication")
enrollment_flow = Flow.objects.get(slug="default-source-enrollment") enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
pre_authentication_flow = Flow.objects.get(
slug="default-source-pre-authentication"
)
keypair = CertificateKeyPair.objects.create( keypair = CertificateKeyPair.objects.create(
name="test-idp-cert", name="test-idp-cert",
certificate_data=IDP_CERT, certificate_data=IDP_CERT,
@ -237,6 +268,7 @@ class TestSourceSAML(SeleniumTestCase):
slug="saml-idp-test", slug="saml-idp-test",
authentication_flow=authentication_flow, authentication_flow=authentication_flow,
enrollment_flow=enrollment_flow, enrollment_flow=enrollment_flow,
pre_authentication_flow=pre_authentication_flow,
issuer="entity-id", issuer="entity-id",
sso_url="http://localhost:8080/simplesaml/saml2/idp/SSOService.php", sso_url="http://localhost:8080/simplesaml/saml2/idp/SSOService.php",
binding_type=SAMLBindingTypes.POST_AUTO, binding_type=SAMLBindingTypes.POST_AUTO,