providers/samlv2: start implementing new SAML Provider

This commit is contained in:
Jens Langhammer 2020-05-07 01:20:08 +02:00
parent 2e9496bb74
commit b40bffdf38
13 changed files with 187 additions and 0 deletions

View File

View File

@ -0,0 +1,11 @@
"""passbook saml provider app config"""
from django.apps import AppConfig
class PassbookProviderSAMLv2Config(AppConfig):
"""passbook samlv2 provider app config"""
name = "passbook.providers.samlv2"
label = "passbook_providers_samlv2"
verbose_name = "passbook Providers.SAMLv2"
mountpoint = "application/samlv2/"

View File

View File

@ -0,0 +1,10 @@
"""SAML-related constants"""
NS_SAML_PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
NS_SAML_ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
NS_SIGNATURE = "http://www.w3.org/2000/09/xmldsig#"
SAML_ATTRIB_ACS_URL = "AssertionConsumerServiceURL"
SAML_ATTRIB_DESTINATION = "Destination"
SAML_ATTRIB_ID = "ID"
SAML_ATTRIB_ISSUE_INSTANT = "IssueInstant"
SAML_ATTRIB_PROTOCOL_BINDING = "ProtocolBinding"

View File

@ -0,0 +1,83 @@
"""SAML Request Parse/builder"""
from typing import TYPE_CHECKING, Optional
from defusedxml import ElementTree
from signxml import XMLVerifier
from passbook.crypto.models import CertificateKeyPair
from passbook.providers.samlv2.saml.constants import (
NS_SAML_ASSERTION,
NS_SAML_PROTOCOL,
SAML_ATTRIB_ACS_URL,
SAML_ATTRIB_DESTINATION,
SAML_ATTRIB_ID,
SAML_ATTRIB_ISSUE_INSTANT,
SAML_ATTRIB_PROTOCOL_BINDING,
)
from passbook.providers.samlv2.saml.utils import decode_base64_and_inflate
if TYPE_CHECKING:
from xml.etree.ElementTree import Element # nosec
# pylint: disable=too-many-instance-attributes
class SAMLRequest:
"""SAML Request data class, parse raw base64-encoded data, checks signature and more"""
_root: "Element"
acs_url: str
destination: str
id: str
issue_instant: str
protocol_binding: str
issuer: str
is_signed: bool
_detached_signature: str
def __init__(self):
self.acs_url = ""
self.destination = ""
# pylint: disable=invalid-name
self.id = ""
self.issue_instant = ""
self.protocol_binding = ""
@staticmethod
def parse(raw: str, detached_signature: Optional[str] = None) -> "SAMLRequest":
"""Prase SAML request from raw string, which can be base64 encoded and deflated.
Optionally accepts a detached_signature, as from a REDIRECT request."""
decoded_xml = decode_base64_and_inflate(raw)
root = ElementTree.fromstring(decoded_xml)
req = SAMLRequest()
req._root = root # pylint: disable=protected-access
# Verify the root element's tag
_expected_tag = f"{{{NS_SAML_PROTOCOL}}}AuthnRequest"
if root.tag != _expected_tag:
raise ValueError(
f"Invalid root tag, got '{root.tag}', expected '{_expected_tag}."
)
req.acs_url = root.attrib[SAML_ATTRIB_ACS_URL]
req.destination = root.attrib[SAML_ATTRIB_DESTINATION]
req.id = root.attrib[SAML_ATTRIB_ID]
req.issue_instant = root.attrib[SAML_ATTRIB_ISSUE_INSTANT]
req.protocol_binding = root.attrib[SAML_ATTRIB_PROTOCOL_BINDING]
req.issuer = root.find(f"{{{NS_SAML_ASSERTION}}}Issuer").text
# Check if this Request is signed
if detached_signature:
# pylint: disable=protected-access
req._detached_signature = detached_signature
return req
def verify_signature(self, keypair: CertificateKeyPair):
"""Verify signature of SAML Request.
Raises `cryptography.exceptions.InvalidSignature` on validaton failure."""
verifier = XMLVerifier()
if self._detached_signature:
verifier.verify(
self._detached_signature, x509_cert=keypair.certificate_data
)
else:
verifier.verify(self._root, x509_cert=keypair.certificate_data)

View File

@ -0,0 +1,24 @@
"""Wrappers to de/encode and de/inflate strings"""
import base64
import zlib
def decode_base64_and_inflate(encoded: str, encoding="utf-8") -> str:
"""Base64 decode and ZLib decompress b64string"""
decoded_data = base64.b64decode(encoded)
try:
return zlib.decompress(decoded_data, -15).decode(encoding)
except zlib.error:
return decoded_data.decode(encoding)
def deflate_and_base64_encode(inflated: bytes, encoding="utf-8"):
"""Base64 and ZLib Compress b64string"""
zlibbed_str = zlib.compress(inflated)
compressed_string = zlibbed_str[2:-4]
return base64.b64encode(compressed_string).decode(encoding)
def nice64(src):
""" Returns src base64-encoded and formatted nicely for our XML. """
return base64.b64encode(src).decode("utf-8").replace("\n", "")

View File

@ -0,0 +1,32 @@
"""passbook samlv2 URLs"""
from django.urls import path
from passbook.providers.samlv2.views import idp_initiated, slo, sso
urlpatterns = [
path(
"<slug:application>/sso/redirect/",
sso.SAMLRedirectBindingView.as_view(),
name="sso-redirect",
),
path(
"<slug:application>/sso/post/",
sso.SAMLPostBindingView.as_view(),
name="sso-post",
),
path(
"<slug:application>/slo/redirect/",
slo.SAMLRedirectBindingView.as_view(),
name="slo-redirect",
),
path(
"<slug:application>/slo/redirect/",
slo.SAMLPostBindingView.as_view(),
name="slo-post",
),
path(
"<slug:application>/initiate/",
idp_initiated.IDPInitiatedView.as_view(),
name="initiate",
),
]

View File

@ -0,0 +1,6 @@
"""IDP-Initiated Views"""
from django.views import View
class IDPInitiatedView(View):
"""IDP-initiated Handler"""

View File

@ -0,0 +1,10 @@
"""Single Logout Views"""
from django.views import View
class SAMLPostBindingView(View):
"""Handle SAML POST-type Requests"""
class SAMLRedirectBindingView(View):
"""Handle SAML Redirect-type Requests"""

View File

@ -0,0 +1,10 @@
"""Single Signon Views"""
from django.views import View
class SAMLPostBindingView(View):
"""Handle SAML POST-type Requests"""
class SAMLRedirectBindingView(View):
"""Handle SAML Redirect-type Requests"""

View File

@ -93,6 +93,7 @@ INSTALLED_APPS = [
"passbook.providers.oauth.apps.PassbookProviderOAuthConfig",
"passbook.providers.oidc.apps.PassbookProviderOIDCConfig",
"passbook.providers.saml.apps.PassbookProviderSAMLConfig",
"passbook.providers.samlv2.apps.PassbookProviderSAMLv2Config",
"passbook.factors.otp.apps.PassbookFactorOTPConfig",
"passbook.factors.captcha.apps.PassbookFactorCaptchaConfig",
"passbook.factors.password.apps.PassbookFactorPasswordConfig",