sources/saml: Add NameID Policy field, sent with AuthnRequest

This commit is contained in:
Jens Langhammer 2020-07-08 16:18:02 +02:00
parent d831599608
commit 0e3e73989d
8 changed files with 95 additions and 9 deletions

View File

@ -15,6 +15,7 @@ class SAMLSourceSerializer(ModelSerializer):
fields = SOURCE_FORM_FIELDS + [ fields = SOURCE_FORM_FIELDS + [
"issuer", "issuer",
"sso_url", "sso_url",
"name_id_policy",
"binding_type", "binding_type",
"slo_url", "slo_url",
"temporary_user_delete_after", "temporary_user_delete_after",

View File

@ -23,6 +23,7 @@ class SAMLSourceForm(forms.ModelForm):
fields = SOURCE_FORM_FIELDS + [ fields = SOURCE_FORM_FIELDS + [
"issuer", "issuer",
"sso_url", "sso_url",
"name_id_policy",
"binding_type", "binding_type",
"slo_url", "slo_url",
"temporary_user_delete_after", "temporary_user_delete_after",

View File

@ -0,0 +1,40 @@
# Generated by Django 3.0.8 on 2020-07-08 13:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_sources_saml", "0004_auto_20200708_1207"),
]
operations = [
migrations.AddField(
model_name="samlsource",
name="name_id_policy",
field=models.TextField(
choices=[
("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", "Email"),
(
"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
"Persistent",
),
(
"urn:oasis:names:tc:SAML:2.0:nameid-format:X509SubjectName",
"X509",
),
(
"urn:oasis:names:tc:SAML:2.0:nameid-format:WindowsDomainQualifiedName",
"Windows",
),
(
"urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
"Transient",
),
],
default="urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
help_text="NameID Policy sent to the IdP. Can be unset, in which case no Policy is sent.",
),
),
]

View File

@ -7,6 +7,13 @@ from passbook.core.models import Source
from passbook.core.types import UILoginButton from passbook.core.types import UILoginButton
from passbook.crypto.models import CertificateKeyPair from passbook.crypto.models import CertificateKeyPair
from passbook.providers.saml.utils.time import timedelta_string_validator from passbook.providers.saml.utils.time import timedelta_string_validator
from passbook.sources.saml.processors.constants import (
SAML_NAME_ID_FORMAT_EMAIL,
SAML_NAME_ID_FORMAT_PRESISTENT,
SAML_NAME_ID_FORMAT_TRANSIENT,
SAML_NAME_ID_FORMAT_WINDOWS,
SAML_NAME_ID_FORMAT_X509,
)
class SAMLBindingTypes(models.TextChoices): class SAMLBindingTypes(models.TextChoices):
@ -17,6 +24,16 @@ class SAMLBindingTypes(models.TextChoices):
POST_AUTO = "POST_AUTO", _("POST Binding with auto-confirmation") POST_AUTO = "POST_AUTO", _("POST Binding with auto-confirmation")
class SAMLNameIDPolicy(models.TextChoices):
"""SAML NameID Policies"""
EMAIL = SAML_NAME_ID_FORMAT_EMAIL
PERSISTENT = SAML_NAME_ID_FORMAT_PRESISTENT
X509 = SAML_NAME_ID_FORMAT_X509
WINDOWS = SAML_NAME_ID_FORMAT_WINDOWS
TRANSIENT = SAML_NAME_ID_FORMAT_TRANSIENT
class SAMLSource(Source): class SAMLSource(Source):
"""Authenticate using an external SAML Identity Provider.""" """Authenticate using an external SAML Identity Provider."""
@ -31,6 +48,13 @@ class SAMLSource(Source):
verbose_name=_("SSO URL"), verbose_name=_("SSO URL"),
help_text=_("URL that the initial Login request is sent to."), help_text=_("URL that the initial Login request is sent to."),
) )
name_id_policy = models.TextField(
choices=SAMLNameIDPolicy.choices,
default=SAMLNameIDPolicy.TRANSIENT,
help_text=_(
"NameID Policy sent to the IdP. Can be unset, in which case no Policy is sent."
),
)
binding_type = models.CharField( binding_type = models.CharField(
max_length=100, max_length=100,
choices=SAMLBindingTypes.choices, choices=SAMLBindingTypes.choices,

View File

@ -127,6 +127,13 @@ class Processor:
def prepare_flow(self, request: HttpRequest) -> HttpResponse: def prepare_flow(self, request: HttpRequest) -> HttpResponse:
"""Prepare flow plan depending on whether or not the user exists""" """Prepare flow plan depending on whether or not the user exists"""
name_id = self._get_name_id() name_id = self._get_name_id()
# Sanity check, show a warning if NameIDPolicy doesn't match what we go
if self._source.name_id_policy != name_id.attrib["Format"]:
LOGGER.warning(
"NameID from IdP doesn't match our policy",
expected=self._source.name_id_policy,
got=name_id.attrib["Format"],
)
# transient NameIDs are handeled seperately as they don't have to go through flows. # transient NameIDs are handeled seperately as they don't have to go through flows.
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_TRANSIENT: if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_TRANSIENT:
return self._handle_name_id_transient(request) return self._handle_name_id_transient(request)

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<samlp:AuthnRequest AssertionConsumerServiceURL="{{ ACS_URL }}" <samlp:AuthnRequest AssertionConsumerServiceURL="{{ ACS_URL }}"
Destination="{{ DESTINATION }}" Destination="{{ DESTINATION }}"
ID="{{ AUTHN_REQUEST_ID }}" ID="{{ AUTHN_REQUEST_ID }}"
IssueInstant="{{ ISSUE_INSTANT }}" IssueInstant="{{ ISSUE_INSTANT }}"
@ -8,4 +8,5 @@
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"> xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">{{ ISSUER }}</saml:Issuer> <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">{{ ISSUER }}</saml:Issuer>
{{ AUTHN_REQUEST_SIGNATURE }} {{ AUTHN_REQUEST_SIGNATURE }}
<samlp:NameIDPolicy Format="{{ NAME_ID_POLICY }}"></samlp:NameIDPolicy>
</samlp:AuthnRequest> </samlp:AuthnRequest>

View File

@ -41,6 +41,7 @@ class InitiateView(View):
"AUTHN_REQUEST_ID": get_random_id(), "AUTHN_REQUEST_ID": get_random_id(),
"ISSUE_INSTANT": get_time_string(), "ISSUE_INSTANT": get_time_string(),
"ISSUER": get_issuer(request, source), "ISSUER": get_issuer(request, source),
"NAME_ID_POLICY": source.name_id_policy,
} }
authn_req = get_authnrequest_xml(parameters, signed=False) authn_req = get_authnrequest_xml(parameters, signed=False)
# If the source is configured for Redirect bindings, we can just redirect there # If the source is configured for Redirect bindings, we can just redirect there

View File

@ -6505,6 +6505,17 @@ definitions:
format: uri format: uri
maxLength: 200 maxLength: 200
minLength: 1 minLength: 1
name_id_policy:
title: Name id policy
description: NameID Policy sent to the IdP. Can be unset, in which case no
Policy is sent.
type: string
enum:
- urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
- urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
- urn:oasis:names:tc:SAML:2.0:nameid-format:X509SubjectName
- urn:oasis:names:tc:SAML:2.0:nameid-format:WindowsDomainQualifiedName
- urn:oasis:names:tc:SAML:2.0:nameid-format:transient
binding_type: binding_type:
title: Binding type title: Binding type
type: string type: string