providers/samlv2: remove SAMLv2 from master
This commit is contained in:
parent
cc0b8164b0
commit
bd40585247
|
@ -1,11 +0,0 @@
|
||||||
"""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/"
|
|
|
@ -1,15 +0,0 @@
|
||||||
"""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#"
|
|
||||||
|
|
||||||
REQ_KEY_REQUEST = "SAMLRequest"
|
|
||||||
REQ_KEY_SIGNATURE = "Signature"
|
|
||||||
|
|
||||||
SESSION_KEY = "passbook_saml_request"
|
|
||||||
|
|
||||||
SAML_ATTRIB_ACS_URL = "AssertionConsumerServiceURL"
|
|
||||||
SAML_ATTRIB_DESTINATION = "Destination"
|
|
||||||
SAML_ATTRIB_ID = "ID"
|
|
||||||
SAML_ATTRIB_ISSUE_INSTANT = "IssueInstant"
|
|
||||||
SAML_ATTRIB_PROTOCOL_BINDING = "ProtocolBinding"
|
|
|
@ -1,83 +0,0 @@
|
||||||
"""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)
|
|
|
@ -1,5 +0,0 @@
|
||||||
"""SAML Provider logic"""
|
|
||||||
|
|
||||||
|
|
||||||
class SAMLProvider:
|
|
||||||
"""SAML Provider"""
|
|
|
@ -1,24 +0,0 @@
|
||||||
"""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", "")
|
|
|
@ -1,35 +0,0 @@
|
||||||
"""passbook samlv2 URLs"""
|
|
||||||
from django.urls import path
|
|
||||||
|
|
||||||
from passbook.providers.samlv2.views import authorize, idp_initiated, slo, sso
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path(
|
|
||||||
"<slug:app_slug>/authorize/",
|
|
||||||
authorize.AuthorizeView.as_view(),
|
|
||||||
name="authorize",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"<slug:app_slug>/sso/redirect/",
|
|
||||||
sso.SAMLRedirectBindingView.as_view(),
|
|
||||||
name="sso-redirect",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"<slug:app_slug>/sso/post/", sso.SAMLPostBindingView.as_view(), name="sso-post",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"<slug:app_slug>/slo/redirect/",
|
|
||||||
slo.SAMLRedirectBindingView.as_view(),
|
|
||||||
name="slo-redirect",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"<slug:app_slug>/slo/redirect/",
|
|
||||||
slo.SAMLPostBindingView.as_view(),
|
|
||||||
name="slo-post",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"<slug:app_slug>/initiate/",
|
|
||||||
idp_initiated.IDPInitiatedView.as_view(),
|
|
||||||
name="initiate",
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,6 +0,0 @@
|
||||||
"""SAML Provider authorization view"""
|
|
||||||
from django.views.generic import FormView
|
|
||||||
|
|
||||||
|
|
||||||
class AuthorizeView(FormView):
|
|
||||||
"""Authorization view"""
|
|
|
@ -1,31 +0,0 @@
|
||||||
"""SAML base views"""
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from django.views import View
|
|
||||||
|
|
||||||
from passbook.core.models import Application
|
|
||||||
from passbook.policies.mixins import PolicyAccessMixin
|
|
||||||
from passbook.providers.samlv2.saml.constants import SESSION_KEY
|
|
||||||
from passbook.providers.samlv2.saml.parser import SAMLRequest
|
|
||||||
|
|
||||||
|
|
||||||
class BaseSAMLView(PolicyAccessMixin, View):
|
|
||||||
"""Base SAML View to resolve app_slug"""
|
|
||||||
|
|
||||||
application: Application
|
|
||||||
|
|
||||||
def setup(self, request: HttpRequest, *args, **kwargs):
|
|
||||||
View.setup(self, request, *args, **kwargs)
|
|
||||||
self.application = self.get_application(self.kwargs.get("app_slug"))
|
|
||||||
|
|
||||||
def get_application(self, app_slug: str) -> Optional[Application]:
|
|
||||||
"""Return application or raise 404"""
|
|
||||||
return get_object_or_404(Application, slug=app_slug)
|
|
||||||
|
|
||||||
def handle_saml_request(self, request: SAMLRequest) -> HttpResponse:
|
|
||||||
"""Handle SAML Request"""
|
|
||||||
self.request.SESSION[SESSION_KEY] = request
|
|
||||||
if self.application.skip_authorization:
|
|
||||||
pass
|
|
|
@ -1,6 +0,0 @@
|
||||||
"""IDP-Initiated Views"""
|
|
||||||
from django.views import View
|
|
||||||
|
|
||||||
|
|
||||||
class IDPInitiatedView(View):
|
|
||||||
"""IDP-initiated Handler"""
|
|
|
@ -1,10 +0,0 @@
|
||||||
"""Single Logout Views"""
|
|
||||||
from django.views import View
|
|
||||||
|
|
||||||
|
|
||||||
class SAMLPostBindingView(View):
|
|
||||||
"""Handle SAML POST-type Requests"""
|
|
||||||
|
|
||||||
|
|
||||||
class SAMLRedirectBindingView(View):
|
|
||||||
"""Handle SAML Redirect-type Requests"""
|
|
|
@ -1,41 +0,0 @@
|
||||||
"""Single Signon Views"""
|
|
||||||
from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest
|
|
||||||
|
|
||||||
from passbook.providers.samlv2.saml.constants import REQ_KEY_REQUEST, REQ_KEY_SIGNATURE
|
|
||||||
from passbook.providers.samlv2.saml.parser import SAMLRequest
|
|
||||||
from passbook.providers.samlv2.views.base import BaseSAMLView
|
|
||||||
|
|
||||||
# SAML Authentication flow in passbook
|
|
||||||
# - Parse and Verify SAML Request
|
|
||||||
# - Check access to application (this is done after parsing as it might take a few seconds)
|
|
||||||
# - Ask for user authorization (if required from Application)
|
|
||||||
# - Log Access to audit log
|
|
||||||
# - Create response with unique ID to protect against replay
|
|
||||||
|
|
||||||
|
|
||||||
class SAMLPostBindingView(BaseSAMLView):
|
|
||||||
"""Handle SAML POST-type Requests"""
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
def post(self, request: HttpRequest, app_slug: str) -> HttpResponse:
|
|
||||||
"""Handle POST Requests"""
|
|
||||||
if REQ_KEY_REQUEST not in request.POST:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
raw_saml_request = request.POST.get(REQ_KEY_REQUEST)
|
|
||||||
detached_signature = request.POST.get(REQ_KEY_SIGNATURE, None)
|
|
||||||
srq = SAMLRequest.parse(raw_saml_request, detached_signature)
|
|
||||||
return self.handle_saml_request(srq)
|
|
||||||
|
|
||||||
|
|
||||||
class SAMLRedirectBindingView(BaseSAMLView):
|
|
||||||
"""Handle SAML Redirect-type Requests"""
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
def get(self, request: HttpRequest, app_slug: str) -> HttpResponse:
|
|
||||||
"""Handle GET Requests"""
|
|
||||||
if REQ_KEY_REQUEST not in request.GET:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
raw_saml_request = request.GET.get(REQ_KEY_REQUEST)
|
|
||||||
detached_signature = request.GET.get(REQ_KEY_SIGNATURE, None)
|
|
||||||
srq = SAMLRequest.parse(raw_saml_request, detached_signature)
|
|
||||||
return self.handle_saml_request(srq)
|
|
|
@ -92,7 +92,6 @@ INSTALLED_APPS = [
|
||||||
"passbook.providers.oauth.apps.PassbookProviderOAuthConfig",
|
"passbook.providers.oauth.apps.PassbookProviderOAuthConfig",
|
||||||
"passbook.providers.oidc.apps.PassbookProviderOIDCConfig",
|
"passbook.providers.oidc.apps.PassbookProviderOIDCConfig",
|
||||||
"passbook.providers.saml.apps.PassbookProviderSAMLConfig",
|
"passbook.providers.saml.apps.PassbookProviderSAMLConfig",
|
||||||
"passbook.providers.samlv2.apps.PassbookProviderSAMLv2Config",
|
|
||||||
"passbook.recovery.apps.PassbookRecoveryConfig",
|
"passbook.recovery.apps.PassbookRecoveryConfig",
|
||||||
"passbook.sources.ldap.apps.PassbookSourceLDAPConfig",
|
"passbook.sources.ldap.apps.PassbookSourceLDAPConfig",
|
||||||
"passbook.sources.oauth.apps.PassbookSourceOAuthConfig",
|
"passbook.sources.oauth.apps.PassbookSourceOAuthConfig",
|
||||||
|
|
Reference in a new issue