diff --git a/.gitignore b/.gitignore
index ad382d79..80fc9494 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,3 +26,6 @@ __pycache_
settings.py
advanced_settings.py
+
+# Any test files / output that is generated by tests
+tests/sample_output
diff --git a/README.md b/README.md
index 7a10e277..22149521 100644
--- a/README.md
+++ b/README.md
@@ -167,6 +167,10 @@ The previous line will run the tests for the whole toolkit. You can also run the
python setup.py test --test-suite tests.src.OneLogin.saml2_tests.auth_test.OneLogin_Saml2_Auth_Test
```
+```
+python setup.py test --test-suite tests.src.OneLogin.saml2_tests.authn_request_test.OneLogin_Saml2_Authn_Request_Test
+```
+
With the --test-suite parameter you can specify the module to test. You'll find all the module available and their class names at tests/src/OneLogin/saml2_tests/
### How it works ###
diff --git a/setup.py b/setup.py
index faf8612b..073f87f9 100644
--- a/setup.py
+++ b/setup.py
@@ -36,6 +36,7 @@
'dm.xmlsec.binding==1.3.2',
'isodate==0.5.0',
'defusedxml==0.4.1',
+ 'Jinja2==2.7.3',
],
extras_require={
'test': (
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index 49cbfbce..319427c6 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -11,8 +11,11 @@
"""
+import logging
from base64 import b64encode
from urllib import quote_plus
+from os.path import dirname, join
+from jinja2 import Template
import dm.xmlsec.binding as xmlsec
@@ -25,6 +28,8 @@
from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request
from onelogin.saml2.authn_request import OneLogin_Saml2_Authn_Request
+log = logging.getLogger(__name__)
+
class OneLogin_Saml2_Auth(object):
"""
@@ -276,6 +281,27 @@ def login(self, return_to=None, force_authn=False, is_passive=False):
if security.get('authnRequestsSigned', False):
parameters['SigAlg'] = security['signatureAlgorithm']
parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState'], security['signatureAlgorithm'])
+
+ # HTTP-POST binding requires generation of a form
+ if self.get_settings().get_idp_data()['singleSignOnService'].get('binding', None) == 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST':
+ log.debug("Generating AuthnRequest HTTP-POST binding form")
+
+ # Return HTML form
+ template_file = open(join(dirname(__file__), 'templates/authn_request.html'))
+ template_text = template_file.read()
+ template = Template(template_text)
+
+ context = {
+ 'sso_url': self.get_sso_url(),
+ 'saml_request': saml_request,
+ 'relay_state': parameters['RelayState']
+ }
+
+ html = template.render(context)
+ log.debug("Generated HTML: %s" % html)
+
+ return html
+
return self.redirect_to(self.get_sso_url(), parameters)
def logout(self, return_to=None, name_id=None, session_index=None):
diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py
index 8e8495e7..ccd468ab 100644
--- a/src/onelogin/saml2/authn_request.py
+++ b/src/onelogin/saml2/authn_request.py
@@ -8,15 +8,25 @@
AuthNRequest class of OneLogin's Python Toolkit.
"""
+import logging
from base64 import b64encode
from zlib import compress
from onelogin.saml2.utils import OneLogin_Saml2_Utils
from onelogin.saml2.constants import OneLogin_Saml2_Constants
+from onelogin.saml2.errors import OneLogin_Saml2_Error
+
+import dm.xmlsec.binding as xmlsec
+from dm.xmlsec.binding.tmpl import Signature
+
+from lxml.etree import tostring, fromstring
+
+log = logging.getLogger(__name__)
class OneLogin_Saml2_Authn_Request(object):
+
"""
This class handles an AuthNRequest. It builds an
@@ -97,6 +107,8 @@ def __init__(self, settings, force_authn=False, is_passive=False):
ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
AssertionConsumerServiceURL="%(assertion_url)s">
%(entity_id)s
+
+
@@ -115,7 +127,77 @@ def __init__(self, settings, force_authn=False, is_passive=False):
'requested_authn_context_str': requested_authn_context_str,
}
- self.__authn_request = request
+ # Only the urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST binding gets the enveloped signature
+ if settings.get_idp_data()['singleSignOnService'].get('binding', None) == 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' and security['authnRequestsSigned'] is True:
+
+ log.debug("Generating AuthnRequest using urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST binding")
+
+ xmlsec.initialize()
+ xmlsec.set_error_callback(self.print_xmlsec_errors)
+
+ signature = Signature(xmlsec.TransformExclC14N, xmlsec.TransformRsaSha1)
+
+ doc = fromstring(request)
+
+ # ID attributes different from xml:id must be made known by the application through a call
+ # to the addIds(node, ids) function defined by xmlsec.
+ xmlsec.addIDs(doc, ['ID'])
+
+ doc.insert(0, signature)
+
+ ref = signature.addReference(xmlsec.TransformSha1, uri="#%s" % uid)
+ ref.addTransform(xmlsec.TransformEnveloped)
+ ref.addTransform(xmlsec.TransformExclC14N)
+
+ key_info = signature.ensureKeyInfo()
+ key_info.addKeyName()
+ key_info.addX509Data()
+
+ # Load the key into the xmlsec context
+ key = settings.get_sp_key()
+ if not key:
+ raise OneLogin_Saml2_Error("Attempt to sign the AuthnRequest but unable to load the SP private key")
+
+ dsig_ctx = xmlsec.DSigCtx()
+
+ sign_key = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None)
+
+ from tempfile import NamedTemporaryFile
+ cert_file = NamedTemporaryFile(delete=True)
+ cert_file.write(settings.get_sp_cert())
+ cert_file.seek(0)
+
+ sign_key.loadCert(cert_file.name, xmlsec.KeyDataFormatPem)
+
+ dsig_ctx.signKey = sign_key
+
+ # Note: the assignment below effectively copies the key
+ dsig_ctx.sign(signature)
+
+ self.__authn_request = tostring(doc)
+ log.debug("Generated AuthnRequest: {}".format(self.__authn_request))
+
+ else:
+ self.__authn_request = request
+
+ def print_xmlsec_errors(self, filename, line, func, errorObject, errorSubject, reason, msg):
+ # this would give complete but often not very usefull) information
+ print "%(filename)s:%(line)d(%(func)s) error %(reason)d obj=%(errorObject)s subject=%(errorSubject)s: %(msg)s" % locals()
+ # the following prints if we get something with relation to the application
+
+ info = []
+
+ if errorObject != "unknown":
+ info.append("obj=" + errorObject)
+
+ if errorSubject != "unknown":
+ info.append("subject=" + errorSubject)
+
+ if msg.strip():
+ info.append("msg=" + msg)
+
+ if info:
+ print "%s:%d(%s)" % (filename, line, func), " ".join(info)
def get_request(self):
"""
diff --git a/src/onelogin/saml2/templates/authn_request.html b/src/onelogin/saml2/templates/authn_request.html
new file mode 100644
index 00000000..5fd0c375
--- /dev/null
+++ b/src/onelogin/saml2/templates/authn_request.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/certs/example.com/example.privatekey b/tests/certs/example.com/example.privatekey
new file mode 100644
index 00000000..42af7266
--- /dev/null
+++ b/tests/certs/example.com/example.privatekey
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpgIBAAKCAQEA65E2/lZg54UVMrgjvoun1iSHsvYpcCzMPqP+00jPKqgeTwmW
+1Z3j5nfez24WpFUaqGBXzaWzGw9UIxV1yApKJVVUtrZkRrBpRPZ9fz/XoJ1Zq3wc
+ka1VYiXeBBBtxWKipqXxmj11ISRqrGF13b+2Yt2XAvqSg6zI4kH7/zJWbB3GdRQW
+zpOSknv2/Eac5LWxeAKoY4jv146bJFCFTJVHMqg5U5C2MVNbjSFPUSRGNP/Sm5v1
+U90jBY4aTE21NeA5hgES17C2Yno7P3OjumL89QiX/KQs1AUfErFA9BVGIJt0dTQa
+gIDTzCXjQ1AMeqpbvfEWIxJ8gVcik/9E4dAYLQIDAQABAoIBAQDGz40ZRK+OVkxY
+vP4138nrunLogEbizHwoVeJIUYe+mZrS2+X4LcRdC0f5yxDC6qyP9JfGERXDPcGl
+xoPcK4r+TTEs72xcGKEPufSaw7fpb0NxrlKyRBbuucTRq0fpseBSQ3VP1pSXPxPk
+nnCKkTWN5TSBKBclmFsGUegrLkGwBiakiIYy7TrNSzQ7tFI/jOHP58b1oN4sWQMr
+qyBIADQgR1G+r+iRjrKiMQ0HFrn3cBIh/pOb9e/R/PzuzjluKR21cbUL9U2VBmID
+mku/JdoVef8vadWO8wF+AsMkHbT6EpWqJnmzm9Yc1d7OnGdFYiqJOfx5jwe0hO4I
+WEV1Mz1BAoGBAPd7wjNAr69/BE2zo4gUYJiELQsvF+cD00kr4wjkYuuwlJkfrGX5
+ywxkpE0o2wzc2FirviHQizDmv0L9NfkEO+Qy9XfOYL1kJ2oV9Yw/Mp1dcDyap4bg
++NTcx65PQJJTB0fr3DGBWAWFko2hTKOTbgF+4DO8lO41H2GqpkSoUQSdAoGBAPOs
+epUxnMAKnQKCjJ/pMgf+vtMYg8VkAtvLrd8bRCmHWKhrBHObojorQx2fGic22Sb/
+B8jFzK6Y61Q9LiBJDLoU3acgkq76x0C71e6YwvGVXSgw6FRuTIsmt10PzqOFfUFn
+Bfq2t92PYnJXJyx8RtFRl0Z1aiSqmxzpvon/9mTRAoGBAMOYGEARm8iEBo6yr0hZ
+co6XyFHSgn2eVFq8SM86UcQc5xSuJ77g0U2WLRSeeaGM2aAa/EYVYCzh8b+sCAAr
+DHqqm754aZTFlzEM8ehJ+mLM+murf0PmgkMZyudE06/R1ytMidbGdx7GFrHBDaUq
+XALql5/MJ5ise4ThLk+NB5sxAoGBALMoOESjYoWMCB7FT5FvSjq4oSLh3lhuDO//
+lAn6qSYDfjrt3CsH3cH49vK7fOYiHIzga5/BVpl0k2mvRc+1Bed22fU8LLz8Yy2E
+LWms5X/r+r9HHjqdkiepQp3otlxiFFLW5X2NhCgheRdqXsIFaagS3i+OuojU6xDa
+Bx69lDJRAoGBAJFMcGCk280m3s3IqoiXOsyOdcvQ6EKOltJrA1PIyI6S8KeWW1su
+66hnxa2ffAw34uiKA0WscyzFCOdXpohOxCnWx0eANpWpQieP41izNYmUURWa5+r1
+qr0BfgdzOPAn2Wa0bUBHBGM7g5XrwLtvaUkFdy6USHXXzN08oPT8Wx1A
+-----END RSA PRIVATE KEY-----
diff --git a/tests/certs/example.com/example.pubkey b/tests/certs/example.com/example.pubkey
new file mode 100644
index 00000000..ee782d87
--- /dev/null
+++ b/tests/certs/example.com/example.pubkey
@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA65E2/lZg54UVMrgjvoun
+1iSHsvYpcCzMPqP+00jPKqgeTwmW1Z3j5nfez24WpFUaqGBXzaWzGw9UIxV1yApK
+JVVUtrZkRrBpRPZ9fz/XoJ1Zq3wcka1VYiXeBBBtxWKipqXxmj11ISRqrGF13b+2
+Yt2XAvqSg6zI4kH7/zJWbB3GdRQWzpOSknv2/Eac5LWxeAKoY4jv146bJFCFTJVH
+Mqg5U5C2MVNbjSFPUSRGNP/Sm5v1U90jBY4aTE21NeA5hgES17C2Yno7P3OjumL8
+9QiX/KQs1AUfErFA9BVGIJt0dTQagIDTzCXjQ1AMeqpbvfEWIxJ8gVcik/9E4dAY
+LQIDAQAB
+-----END PUBLIC KEY-----
diff --git a/tests/settings/example_settings_http_post_binding.json b/tests/settings/example_settings_http_post_binding.json
new file mode 100644
index 00000000..13eb47ec
--- /dev/null
+++ b/tests/settings/example_settings_http_post_binding.json
@@ -0,0 +1,59 @@
+{
+ "comment": "This settings file uses the HTTP-POST binding for the assertionConsumerService.",
+ "strict": false,
+ "debug": false,
+ "sp": {
+ "entityId": "sp.example.com",
+ "assertionConsumerService": {
+ "url": "https://sp.example.com/saml/?acs",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+ },
+ "singleLogoutService": {
+ "url": "https://sp.example.com/saml?sls",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ },
+ "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified",
+ "x509cert": "MIIDuTCCAqGgAwIBAgIJALO8tfVURFsvMA0GCSqGSIb3DQEBCwUAMHMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxFDASBgNVBAMMC2V4YW1wbGUuY29tMB4XDTE1MDgwODE4NDQ1OVoXDTI1MDgwNTE4NDQ1OVowczELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDrkTb+VmDnhRUyuCO+i6fWJIey9ilwLMw+o/7TSM8qqB5PCZbVnePmd97PbhakVRqoYFfNpbMbD1QjFXXICkolVVS2tmRGsGlE9n1/P9egnVmrfByRrVViJd4EEG3FYqKmpfGaPXUhJGqsYXXdv7Zi3ZcC+pKDrMjiQfv/MlZsHcZ1FBbOk5KSe/b8RpzktbF4AqhjiO/XjpskUIVMlUcyqDlTkLYxU1uNIU9RJEY0/9Kbm/VT3SMFjhpMTbU14DmGARLXsLZiejs/c6O6Yvz1CJf8pCzUBR8SsUD0FUYgm3R1NBqAgNPMJeNDUAx6qlu98RYjEnyBVyKT/0Th0BgtAgMBAAGjUDBOMB0GA1UdDgQWBBRghqUeLjDqMaJikgHWgxYQnQm1azAfBgNVHSMEGDAWgBRghqUeLjDqMaJikgHWgxYQnQm1azAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAs0h0MFMDme/07Gj5TJkaxQLWiacNhqmqEa3Mt1C3k2Wva6OwAAqMMWcAQMdmACg8Qk0xjLBizs7go7QvkmV+RNbHDBdD0p00GRrBj1XTPR4VJiJJvOmY2G7A084lx0M9nOGHtQRLgs116TNOTMHz3rPDeN0SdQJien1AAKa4JhcWtuC0yBlPXtx9nQ4TSrCTgOEOnYfYjh+WKD/ZGUeYzmNlT9Sl6aPjjbpSBMzZzckhxDAdQrveTKb75z8BFr+60X3f76qCO7xsoCCUJfybsgiUZvp6s7qcAkJNnWeIKWao5XEkAOvr6Kry/oncaUNm6Q1LRKgXmB8FiIx3i3xdP",
+ "privateKey": "MIIEpgIBAAKCAQEA65E2/lZg54UVMrgjvoun1iSHsvYpcCzMPqP+00jPKqgeTwmW1Z3j5nfez24WpFUaqGBXzaWzGw9UIxV1yApKJVVUtrZkRrBpRPZ9fz/XoJ1Zq3wcka1VYiXeBBBtxWKipqXxmj11ISRqrGF13b+2Yt2XAvqSg6zI4kH7/zJWbB3GdRQWzpOSknv2/Eac5LWxeAKoY4jv146bJFCFTJVHMqg5U5C2MVNbjSFPUSRGNP/Sm5v1U90jBY4aTE21NeA5hgES17C2Yno7P3OjumL89QiX/KQs1AUfErFA9BVGIJt0dTQagIDTzCXjQ1AMeqpbvfEWIxJ8gVcik/9E4dAYLQIDAQABAoIBAQDGz40ZRK+OVkxYvP4138nrunLogEbizHwoVeJIUYe+mZrS2+X4LcRdC0f5yxDC6qyP9JfGERXDPcGlxoPcK4r+TTEs72xcGKEPufSaw7fpb0NxrlKyRBbuucTRq0fpseBSQ3VP1pSXPxPknnCKkTWN5TSBKBclmFsGUegrLkGwBiakiIYy7TrNSzQ7tFI/jOHP58b1oN4sWQMrqyBIADQgR1G+r+iRjrKiMQ0HFrn3cBIh/pOb9e/R/PzuzjluKR21cbUL9U2VBmIDmku/JdoVef8vadWO8wF+AsMkHbT6EpWqJnmzm9Yc1d7OnGdFYiqJOfx5jwe0hO4IWEV1Mz1BAoGBAPd7wjNAr69/BE2zo4gUYJiELQsvF+cD00kr4wjkYuuwlJkfrGX5ywxkpE0o2wzc2FirviHQizDmv0L9NfkEO+Qy9XfOYL1kJ2oV9Yw/Mp1dcDyap4bg+NTcx65PQJJTB0fr3DGBWAWFko2hTKOTbgF+4DO8lO41H2GqpkSoUQSdAoGBAPOsepUxnMAKnQKCjJ/pMgf+vtMYg8VkAtvLrd8bRCmHWKhrBHObojorQx2fGic22Sb/B8jFzK6Y61Q9LiBJDLoU3acgkq76x0C71e6YwvGVXSgw6FRuTIsmt10PzqOFfUFnBfq2t92PYnJXJyx8RtFRl0Z1aiSqmxzpvon/9mTRAoGBAMOYGEARm8iEBo6yr0hZco6XyFHSgn2eVFq8SM86UcQc5xSuJ77g0U2WLRSeeaGM2aAa/EYVYCzh8b+sCAArDHqqm754aZTFlzEM8ehJ+mLM+murf0PmgkMZyudE06/R1ytMidbGdx7GFrHBDaUqXALql5/MJ5ise4ThLk+NB5sxAoGBALMoOESjYoWMCB7FT5FvSjq4oSLh3lhuDO//lAn6qSYDfjrt3CsH3cH49vK7fOYiHIzga5/BVpl0k2mvRc+1Bed22fU8LLz8Yy2ELWms5X/r+r9HHjqdkiepQp3otlxiFFLW5X2NhCgheRdqXsIFaagS3i+OuojU6xDaBx69lDJRAoGBAJFMcGCk280m3s3IqoiXOsyOdcvQ6EKOltJrA1PIyI6S8KeWW1su66hnxa2ffAw34uiKA0WscyzFCOdXpohOxCnWx0eANpWpQieP41izNYmUURWa5+r1qr0BfgdzOPAn2Wa0bUBHBGM7g5XrwLtvaUkFdy6USHXXzN08oPT8Wx1A"
+ },
+ "idp": {
+ "entityId": "idp.example.com",
+ "singleSignOnService": {
+ "url": "https://idp.example.com/login",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+ },
+ "singleLogoutService": {
+ "url": "https://idp.example.com/sls",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ },
+ "x509cert": "MIIDuTCCAqGgAwIBAgIJALO8tfVURFsvMA0GCSqGSIb3DQEBCwUAMHMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxFDASBgNVBAMMC2V4YW1wbGUuY29tMB4XDTE1MDgwODE4NDQ1OVoXDTI1MDgwNTE4NDQ1OVowczELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDrkTb+VmDnhRUyuCO+i6fWJIey9ilwLMw+o/7TSM8qqB5PCZbVnePmd97PbhakVRqoYFfNpbMbD1QjFXXICkolVVS2tmRGsGlE9n1/P9egnVmrfByRrVViJd4EEG3FYqKmpfGaPXUhJGqsYXXdv7Zi3ZcC+pKDrMjiQfv/MlZsHcZ1FBbOk5KSe/b8RpzktbF4AqhjiO/XjpskUIVMlUcyqDlTkLYxU1uNIU9RJEY0/9Kbm/VT3SMFjhpMTbU14DmGARLXsLZiejs/c6O6Yvz1CJf8pCzUBR8SsUD0FUYgm3R1NBqAgNPMJeNDUAx6qlu98RYjEnyBVyKT/0Th0BgtAgMBAAGjUDBOMB0GA1UdDgQWBBRghqUeLjDqMaJikgHWgxYQnQm1azAfBgNVHSMEGDAWgBRghqUeLjDqMaJikgHWgxYQnQm1azAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAs0h0MFMDme/07Gj5TJkaxQLWiacNhqmqEa3Mt1C3k2Wva6OwAAqMMWcAQMdmACg8Qk0xjLBizs7go7QvkmV+RNbHDBdD0p00GRrBj1XTPR4VJiJJvOmY2G7A084lx0M9nOGHtQRLgs116TNOTMHz3rPDeN0SdQJien1AAKa4JhcWtuC0yBlPXtx9nQ4TSrCTgOEOnYfYjh+WKD/ZGUeYzmNlT9Sl6aPjjbpSBMzZzckhxDAdQrveTKb75z8BFr+60X3f76qCO7xsoCCUJfybsgiUZvp6s7qcAkJNnWeIKWao5XEkAOvr6Kry/oncaUNm6Q1LRKgXmB8FiIx3i3xdP"
+ },
+ "security": {
+ "nameIdEncrypted": false,
+ "authnRequestsSigned": true,
+ "logoutRequestSigned": true,
+ "logoutResponseSigned": true,
+ "signMetadata": true,
+ "wantMessagesSigned": false,
+ "wantAssertionsSigned": false,
+ "wantNameIdEncrypted": false,
+ "requestedAuthnContext": true
+ },
+ "contactPerson": {
+ "technical": {
+ "givenName": "Example Admin",
+ "emailAddress": "admin@example.com"
+ },
+ "support": {
+ "givenName": "Example Admin",
+ "emailAddress": "admin@example.com"
+ }
+ },
+ "organization": {
+ "en-US": {
+ "name": "example.com",
+ "displayname": "example.com",
+ "url": "http://example.com"
+ }
+ }
+}
diff --git a/tests/settings/settings4.json b/tests/settings/settings4.json
new file mode 100644
index 00000000..9ee3eea0
--- /dev/null
+++ b/tests/settings/settings4.json
@@ -0,0 +1,58 @@
+{
+ "strict": false,
+ "debug": false,
+ "sp": {
+ "entityId": "sp.example.com",
+ "assertionConsumerService": {
+ "url": "https://sp.example.com/saml/?acs",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+ },
+ "singleLogoutService": {
+ "url": "https://sp.example.com/saml?sls",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ },
+ "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified",
+ "x509cert": "MIIETTCCA7agAwIBAgIJANaOuOCRgiz3MA0GCSqGSIb3DQEBBQUAMIG8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTE9MDsGA1UEChM0WE1MIFNlY3VyaXR5IExpYnJhcnkgKGh0dHA6Ly93d3cuYWxla3NleS5jb20veG1sc2VjKTEeMBwGA1UECxMVVGVzdCBSb290IENlcnRpZmljYXRlMRYwFAYDVQQDEw1BbGVrc2V5IFNhbmluMSEwHwYJKoZIhvcNAQkBFhJ4bWxzZWNAYWxla3NleS5jb20wHhcNMDUwNzEwMDIyOTAxWhcNMTUwNzA4MDIyOTAxWjCBvDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExPTA7BgNVBAoTNFhNTCBTZWN1cml0eSBMaWJyYXJ5IChodHRwOi8vd3d3LmFsZWtzZXkuY29tL3htbHNlYykxHjAcBgNVBAsTFVRlc3QgUm9vdCBDZXJ0aWZpY2F0ZTEWMBQGA1UEAxMNQWxla3NleSBTYW5pbjEhMB8GCSqGSIb3DQEJARYSeG1sc2VjQGFsZWtzZXkuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDayaFajJxOdVU+8EjwO31S2XqNmYxxbHfiUJO3w2h57OPUkKAcKe5Gvt9hJbPTb3C4blPScOke2RexKnXS7pAXXbxFlgUlZ0QK0K2pdl559OSmrtH3mPP9BJvvDMlxkcNj9/EeD+yGd8GN/yT6PTDh8G/4lszOXL+tyKIkC4Ys/wIDAQABo4IBUzCCAU8wDAYDVR0TBAUwAwEB/zAsBglghkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFNpG6Wvmr9M9quUhS1LtymYo4P6FMIHxBgNVHSMEgekwgeaAFNpG6Wvmr9M9quUhS1LtymYo4P6FoYHCpIG/MIG8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTE9MDsGA1UEChM0WE1MIFNlY3VyaXR5IExpYnJhcnkgKGh0dHA6Ly93d3cuYWxla3NleS5jb20veG1sc2VjKTEeMBwGA1UECxMVVGVzdCBSb290IENlcnRpZmljYXRlMRYwFAYDVQQDEw1BbGVrc2V5IFNhbmluMSEwHwYJKoZIhvcNAQkBFhJ4bWxzZWNAYWxla3NleS5jb22CCQDWjrjgkYIs9zANBgkqhkiG9w0BAQUFAAOBgQBUXbdOTQwArcNrbxavzARp2JGOnzo6WzTm+OFSXC0F08YwT8jWbht97e8lNNVOBU4Y/38ReZqYC9OqFofG1/O9AdQ58WL/FWg8DgP5MJPTT9kRU3FU01jUiX2+kbdnghZAOJm0ziRNxfNPwIIWPKYXyXEKQQzrnxyFey1hP7cg6A==",
+ "privateKey": "MIICXQIBAAKBgQDayaFajJxOdVU+8EjwO31S2XqNmYxxbHfiUJO3w2h57OPUkKAcKe5Gvt9hJbPTb3C4blPScOke2RexKnXS7pAXXbxFlgUlZ0QK0K2pdl559OSmrtH3mPP9BJvvDMlxkcNj9/EeD+yGd8GN/yT6PTDh8G/4lszOXL+tyKIkC4Ys/wIDAQABAoGAKfyh43+yi3gG+QIh7UBtZ5Xm5/+8rRO02hC+mHh+t09X1bY/k8gUOy1sLveOUBhF2I8LtQoIIuxkmJJedDFmI0rbHwEMT/3uWzuqVbaPSd5GP9rfHvieF2d4DoZ+iHwyzsagSIgJLe9ZKNaZymdLqMJSaRIkLwTIwUZOpMHeMwkCQQD5VyfjihPCKU5CBHQ3JTAU42Sw8hiPSPUQWPc4F8lcxhqIkC87GgjY6qD9JlRUE4E5zy+hLCNTgggXa5uS0KMTAkEA4KGScqRoiKl1s9Ei1AQX32RSMQaw0Vh46S3ms44zIfjkCjcKON79MUTL1at/2IENoFDimFZUbYhY9D4QH+cf5QJAeQzYH76kMwospR5WcYNLYYi4FLOkOsP3vdUDSKc7qh+/N/eQBohwLSdTuzMFk7/YaAFvJTcxe1RQq1YhtFg4IwJBAI04xwtQFXAlqZv9FXpZgHCvb4TnAe77QjjG5M1pzvfCtAtAAysx9dgtukCA64U/zUNG1s6TJ80c9V/ITPbhpYkCQQCjQbKZgL5E1njCMFUMkYZF53LaGOUrxC7BzObMIECDaoMQppLWm3coXdEjDCl1ZbmZXIdzZ5QwGUdSj/v7nkne"
+ },
+ "idp": {
+ "entityId": "idp.example.com",
+ "singleSignOnService": {
+ "url": "https://idp.example.com/login",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+ },
+ "singleLogoutService": {
+ "url": "https://idp.example.com/sls",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ },
+ "x509cert": "MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo"
+ },
+ "security": {
+ "nameIdEncrypted": false,
+ "authnRequestsSigned": true,
+ "logoutRequestSigned": true,
+ "logoutResponseSigned": true,
+ "signMetadata": true,
+ "wantMessagesSigned": false,
+ "wantAssertionsSigned": false,
+ "wantNameIdEncrypted": false,
+ "requestedAuthnContext": true
+ },
+ "contactPerson": {
+ "technical": {
+ "givenName": "Example Admin",
+ "emailAddress": "admin@example.com"
+ },
+ "support": {
+ "givenName": "Example Admin",
+ "emailAddress": "admin@example.com"
+ }
+ },
+ "organization": {
+ "en-US": {
+ "name": "example.com",
+ "displayname": "example.com",
+ "url": "http://example.com"
+ }
+ }
+}
diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py
index ea0b9fd0..77317fd9 100644
--- a/tests/src/OneLogin/saml2_tests/auth_test.py
+++ b/tests/src/OneLogin/saml2_tests/auth_test.py
@@ -543,6 +543,25 @@ def testLogin(self):
hostname = OneLogin_Saml2_Utils.get_self_host(request_data)
self.assertIn(u'http://%s/index.html' % hostname, parsed_query['RelayState'])
+ def testLoginHttpPostBinding(self):
+ """
+ Tests the login method of the OneLogin_Saml2_Auth class
+ Case Login with no parameters. An AuthnRequest is built into a form.
+ """
+
+ filename = join(dirname(__file__), '..', '..', '..', 'settings', 'example_settings_http_post_binding.json')
+ stream = open(filename, 'r')
+ settings_info = json.load(stream)
+ stream.close()
+
+ request_data = self.get_request()
+ auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_info)
+
+ html = auth.login()
+
+ sso_url = settings_info['idp']['singleSignOnService']['url']
+ self.assertIn(sso_url, html)
+
def testLoginWithRelayState(self):
"""
Tests the login method of the OneLogin_Saml2_Auth class
diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py
index 2d5a0a2f..5a08c026 100644
--- a/tests/src/OneLogin/saml2_tests/authn_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py
@@ -3,6 +3,7 @@
# Copyright (c) 2014, OneLogin, Inc.
# All rights reserved.
+import os
from base64 import b64decode
import json
from os.path import dirname, join, exists
@@ -15,8 +16,12 @@
from onelogin.saml2.settings import OneLogin_Saml2_Settings
from onelogin.saml2.utils import OneLogin_Saml2_Utils
+from lxml.etree import fromstring
+import dm.xmlsec.binding as xmlsec
+
class OneLogin_Saml2_Authn_Request_Test(unittest.TestCase):
+
def loadSettingsJSON(self):
filename = join(dirname(__file__), '..', '..', '..', 'settings', 'settings1.json')
if exists(filename):
@@ -224,3 +229,55 @@ def testCreateEncSAMLRequest(self):
self.assertRegexpMatches(inflated, 'http://stuff.com/endpoints/metadata.php')
self.assertRegexpMatches(inflated, 'Format="urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted"')
self.assertRegexpMatches(inflated, 'ProviderName="SP prueba"')
+
+ def testSignedHttpPostBinding(self):
+ """
+ Test to use the binding: urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST
+
+ To sign a samlp:AuthnRequest you need to have a private key set for the service provider.
+
+ To verify the assertion is signed correct you can also use the xmlsec1 command line tool:
+
+ xmlsec1 --verify --id-attr:ID AuthnRequest --trusted-pem tests/certs/example.com/example.crt authn_signed_assertion.xml
+
+ """
+ filename = join(dirname(__file__), '..', '..', '..', 'settings', 'example_settings_http_post_binding.json')
+ stream = open(filename, 'r')
+ settings = json.load(stream)
+ stream.close()
+
+ settings = OneLogin_Saml2_Settings(settings)
+
+ authn_request = OneLogin_Saml2_Authn_Request(settings)
+ authn_request_encoded = authn_request.get_request()
+ decoded = b64decode(authn_request_encoded)
+ inflated = decompress(decoded, -15)
+
+ sample_output_directory = join(dirname(__file__), '..', '..', '..', 'sample_output')
+ if not os.path.exists(sample_output_directory):
+ os.makedirs(sample_output_directory)
+
+ with open(join(dirname(__file__), '..', '..', '..', 'sample_output/authn_signed_assertion.xml'), 'wb') as f:
+ f.write(inflated)
+
+ # Turn the inflated xml (which is just a string) into a in memory XML document
+ doc = fromstring(inflated)
+
+ # Verification of enveloped signature
+ node = doc.find(".//{%s}Signature" % xmlsec.DSigNs)
+ key_file = join(dirname(__file__), '..', '..', '..', 'certs/example.com', 'example.pubkey')
+
+ dsigCtx = xmlsec.DSigCtx()
+
+ signKey = xmlsec.Key.load(key_file, xmlsec.KeyDataFormatPem, None)
+ signKey.name = 'example.pubkey'
+
+ # Note: the assignment below effectively copies the key
+ dsigCtx.signKey = signKey
+
+ # Add ID attributes different from xml:id
+ # See the Notes on https://pypi.python.org/pypi/dm.xmlsec.binding/1.3.2
+ xmlsec.addIDs(doc, ["ID"])
+
+ # This raises an exception if the document does not verify
+ dsigCtx.verify(node)