Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for AuthnRequest HTTP-POST binding with enveloped signatures #78

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ __pycache_

settings.py
advanced_settings.py

# Any test files / output that is generated by tests
tests/sample_output
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ###
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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': (
Expand Down
26 changes: 26 additions & 0 deletions src/onelogin/saml2/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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):
"""
Expand Down Expand Up @@ -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):
Expand Down
84 changes: 83 additions & 1 deletion src/onelogin/saml2/authn_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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">
<saml:Issuer>%(entity_id)s</saml:Issuer>


<samlp:NameIDPolicy
Format="%(name_id_policy)s"
AllowCreate="true" />
Expand All @@ -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):
"""
Expand Down
12 changes: 12 additions & 0 deletions src/onelogin/saml2/templates/authn_request.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<html>
<body>
<form method="POST" id="SAMLForm" action="{{ sso_url }}">
<input type="hidden" name="SAMLRequest" value="{{ saml_request }}" />
<input type="hidden" name="RelayState" value="{{ relay_state }}" />
<input type="submit" value="Submit" />
</form>
<script type="text/javascript">
document.forms["SAMLForm"].submit();
</script>
</body>
</html>
27 changes: 27 additions & 0 deletions tests/certs/example.com/example.privatekey
Original file line number Diff line number Diff line change
@@ -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-----
9 changes: 9 additions & 0 deletions tests/certs/example.com/example.pubkey
Original file line number Diff line number Diff line change
@@ -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-----
59 changes: 59 additions & 0 deletions tests/settings/example_settings_http_post_binding.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}