diff --git a/README.rst b/README.rst index a7ccdadb..71b02c74 100644 --- a/README.rst +++ b/README.rst @@ -85,13 +85,13 @@ in the form of URL string which we use to redirect the user to our identity provider--OneLogin:: from BaseHTTPServer import BaseHTTPRequestHandler - from onelogin.saml import AuthnRequest + from onelogin.saml import AuthRequest ... class SampleAppHTTPRequestHandler(BaseHTTPRequestHandler): ... def do_GET(self): ... - url = AuthnRequest.create(**self.settings) + url = AuthRequest.create(**self.settings) self.send_response(301) self.send_header("Location", url) self.end_headers() @@ -135,7 +135,7 @@ of the public certificate originally obtained from OneLogin:: valid = res.is_valid() name_id = res.name_id if valid: - msg = 'The identity of {name_id} has been verified'.format( + msg = 'The identify of {name_id} has been verified'.format( name_id=name_id, ) self._serve_msg(200, msg) diff --git a/example.py b/example.py index bfcc58c8..b8a969d6 100644 --- a/example.py +++ b/example.py @@ -9,7 +9,7 @@ from BaseHTTPServer import BaseHTTPRequestHandler from BaseHTTPServer import HTTPServer -from onelogin.saml import AuthnRequest, Response +from onelogin.saml import AuthRequest, Response __version__ = '0.1' @@ -57,7 +57,7 @@ def do_GET(self): self._bad_request() return - url = AuthnRequest.create(**self.settings) + url = AuthRequest.create(**self.settings) self.send_response(301) self.send_header("Location", url) self.end_headers() @@ -78,7 +78,7 @@ def do_POST(self): valid = res.is_valid() name_id = res.name_id if valid: - msg = 'The identity of {name_id} has been verified'.format( + msg = 'The identify of {name_id} has been verified'.format( name_id=name_id, ) self._serve_msg(200, msg) diff --git a/onelogin/saml/AuthRequest.py b/onelogin/saml/AuthRequest.py index ae5387c0..c2f3f6df 100644 --- a/onelogin/saml/AuthRequest.py +++ b/onelogin/saml/AuthRequest.py @@ -2,6 +2,7 @@ import base64 import uuid import urllib +import urlparse from datetime import datetime from lxml import etree @@ -13,6 +14,7 @@ def create( _zlib=None, _base64=None, _urllib=None, + _as_url=True, **kwargs ): """Create a URL string which can be used to redirect a samlp:AuthnRequest to the identity provider. @@ -35,9 +37,15 @@ def create( if _urllib is None: _urllib = urllib - assertion_consumer_service_url = kwargs.pop( - 'assertion_consumer_service_url', - ) + assertion_consumer_service_url = None + assertion_consumer_service_index = "1" + if 'assertion_consumer_service_url' in kwargs: + assertion_consumer_service_url = kwargs.pop( + 'assertion_consumer_service_url', + ) + elif 'assertion_consumer_service_index' in kwargs: + assertion_consumer_service_index = "%s" % kwargs.pop('assertion_consumer_service_index') + issuer = kwargs.pop('issuer') name_identifier_format = kwargs.pop('name_identifier_format') idp_sso_target_url = kwargs.pop('idp_sso_target_url') @@ -47,10 +55,11 @@ def create( # http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf Section # 1.3.3 now = now.replace(microsecond=0) - now_iso = now.isoformat() + # Fix for ADFS parsing bug. Add 'Z' + now_iso = now.isoformat() + 'Z' unique_id = _uuid() - unique_id = unique_id.hex + unique_id = '_' + unique_id.hex[:-1] samlp_maker = ElementMaker( namespace='urn:oasis:names:tc:SAML:2.0:protocol', @@ -61,14 +70,23 @@ def create( nsmap=dict(saml='urn:oasis:names:tc:SAML:2.0:assertion'), ) - authn_request = samlp_maker.AuthnRequest( - ProtocolBinding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', - Version='2.0', - IssueInstant=now_iso, - ID=unique_id, - AssertionConsumerServiceURL=assertion_consumer_service_url, - ) - + if assertion_consumer_service_url: + authn_request = samlp_maker.AuthnRequest( + ProtocolBinding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + Version='2.0', + IssueInstant=now_iso, + ID=unique_id, + AssertionConsumerServiceURL=assertion_consumer_service_url, + ) + elif assertion_consumer_service_index: + authn_request = samlp_maker.AuthnRequest( + ProtocolBinding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + Version='2.0', + IssueInstant=now_iso, + ID=unique_id, + AssertionConsumerServiceIndex=assertion_consumer_service_index, + ) + saml_issuer = saml_maker.Issuer() saml_issuer.text = issuer authn_request.append(saml_issuer) @@ -77,7 +95,7 @@ def create( Format=name_identifier_format, AllowCreate='true', ) - authn_request.append(name_id_policy) + #authn_request.append(name_id_policy) request_authn_context = samlp_maker.RequestedAuthnContext( Comparison='exact', @@ -90,15 +108,20 @@ def create( ) request_authn_context.append(authn_context_class_ref) - compressed_request = _zlib.compress(etree.tostring(authn_request)) - # Strip the first 2 bytes (header) and the last 4 bytes (checksum) to get the raw deflate - deflated_request = compressed_request[2:-4] - encoded_request = _base64.b64encode(deflated_request) - urlencoded_request = _urllib.urlencode( - [('SAMLRequest', encoded_request)], - ) + if _as_url: + compressed_request = _zlib.compress(etree.tostring(authn_request)) + pu = urlparse.urlparse(idp_sso_target_url) + qsl = urlparse.parse_qsl(pu.query) + # Strip the first 2 bytes (header) and the last 4 bytes (checksum) to get the raw deflate + deflated_request = compressed_request[2:-4] + encoded_request = _base64.b64encode(deflated_request) + qsl.append(('SAMLRequest', encoded_request)) + qs = _urllib.urlencode(qsl) + pul = list(pu) + pul[4] = qs + return urlparse.urlunparse(pul) + else: + encoded_request = _base64.b64encode(etree.tostring(authn_request)) + return encoded_request + - return '{url}?{query}'.format( - url=idp_sso_target_url, - query=urlencoded_request, - ) diff --git a/onelogin/saml/Response.py b/onelogin/saml/Response.py index 20fb98c4..0113ff38 100644 --- a/onelogin/saml/Response.py +++ b/onelogin/saml/Response.py @@ -58,7 +58,10 @@ def __init__( self._signature = signature def _parse_datetime(self, dt): - return datetime.strptime(dt, '%Y-%m-%dT%H:%M:%SZ') + try: + return datetime.strptime(dt, '%Y-%m-%dT%H:%M:%SZ') + except ValueError: + return datetime.strptime(dt, '%Y-%m-%dT%H:%M:%S.%fZ') def _get_name_id(self): result = self._document.xpath( diff --git a/onelogin/saml/SignatureVerifier.py b/onelogin/saml/SignatureVerifier.py index 30731f6b..9dd9e8fd 100644 --- a/onelogin/saml/SignatureVerifier.py +++ b/onelogin/saml/SignatureVerifier.py @@ -120,6 +120,8 @@ def verify( cert_filename, '--id-attr:ID', 'urn:oasis:names:tc:SAML:2.0:assertion:Assertion', + '--id-attr:ID', + 'urn:oasis:names:tc:SAML:2.0:protocol:Response', xml_filename, ] diff --git a/onelogin/saml/test/TestAuthRequest.py b/onelogin/saml/test/TestAuthRequest.py index a3ede75d..72e9c501 100644 --- a/onelogin/saml/test/TestAuthRequest.py +++ b/onelogin/saml/test/TestAuthRequest.py @@ -3,9 +3,9 @@ from datetime import datetime from nose.tools import eq_ as eq -from onelogin.saml import AuthnRequest +from onelogin.saml import AuthRequest -class TestAuthnRequest(object): +class TestAuthRequest(object): def setUp(self): fudge.clear_expectations() @@ -24,7 +24,7 @@ def fake_clock(): fake_zlib.remember_order() fake_compress = fake_zlib.expects('compress') fake_compress.with_args( -"""foo_issuerurn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport""" +"""foo_issuerurn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport""" ) fake_compress.returns('HDfoo_compressedCHCK') @@ -42,7 +42,7 @@ def fake_clock(): ) fake_urlencode.returns('foo_urlencoded') - req = AuthnRequest.create( + req = AuthRequest.create( _clock=fake_clock, _uuid=fake_uuid_func, _zlib=fake_zlib,