0005-misc-add-support-for-SOAP-SLO-41949.patch
mellon/templates/mellon/metadata.xml | ||
---|---|---|
26 | 26 |
<SingleLogoutService |
27 | 27 |
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" |
28 | 28 |
Location="{{ logout_url }}" /> |
29 |
<SingleLogoutService |
|
30 |
Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" |
|
31 |
Location="{{ logout_url }}" /> |
|
29 | 32 |
{% for name_id_format in name_id_formats %} |
30 | 33 |
<NameIDFormat>{{ name_id_format }}</NameIDFormat> |
31 | 34 |
{% endfor %} |
mellon/views.py | ||
---|---|---|
35 | 35 |
from django.urls import reverse |
36 | 36 |
from django.utils.http import urlencode |
37 | 37 |
from django.utils import six |
38 |
from django.utils.encoding import force_text |
|
38 |
from django.utils.encoding import force_text, force_str
|
|
39 | 39 |
from django.contrib.auth import REDIRECT_FIELD_NAME |
40 | 40 |
from django.db import transaction |
41 | 41 |
from django.utils.translation import ugettext as _ |
... | ... | |
515 | 515 |
class LogoutView(ProfileMixin, LogMixin, View): |
516 | 516 |
def get(self, request, *args, **kwargs): |
517 | 517 |
if 'SAMLRequest' in request.GET: |
518 |
return self.idp_logout(request, request.META['QUERY_STRING']) |
|
518 |
return self.idp_logout(request, request.META['QUERY_STRING'], 'redirect')
|
|
519 | 519 |
elif 'SAMLResponse' in request.GET: |
520 | 520 |
return self.sp_logout_response(request) |
521 | 521 |
else: |
522 | 522 |
return self.sp_logout_request(request) |
523 | 523 | |
524 |
def logout(self, request, issuer, saml_user, session_indexes, indexes): |
|
524 |
def post(self, request, *args, **kwargs): |
|
525 |
return self.idp_logout(request, force_str(request.body), 'soap') |
|
526 | ||
527 |
def logout(self, request, issuer, saml_user, session_indexes, indexes, mode): |
|
525 | 528 |
session_keys = set(indexes.values_list('session_key', flat=True)) |
526 | 529 |
indexes.delete() |
527 | 530 | |
528 | 531 |
synchronous_logout = request.user == saml_user |
529 | 532 |
asynchronous_logout = ( |
533 |
mode == 'soap' |
|
530 | 534 |
# the current session is not the only killed |
531 |
len(session_keys) != 1 |
|
535 |
or len(session_keys) != 1
|
|
532 | 536 |
or ( |
533 | 537 |
# there is not current session |
534 | 538 |
not request.user.is_authenticated() |
... | ... | |
559 | 563 |
auth.logout(request) |
560 | 564 |
self.log.info('synchronous logout of %s', user) |
561 | 565 | |
562 |
def idp_logout(self, request, msg): |
|
566 |
def idp_logout(self, request, msg, mode):
|
|
563 | 567 |
'''Handle logout request emitted by the IdP''' |
564 | 568 |
self.profile = logout = utils.create_logout(request) |
565 | 569 |
try: |
... | ... | |
602 | 606 |
issuer=issuer, |
603 | 607 |
saml_user=name_id_user, |
604 | 608 |
session_indexes=session_indexes, |
605 |
indexes=indexes) |
|
609 |
indexes=indexes, |
|
610 |
mode=mode) |
|
606 | 611 | |
607 | 612 |
try: |
608 | 613 |
logout.buildResponseMsg() |
tests/test_sso_slo.py | ||
---|---|---|
277 | 277 |
idp.check_slo_return(response.location) |
278 | 278 | |
279 | 279 | |
280 |
def test_sso_idp_slo_soap(db, app, idp, caplog, sp_settings): |
|
281 |
assert Session.objects.count() == 0 |
|
282 |
assert User.objects.count() == 0 |
|
283 | ||
284 |
# first session |
|
285 |
response = app.get(reverse('mellon_login') + '?next=/whatever/') |
|
286 |
url, body, relay_state = idp.process_authn_request_redirect(response['Location']) |
|
287 |
assert relay_state |
|
288 |
assert 'eo:next_url' not in str(idp.request) |
|
289 |
assert url.endswith(reverse('mellon_login')) |
|
290 |
response = app.post(reverse('mellon_login'), params={'SAMLResponse': body, 'RelayState': relay_state}) |
|
291 |
assert 'created new user' in caplog.text |
|
292 |
assert 'logged in using SAML' in caplog.text |
|
293 |
assert urlparse.urlparse(response['Location']).path == '/whatever/' |
|
294 | ||
295 |
# start a new Lasso session |
|
296 |
idp.reset_session_dump() |
|
297 | ||
298 |
# second session |
|
299 |
app.cookiejar.clear() |
|
300 |
response = app.get(reverse('mellon_login') + '?next=/whatever/') |
|
301 |
url, body, relay_state = idp.process_authn_request_redirect(response['Location']) |
|
302 |
assert relay_state |
|
303 |
assert 'eo:next_url' not in str(idp.request) |
|
304 |
assert url.endswith(reverse('mellon_login')) |
|
305 |
response = app.post(reverse('mellon_login'), params={'SAMLResponse': body, 'RelayState': relay_state}) |
|
306 |
assert 'created new user' in caplog.text |
|
307 |
assert 'logged in using SAML' in caplog.text |
|
308 |
assert urlparse.urlparse(response['Location']).path == '/whatever/' |
|
309 | ||
310 |
assert Session.objects.count() == 2 |
|
311 |
assert User.objects.count() == 1 |
|
312 | ||
313 |
# idp logout |
|
314 |
app.cookiejar.clear() |
|
315 | ||
316 |
url, body, relay_state = idp.init_slo(method=lasso.HTTP_METHOD_SOAP) |
|
317 |
response = app.post(url, params=body, headers={'Content-Type': force_str('text/xml')}) |
|
318 |
assert Session.objects.count() == 1 |
|
319 |
idp.check_slo_return(body=response.content) |
|
320 | ||
321 | ||
280 | 322 |
def test_sso_idp_slo_full(db, app, idp, caplog, sp_settings): |
281 | 323 |
assert Session.objects.count() == 0 |
282 | 324 |
assert User.objects.count() == 0 |
... | ... | |
315 | 357 |
idp.check_slo_return(url=response.location) |
316 | 358 | |
317 | 359 | |
360 |
def test_sso_idp_slo_full_soap(db, app, idp, caplog, sp_settings): |
|
361 |
assert Session.objects.count() == 0 |
|
362 |
assert User.objects.count() == 0 |
|
363 | ||
364 |
# first session |
|
365 |
response = app.get(reverse('mellon_login') + '?next=/whatever/') |
|
366 |
url, body, relay_state = idp.process_authn_request_redirect(response['Location']) |
|
367 |
assert relay_state |
|
368 |
assert 'eo:next_url' not in str(idp.request) |
|
369 |
assert url.endswith(reverse('mellon_login')) |
|
370 |
response = app.post(reverse('mellon_login'), params={'SAMLResponse': body, 'RelayState': relay_state}) |
|
371 |
assert 'created new user' in caplog.text |
|
372 |
assert 'logged in using SAML' in caplog.text |
|
373 |
assert urlparse.urlparse(response['Location']).path == '/whatever/' |
|
374 | ||
375 |
# second session |
|
376 |
app.cookiejar.clear() |
|
377 |
response = app.get(reverse('mellon_login') + '?next=/whatever/') |
|
378 |
url, body, relay_state = idp.process_authn_request_redirect(response['Location']) |
|
379 |
assert relay_state |
|
380 |
assert 'eo:next_url' not in str(idp.request) |
|
381 |
assert url.endswith(reverse('mellon_login')) |
|
382 |
response = app.post(reverse('mellon_login'), params={'SAMLResponse': body, 'RelayState': relay_state}) |
|
383 |
assert 'created new user' in caplog.text |
|
384 |
assert 'logged in using SAML' in caplog.text |
|
385 |
assert urlparse.urlparse(response['Location']).path == '/whatever/' |
|
386 | ||
387 |
assert Session.objects.count() == 2 |
|
388 |
assert User.objects.count() == 1 |
|
389 | ||
390 |
# idp logout |
|
391 |
app.cookiejar.clear() |
|
392 |
url, body, relay_state = idp.init_slo(method=lasso.HTTP_METHOD_SOAP, full=True) |
|
393 |
response = app.post(url, params=body, headers={'Content-Type': force_str('text/xml')}) |
|
394 |
assert Session.objects.count() == 0 |
|
395 |
idp.check_slo_return(body=response.content) |
|
396 | ||
397 | ||
318 | 398 |
def test_sso(db, app, idp, caplog, sp_settings): |
319 | 399 |
response = app.get(reverse('mellon_login')) |
320 | 400 |
url, body, relay_state = idp.process_authn_request_redirect(response['Location']) |
tests/test_utils.py | ||
---|---|---|
42 | 42 |
('/sm:EntityDescriptor[@entityID="http://testserver/metadata/"]', 1, |
43 | 43 |
('/*', 1), |
44 | 44 |
('/sm:SPSSODescriptor', 1, |
45 |
('/*', 6),
|
|
45 |
('/*', 7),
|
|
46 | 46 |
('/sm:NameIDFormat', 1), |
47 |
('/sm:SingleLogoutService', 1),
|
|
47 |
('/sm:SingleLogoutService', 2),
|
|
48 | 48 |
('/sm:AssertionConsumerService[@isDefault=\'true\'][@Binding=\'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact\']', 1), |
49 | 49 |
('/sm:AssertionConsumerService[@isDefault=\'true\'][@Binding=\'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\']', |
50 | 50 |
0), |
... | ... | |
64 | 64 |
('/sm:EntityDescriptor[@entityID="http://testserver/metadata/"]', 1, |
65 | 65 |
('/*', 1), |
66 | 66 |
('/sm:SPSSODescriptor', 1, |
67 |
('/*', 7),
|
|
67 |
('/*', 8),
|
|
68 | 68 |
('/sm:Extensions', 1, |
69 | 69 |
('/idpdisc:DiscoveryResponse', 1)), |
70 | 70 |
('/sm:NameIDFormat', 1), |
71 |
('/sm:SingleLogoutService', 1),
|
|
71 |
('/sm:SingleLogoutService', 2),
|
|
72 | 72 |
('/sm:AssertionConsumerService[@isDefault=\'true\'][@Binding=\'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact\']', 1), |
73 | 73 |
('/sm:AssertionConsumerService[@isDefault=\'true\'][@Binding=\'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\']', |
74 | 74 |
0), |
tests/test_views.py | ||
---|---|---|
20 | 20 |
import lasso |
21 | 21 |
from django.utils.six.moves.urllib.parse import parse_qs, urlparse |
22 | 22 |
import base64 |
23 |
import random |
|
24 | 23 |
import hashlib |
25 | 24 |
from httmock import HTTMock |
26 | 25 | |
27 |
import django |
|
28 | 26 |
from django.urls import reverse |
29 | 27 |
from django.utils.encoding import force_text |
30 | 28 |
from django.utils.http import urlencode |
... | ... | |
109 | 107 |
('/sm:EntityDescriptor[@entityID="http://testserver/metadata/"]', 1, |
110 | 108 |
('/*', 4), |
111 | 109 |
('/sm:SPSSODescriptor', 1, |
112 |
('/*', 6),
|
|
110 |
('/*', 7),
|
|
113 | 111 |
('/sm:NameIDFormat', 1), |
114 |
('/sm:SingleLogoutService', 1),
|
|
112 |
('/sm:SingleLogoutService', 2),
|
|
115 | 113 |
('/sm:AssertionConsumerService', None, |
116 | 114 |
('[@isDefault="true"]', None, |
117 | 115 |
('[@Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"]', 1), |
118 |
- |