Skip to content

Commit 8d9b692

Browse files
authored
Merge pull request #189 from sdelements/add_missing_authn_test
Add missing post authn test
2 parents 2f80dbb + 1ce5405 commit 8d9b692

File tree

5 files changed

+87
-1
lines changed

5 files changed

+87
-1
lines changed

djangosaml2/tests/__init__.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from djangosaml2.conf import get_config
3838
from djangosaml2.signals import post_authenticated
3939
from djangosaml2.tests import conf
40+
from djangosaml2.tests.utils import SAMLPostFormParser
4041
from djangosaml2.tests.auth_response import auth_response
4142
from djangosaml2.views import finish_logout
4243
from saml2.config import SPConfig
@@ -108,6 +109,35 @@ def render_template(self, text):
108109
def b64_for_post(self, xml_text, encoding='utf-8'):
109110
return base64.b64encode(xml_text.encode(encoding)).decode('ascii')
110111

112+
def test_unsigned_post_authn_request(self):
113+
"""
114+
Test that unsigned authentication requests via POST binding
115+
does not error.
116+
117+
https://github.com/knaperek/djangosaml2/issues/168
118+
"""
119+
settings.SAML_CONFIG = conf.create_conf(
120+
sp_host='sp.example.com',
121+
idp_hosts=['idp.example.com'],
122+
metadata_file='remote_metadata_post_binding.xml',
123+
authn_requests_signed=False
124+
)
125+
response = self.client.get(reverse('saml2_login'))
126+
127+
self.assertEqual(response.status_code, 200)
128+
129+
# Using POST-binding returns a page with form containing the SAMLRequest
130+
response_parser = SAMLPostFormParser()
131+
response_parser.feed(response.content.decode('utf-8'))
132+
saml_request = response_parser.saml_request_value
133+
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>"""
134+
135+
self.assertIsNotNone(saml_request)
136+
self.assertSAMLRequestsEquals(
137+
base64.b64decode(saml_request).decode('utf-8'),
138+
expected_request
139+
)
140+
111141
def test_login_evil_redirect(self):
112142
"""
113143
Make sure that if we give an URL other than our own host as the next

djangosaml2/tests/conf.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020

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

2424
try:
2525
from saml2.sigver import get_xmlsec_binary
@@ -90,6 +90,9 @@ def create_conf(sp_host='sp.example.com', idp_hosts=['idp.example.com'],
9090
'valid_for': 24,
9191
}
9292

93+
if authn_requests_signed is not None:
94+
config['service']['sp']['authn_requests_signed'] = authn_requests_signed
95+
9396
for idp in idp_hosts:
9497
entity_id = 'https://%s/simplesaml/saml2/idp/metadata.php' % idp
9598
config['service']['sp']['idp'][entity_id] = {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?xml version="1.0"?>
2+
<md:EntitiesDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
3+
<md:EntityDescriptor entityID="https://idp.example.com/simplesaml/saml2/idp/metadata.php">
4+
<md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
5+
<md:KeyDescriptor use="signing">
6+
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
7+
<ds:X509Data>
8+
<ds:X509Certificate>MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo</ds:X509Certificate>
9+
</ds:X509Data>
10+
</ds:KeyInfo>
11+
</md:KeyDescriptor>
12+
<md:KeyDescriptor use="encryption">
13+
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
14+
<ds:X509Data>
15+
<ds:X509Certificate>MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo</ds:X509Certificate>
16+
</ds:X509Data>
17+
</ds:KeyInfo>
18+
</md:KeyDescriptor>
19+
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php"/>
20+
<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
21+
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://idp.example.com/simplesaml/saml2/idp/SSOService.php"/>
22+
</md:IDPSSODescriptor>
23+
<md:Organization>
24+
<md:OrganizationName xml:lang="en">Lorenzo's test IdP</md:OrganizationName>
25+
<md:OrganizationDisplayName xml:lang="en">idp.example.com IdP</md:OrganizationDisplayName>
26+
<md:OrganizationURL xml:lang="en">http://idp.example.com/</md:OrganizationURL>
27+
</md:Organization>
28+
<md:ContactPerson contactType="technical">
29+
<md:SurName>Administrator</md:SurName>
30+
<md:EmailAddress>[email protected]</md:EmailAddress>
31+
</md:ContactPerson>
32+
</md:EntityDescriptor>
33+
</md:EntitiesDescriptor>

djangosaml2/tests/utils.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from html.parser import HTMLParser
2+
3+
4+
class SAMLPostFormParser(HTMLParser):
5+
"""
6+
Parses the SAML Post binding form page for the SAMLRequest value.
7+
"""
8+
9+
saml_request_value = None
10+
11+
def handle_starttag(self, tag, attrs):
12+
attrs_dict = dict(attrs)
13+
14+
if tag != "input" or attrs_dict.get("name") != "SAMLRequest":
15+
return
16+
self.saml_request_value = attrs_dict.get("value")

djangosaml2/views.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
)
4545
from saml2.mdstore import SourceNotFound
4646
from saml2.sigver import MissingKey
47+
from saml2.samlp import AuthnRequest
4748
from saml2.validate import ResponseLifetimeExceed, ToEarly
4849
from saml2.xmldsig import ( # support for SHA1 is required by spec
4950
SIG_RSA_SHA1, SIG_RSA_SHA256)
@@ -228,6 +229,9 @@ def login(request,
228229
binding=binding,
229230
**kwargs)
230231
try:
232+
if isinstance(request_xml, AuthnRequest):
233+
# request_xml will be an instance of AuthnRequest if the message is not signed
234+
request_xml = str(request_xml)
231235
saml_request = base64.b64encode(bytes(request_xml, 'UTF-8')).decode('utf-8')
232236

233237
http_response = render(request, post_binding_form_template, {

0 commit comments

Comments
 (0)