snapkitty
cryptography
python
xml
vault-live / saml /sp /authn_request.py
SNAPKITTYWEST's picture
push from SNAPKITTYWEST/vault-live
ecf9cb9 verified
Raw History Blame Contribute Delete
3.62 kB
"""
saml/sp/authn_request.py
SAML AuthnRequest builder: XML construction, C14N, RSA-SHA256 signing,
Base64 + deflate encoding for HTTP-Redirect binding.
"""
import base64
import uuid
import zlib
from dataclasses import dataclass
from datetime import datetime, timezone
from xml.etree import ElementTree as ET
from crypto.xml_dsig import sign_element, exclusive_c14n
SAML_NS = "urn:oasis:names:tc:SAML:2.0:assertion"
SAMLP_NS = "urn:oasis:names:tc:SAML:2.0:protocol"
_SA = "{%s}" % SAML_NS
_SAP = "{%s}" % SAMLP_NS
REDIRECT_BINDING = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
POST_BINDING = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
@dataclass
class AuthnRequestConfig:
sp_entity_id: str
idp_sso_url: str
acs_url: str
private_key: object # RSAPrivateKey
certificate: object # x509.Certificate
@dataclass
class SignedRequest:
xml_element: ET.Element
redirect_url: str
request_id: str
post_value: str
class AuthnRequestBuilder:
def __init__(self, config: AuthnRequestConfig):
self.config = config
def build(self) -> ET.Element:
request_id = '_' + uuid.uuid4().hex
now = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
root = ET.Element(_SAP + 'AuthnRequest')
root.set('ID', request_id)
root.set('Version', '2.0')
root.set('IssueInstant', now)
root.set('Destination', self.config.idp_sso_url)
root.set('ProtocolBinding', POST_BINDING)
root.set('AssertionConsumerServiceURL', self.config.acs_url)
issuer = ET.SubElement(root, _SA + 'Issuer')
issuer.text = self.config.sp_entity_id
nameid_policy = ET.SubElement(root, _SAP + 'NameIDPolicy')
nameid_policy.set('AllowCreate', 'true')
nameid_policy.set('Format', 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient')
return root
def sign(self, element: ET.Element) -> ET.Element:
request_id = element.get('ID', '_unknown')
sign_element(element, self.config.private_key, self.config.certificate, request_id)
return element
def encode_redirect(self, element: ET.Element) -> str:
"""Deflate + Base64 + URL-encode for HTTP-Redirect binding SAMLRequest param."""
xml_bytes = ET.tostring(element, encoding='unicode').encode('utf-8')
compressed = zlib.compress(xml_bytes)[2:-4] # raw deflate (strip zlib header/trailer)
b64 = base64.b64encode(compressed).decode('ascii')
from urllib.parse import quote
return quote(b64)
def encode_post(self, element: ET.Element) -> str:
"""Base64-encode for HTTP-POST binding SAMLRequest field."""
xml_bytes = ET.tostring(element, encoding='unicode').encode('utf-8')
return base64.b64encode(xml_bytes).decode('ascii')
def build_and_sign(self) -> SignedRequest:
element = self.build()
request_id = element.get('ID', '_unknown')
self.sign(element)
redirect_encoded = self.encode_redirect(element)
post_encoded = self.encode_post(element)
redirect_url = (
f"{self.config.idp_sso_url}"
f"?SAMLRequest={redirect_encoded}"
)
return SignedRequest(
xml_element=element,
redirect_url=redirect_url,
request_id=request_id,
post_value=post_encoded,
)