Skip to content

Commit 7e67158

Browse files
authored
Merge branch 'master' into unsolicited_response
2 parents 6d6bc9c + b0ed9ea commit 7e67158

File tree

6 files changed

+121
-11
lines changed

6 files changed

+121
-11
lines changed

CHANGES

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,16 @@ Changes
44
UNRELEASED
55
----------
66
- A 403 (permission denied) is now raised if a SAMLResponse is replayed, instead of 500.
7+
8+
0.16.11 (2017-12-25)
9+
----------
10+
- Dropped compatibility for Python < 2.7 and Django < 1.8.
11+
- Added a clean_attributes hook allowing backends to restructure attributes extracted from SAML response.
712
- Log when fields are missing in a SAML response.
813
- Log when attribute_mapping maps to nonexistent User fields.
9-
- Dropped compatibility for Python < 2.7 and Django < 1.8.
14+
- Multiple compatibility fixes and other minor improvements and code cleanups
15+
16+
Thanks to francoisfreitag, mhindery, charn, jdufresne
1017

1118
0.16.10 (2017-10-02)
1219
-------------------

djangosaml2/backends.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,14 @@ class Saml2Backend(ModelBackend):
6464
def authenticate(self, request, session_info=None, attribute_mapping=None,
6565
create_unknown_user=True, **kwargs):
6666
if session_info is None or attribute_mapping is None:
67-
logger.error('Session info or attribute mapping are None')
67+
logger.info('Session info or attribute mapping are None')
6868
return None
6969

70-
if not 'ava' in session_info:
70+
if 'ava' not in session_info:
7171
logger.error('"ava" key not found in session_info')
7272
return None
7373

74-
attributes = session_info['ava']
74+
attributes = self.clean_attributes(session_info['ava'])
7575
if not attributes:
7676
logger.error('The attributes dictionary is empty')
7777

@@ -120,6 +120,10 @@ def is_authorized(self, attributes, attribute_mapping):
120120
"""
121121
return True
122122

123+
def clean_attributes(self, attributes):
124+
"""Hook to clean attributes from the SAML response."""
125+
return attributes
126+
123127
def clean_user_main_attribute(self, main_attribute):
124128
"""Performs any cleaning on the user main attribute (which
125129
usually is "username") prior to using it to get or

djangosaml2/tests/auth_response.py

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,87 @@
1616
import datetime
1717

1818

19-
def auth_response(session_id, uid):
20-
"""Generates a fresh signed authentication response"""
19+
def auth_response(session_id,
20+
uid,
21+
audience='http://sp.example.com/saml2/metadata/',
22+
acs_url='http://sp.example.com/saml2/acs/',
23+
metadata_url='http://sp.example.com/saml2/metadata/',
24+
attribute_statements=None):
25+
"""Generates a fresh signed authentication response
26+
27+
Params:
28+
session_id: The session ID to generate the reponse for. Login set an
29+
outstanding session ID, i.e. djangosaml2 waits for a response for
30+
that session.
31+
uid: Unique identifier for a User (will be present as an attribute in
32+
the answer). Ignored when attribute_statements is not ``None``.
33+
audience: SP entityid (used when PySAML validates the response
34+
audience).
35+
acs_url: URL where the response has been posted back.
36+
metadata_url: URL where the SP metadata can be queried.
37+
attribute_statements: An alternative XML AttributeStatement to use in
38+
lieu of the default (uid). The uid argument is ignored when
39+
attribute_statements is not ``None``.
40+
"""
2141
timestamp = datetime.datetime.now() - datetime.timedelta(seconds=10)
2242
tomorrow = datetime.datetime.now() + datetime.timedelta(days=1)
2343
yesterday = datetime.datetime.now() - datetime.timedelta(days=1)
2444

25-
saml_response_tpl = """<?xml version='1.0' encoding='UTF-8'?>
26-
<samlp:Response xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" Destination="http://sp.example.com/saml2/acs/" ID="id-88b9f586a2a3a639f9327485cc37c40a" InResponseTo="%(session_id)s" IssueInstant="%(timestamp)s" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">https://idp.example.com/simplesaml/saml2/idp/metadata.php</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status><saml:Assertion ID="id-093952102ceb73436e49cb91c58b0578" IssueInstant="%(timestamp)s" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">https://idp.example.com/simplesaml/saml2/idp/metadata.php</saml:Issuer><saml:Subject><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" NameQualifier="" SPNameQualifier="http://sp.example.com/saml2/metadata/">1f87035b4c1325b296a53d92097e6b3fa36d7e30ee82e3fcb0680d60243c1f03</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData InResponseTo="%(session_id)s" NotOnOrAfter="%(tomorrow)s" Recipient="http://sp.example.com/saml2/acs/" /></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="%(yesterday)s" NotOnOrAfter="%(tomorrow)s"><saml:AudienceRestriction><saml:Audience>http://sp.example.com/saml2/metadata/</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant="%(timestamp)s" SessionIndex="%(session_id)s"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute FriendlyName="uid" Name="urn:oid:0.9.2342.19200300.100.1.1" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><saml:AttributeValue xsi:nil="true" xsi:type="xs:string">%(uid)s</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion></samlp:Response>"""
45+
if attribute_statements is None:
46+
attribute_statements = (
47+
'<saml:AttributeStatement>'
48+
'<saml:Attribute FriendlyName="uid" Name="urn:oid:0.9.2342.19200300.100.1.1" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">'
49+
'<saml:AttributeValue xsi:nil="true" xsi:type="xs:string">'
50+
'%(uid)s'
51+
'</saml:AttributeValue>'
52+
'</saml:Attribute>'
53+
'</saml:AttributeStatement>'
54+
) % {'uid': uid}
55+
56+
saml_response_tpl = (
57+
"<?xml version='1.0' encoding='UTF-8'?>"
58+
'<samlp:Response xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" Destination="%(acs_url)s" ID="id-88b9f586a2a3a639f9327485cc37c40a" InResponseTo="%(session_id)s" IssueInstant="%(timestamp)s" Version="2.0">'
59+
'<saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">'
60+
'https://idp.example.com/simplesaml/saml2/idp/metadata.php'
61+
'</saml:Issuer>'
62+
'<samlp:Status>'
63+
'<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />'
64+
'</samlp:Status>'
65+
'<saml:Assertion ID="id-093952102ceb73436e49cb91c58b0578" IssueInstant="%(timestamp)s" Version="2.0">'
66+
'<saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">'
67+
'https://idp.example.com/simplesaml/saml2/idp/metadata.php'
68+
'</saml:Issuer>'
69+
'<saml:Subject>'
70+
'<saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" NameQualifier="" SPNameQualifier="%(metadata_url)s">'
71+
'1f87035b4c1325b296a53d92097e6b3fa36d7e30ee82e3fcb0680d60243c1f03'
72+
'</saml:NameID>'
73+
'<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">'
74+
'<saml:SubjectConfirmationData InResponseTo="%(session_id)s" NotOnOrAfter="%(tomorrow)s" Recipient="%(acs_url)s" />'
75+
'</saml:SubjectConfirmation>'
76+
'</saml:Subject>'
77+
'<saml:Conditions NotBefore="%(yesterday)s" NotOnOrAfter="%(tomorrow)s">'
78+
'<saml:AudienceRestriction>'
79+
'<saml:Audience>'
80+
'%(audience)s'
81+
'</saml:Audience>'
82+
'</saml:AudienceRestriction>'
83+
'</saml:Conditions>'
84+
'<saml:AuthnStatement AuthnInstant="%(timestamp)s" SessionIndex="%(session_id)s">'
85+
'<saml:AuthnContext>'
86+
'<saml:AuthnContextClassRef>'
87+
'urn:oasis:names:tc:SAML:2.0:ac:classes:Password'
88+
'</saml:AuthnContextClassRef>'
89+
'</saml:AuthnContext>'
90+
'</saml:AuthnStatement>'
91+
'%(attribute_statements)s'
92+
'</saml:Assertion>'
93+
'</samlp:Response>')
2794
return saml_response_tpl % {
28-
'uid': uid,
2995
'session_id': session_id,
96+
'audience': audience,
97+
'acs_url': acs_url,
98+
'metadata_url': metadata_url,
99+
'attribute_statements': attribute_statements,
30100
'timestamp': timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'),
31101
'tomorrow': tomorrow.strftime('%Y-%m-%dT%H:%M:%SZ'),
32102
'yesterday': yesterday.strftime('%Y-%m-%dT%H:%M:%SZ'),

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def read(*rnames):
3131

3232
setup(
3333
name='djangosaml2',
34-
version='0.16.10',
34+
version='0.16.11',
3535
description='pysaml2 integration for Django',
3636
long_description='\n\n'.join([read('README.rst'), read('CHANGES')]),
3737
classifiers=[

tests/testprofiles/tests.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,33 @@ def test_django_user_main_attribute_lookup(self):
166166
self.assertEqual(
167167
backend.get_django_user_main_attribute_lookup(),
168168
'__iexact')
169+
170+
171+
class LowerCaseSaml2Backend(Saml2Backend):
172+
def clean_attributes(self, attributes):
173+
return dict([k.lower(), v] for k, v in attributes.items())
174+
175+
176+
class LowerCaseSaml2BackendTest(TestCase):
177+
def test_update_user_clean_attributes(self):
178+
user = User.objects.create(username='john')
179+
attribute_mapping = {
180+
'uid': ('username', ),
181+
'mail': ('email', ),
182+
'cn': ('first_name', ),
183+
'sn': ('last_name', ),
184+
}
185+
attributes = {
186+
'UID': ['john'],
187+
'MAIL': ['[email protected]'],
188+
'CN': ['John'],
189+
'SN': [],
190+
}
191+
192+
backend = LowerCaseSaml2Backend()
193+
user = backend.authenticate(
194+
None,
195+
session_info={'ava': attributes},
196+
attribute_mapping=attribute_mapping,
197+
)
198+
self.assertIsNotNone(user)

tox.ini

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,5 @@ deps =
1818
djangomaster: https://github.com/django/django/archive/master.tar.gz
1919
.[test]
2020

21-
# Waiting on upstream fix for https://code.djangoproject.com/ticket/28679
2221
ignore_outcome =
2322
djangomaster: True

0 commit comments

Comments
 (0)