Skip to content

Add missing post authn test #189

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

Merged
merged 5 commits into from
Apr 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions djangosaml2/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from djangosaml2.conf import get_config
from djangosaml2.signals import post_authenticated
from djangosaml2.tests import conf
from djangosaml2.tests.utils import SAMLPostFormParser
from djangosaml2.tests.auth_response import auth_response
from djangosaml2.views import finish_logout
from saml2.config import SPConfig
Expand Down Expand Up @@ -108,6 +109,35 @@ def render_template(self, text):
def b64_for_post(self, xml_text, encoding='utf-8'):
return base64.b64encode(xml_text.encode(encoding)).decode('ascii')

def test_unsigned_post_authn_request(self):
"""
Test that unsigned authentication requests via POST binding
does not error.

https://github.com/knaperek/djangosaml2/issues/168
"""
settings.SAML_CONFIG = conf.create_conf(
sp_host='sp.example.com',
idp_hosts=['idp.example.com'],
metadata_file='remote_metadata_post_binding.xml',
authn_requests_signed=False
)
response = self.client.get(reverse('saml2_login'))

self.assertEqual(response.status_code, 200)

# Using POST-binding returns a page with form containing the SAMLRequest
response_parser = SAMLPostFormParser()
response_parser.feed(response.content.decode('utf-8'))
saml_request = response_parser.saml_request_value
expected_request = """<samlp:AuthnRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="false" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" /></samlp:AuthnRequest>"""

self.assertIsNotNone(saml_request)
self.assertSAMLRequestsEquals(
base64.b64decode(saml_request).decode('utf-8'),
expected_request
)

def test_login_evil_redirect(self):
"""
Make sure that if we give an URL other than our own host as the next
Expand Down
5 changes: 4 additions & 1 deletion djangosaml2/tests/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@


def create_conf(sp_host='sp.example.com', idp_hosts=['idp.example.com'],
metadata_file='remote_metadata.xml'):
metadata_file='remote_metadata.xml', authn_requests_signed=None):

try:
from saml2.sigver import get_xmlsec_binary
Expand Down Expand Up @@ -90,6 +90,9 @@ def create_conf(sp_host='sp.example.com', idp_hosts=['idp.example.com'],
'valid_for': 24,
}

if authn_requests_signed is not None:
config['service']['sp']['authn_requests_signed'] = authn_requests_signed

for idp in idp_hosts:
entity_id = 'https://%s/simplesaml/saml2/idp/metadata.php' % idp
config['service']['sp']['idp'][entity_id] = {
Expand Down
33 changes: 33 additions & 0 deletions djangosaml2/tests/remote_metadata_post_binding.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0"?>
<md:EntitiesDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<md:EntityDescriptor entityID="https://idp.example.com/simplesaml/saml2/idp/metadata.php">
<md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:KeyDescriptor use="encryption">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php"/>
<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://idp.example.com/simplesaml/saml2/idp/SSOService.php"/>
</md:IDPSSODescriptor>
<md:Organization>
<md:OrganizationName xml:lang="en">Lorenzo's test IdP</md:OrganizationName>
<md:OrganizationDisplayName xml:lang="en">idp.example.com IdP</md:OrganizationDisplayName>
<md:OrganizationURL xml:lang="en">http://idp.example.com/</md:OrganizationURL>
</md:Organization>
<md:ContactPerson contactType="technical">
<md:SurName>Administrator</md:SurName>
<md:EmailAddress>[email protected]</md:EmailAddress>
</md:ContactPerson>
</md:EntityDescriptor>
</md:EntitiesDescriptor>
16 changes: 16 additions & 0 deletions djangosaml2/tests/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from html.parser import HTMLParser


class SAMLPostFormParser(HTMLParser):
"""
Parses the SAML Post binding form page for the SAMLRequest value.
"""

saml_request_value = None

def handle_starttag(self, tag, attrs):
attrs_dict = dict(attrs)

if tag != "input" or attrs_dict.get("name") != "SAMLRequest":
return
self.saml_request_value = attrs_dict.get("value")
4 changes: 4 additions & 0 deletions djangosaml2/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
)
from saml2.mdstore import SourceNotFound
from saml2.sigver import MissingKey
from saml2.samlp import AuthnRequest
from saml2.validate import ResponseLifetimeExceed, ToEarly
from saml2.xmldsig import ( # support for SHA1 is required by spec
SIG_RSA_SHA1, SIG_RSA_SHA256)
Expand Down Expand Up @@ -228,6 +229,9 @@ def login(request,
binding=binding,
**kwargs)
try:
if isinstance(request_xml, AuthnRequest):
# request_xml will be an instance of AuthnRequest if the message is not signed
request_xml = str(request_xml)
saml_request = base64.b64encode(bytes(request_xml, 'UTF-8')).decode('utf-8')

http_response = render(request, post_binding_form_template, {
Expand Down