Project

General

Profile

0005-misc-add-support-for-SOAP-SLO-41949.patch

Benjamin Dauvergne, 24 Apr 2020 01:22 PM

Download (10.3 KB)

View differences:

Subject: [PATCH 5/7] misc: add support for SOAP SLO (#41949)

 mellon/templates/mellon/metadata.xml |  3 ++
 mellon/views.py                      | 17 +++---
 tests/test_sso_slo.py                | 80 ++++++++++++++++++++++++++++
 tests/test_utils.py                  |  8 +--
 tests/test_views.py                  |  6 +--
 5 files changed, 100 insertions(+), 14 deletions(-)
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
-