Projet

Général

Profil

0007-views-implement-a-sessionless-logout-endpoint-69740.patch

Benjamin Dauvergne, 04 octobre 2022 11:47

Télécharger (7,14 ko)

Voir les différences:

Subject: [PATCH 7/8] views: implement a sessionless logout endpoint (#69740)

To implement SAML single logout in authentic we need a logout endpoint
which works event after the user session has been killed, to do that we
store the needed information in Django signed token, and use it to
initiate the logout request. Afterward the next_url is stored in
short-lived session cookie instead of the session.
 mellon/views.py       | 69 +++++++++++++++++++++++++++++++++++++++++--
 tests/test_sso_slo.py | 24 +++++++++++++++
 2 files changed, 91 insertions(+), 2 deletions(-)
mellon/views.py
28 28
from django.conf import settings
29 29
from django.contrib import auth
30 30
from django.contrib.auth import REDIRECT_FIELD_NAME, get_user_model
31
from django.core import signing
31 32
from django.db import transaction
32 33
from django.http import Http404, HttpResponse, HttpResponseForbidden, HttpResponseRedirect
33 34
from django.shortcuts import render, resolve_url
......
620 621

  
621 622
class LogoutView(ProfileMixin, LogMixin, View):
622 623
    def get(self, request, *args, logout_next_url='/', **kwargs):
623
        if 'SAMLRequest' in request.GET:
624
        if 'token' in request.GET:
625
            return self.sp_logout_token(request, token=request.GET['token'], logout_next_url=logout_next_url)
626
        elif 'SAMLRequest' in request.GET:
624 627
            return self.idp_logout(request, request.META['QUERY_STRING'], 'redirect')
625 628
        elif 'SAMLResponse' in request.GET:
626 629
            return self.sp_logout_response(request, logout_next_url=logout_next_url)
......
728 731
        else:
729 732
            return HttpResponseRedirect(logout.msgUrl)
730 733

  
734
    def next_url_cookie_name(self, relaystate):
735
        return f'MellonNextURL-{relaystate}'
736

  
731 737
    def sp_logout_request(self, request, logout_next_url=None):
732 738
        '''Launch a logout request to the identity provider'''
733 739
        referer = request.headers.get('Referer')
......
778 784
        '''Launch a logout request to the identity provider'''
779 785
        self.profile = logout = utils.create_logout(request)
780 786
        logout.msgRelayState = request.GET.get('RelayState')
787
        cookie_name = self.next_url_cookie_name(logout.msgRelayState)
788
        cookie_next_url = request.COOKIES.get(cookie_name)
789
        next_url = self.get_next_url() or cookie_next_url or logout_next_url
781 790
        # the user shouldn't be logged anymore at this point but it may happen
782 791
        # that a concurrent SSO happened in the meantime, so we do another
783 792
        # logout to make sure.
......
789 798
            self.log.warning('partial logout')
790 799
        except lasso.Error as e:
791 800
            self.log.warning('unable to process a logout response: %s', e)
792
        return HttpResponseRedirect(self.get_next_url() or logout_next_url)
801
        response = HttpResponseRedirect(next_url)
802
        if cookie_name in request.COOKIES:
803
            response.delete_cookie(cookie_name)
804
        return response
805

  
806
    TOKEN_SALT = 'mellon-logout-token'
807

  
808
    def sp_logout_token(self, request, token, logout_next_url):
809
        token_content = signing.loads(token, salt=self.TOKEN_SALT)
810
        next_url = token_content['next_url'] or logout_next_url
811
        session_index_pk = token_content['session_index_pk']
812
        session_indexes = models.SessionIndex.objects.filter(pk=session_index_pk)
813
        if session_indexes:
814
            session_dump = utils.make_session_dump(session_indexes)
815
            logout = utils.create_logout(request)
816
            logout.msgRelayState = str(uuid.uuid4())
817
            try:
818
                logout.setSessionFromDump(session_dump)
819
                logout.initRequest(
820
                    session_indexes[0].saml_identifier.issuer.entity_id, lasso.HTTP_METHOD_REDIRECT
821
                )
822
                logout.buildRequestMsg()
823
            except lasso.Error as e:
824
                self.log.error('unable to initiate a logout request %r', e)
825
                return HttpResponseRedirect(next_url)
826
            except Exception:
827
                self.log.exception('unable to initiate a logout request')
828
                return HttpResponseRedirect(next_url)
829
            else:
830
                self.log.debug('sending LogoutRequest %r to URL %r', logout.request.dump(), logout.msgUrl)
831
                response = HttpResponseRedirect(logout.msgUrl)
832
                response.set_cookie(
833
                    self.next_url_cookie_name(logout.msgRelayState),
834
                    value=next_url,
835
                    max_age=600,
836
                    samesite='Lax',
837
                )
838
                return response
839
        return HttpResponseRedirect(next_url)
840

  
841
    @classmethod
842
    def make_logout_token_url(cls, request, next_url=None):
843
        issuer = request.session.get('mellon_session', {}).get('issuer')
844
        if not issuer:
845
            return None
846
        session_indexes = models.SessionIndex.objects.filter(
847
            saml_identifier__user=request.user, saml_identifier__issuer__entity_id=issuer
848
        ).order_by('-id')[:1]
849
        if not session_indexes:
850
            return None
851

  
852
        token_content = {
853
            'next_url': next_url,
854
            'session_index_pk': session_indexes[0].pk,
855
        }
856
        token = signing.dumps(token_content, salt=cls.TOKEN_SALT)
857
        return reverse('mellon_logout') + '?' + urlencode({'token': token})
793 858

  
794 859

  
795 860
logout = csrf_exempt(LogoutView.as_view())
tests/test_sso_slo.py
864 864
    response = app.get(url)
865 865
    assert len(caplog.records) == 0, 'logout failed'
866 866
    assert response.location == '/'
867

  
868

  
869
def test_sso_slo_token(db, app, rf, idp, caplog, django_user_model):
870
    from mellon.views import LogoutView
871

  
872
    caplog.set_level(logging.WARNING)
873
    response = app.get('/login/')
874
    url, body, relay_state = idp.process_authn_request_redirect(response['Location'])
875
    response = app.post('/login/', params={'SAMLResponse': body, 'RelayState': relay_state})
876

  
877
    request = rf.get('/whatever/')
878
    request.session = app.session
879
    request.user = django_user_model.objects.get()
880
    token_logout_url = LogoutView.make_logout_token_url(request, next_url='/somepath/')
881
    assert token_logout_url
882
    app.session.flush()
883
    assert '_auth_user_id' not in app.session
884
    response = app.get(token_logout_url)
885
    assert urlparse.urlparse(response['Location']).path == '/singleLogout'
886
    url = idp.process_logout_request_redirect(response.location)
887
    caplog.clear()
888
    response = app.get(url)
889
    assert len(caplog.records) == 0, 'logout failed'
890
    assert response.location == '/somepath/'
867
-