Projet

Général

Profil

0002-misc-improve-passive-sso-on-state-change-67090.patch

Benjamin Dauvergne, 23 décembre 2022 11:01

Télécharger (9,69 ko)

Voir les différences:

Subject: [PATCH 2/4] misc: improve passive sso on state change (#67090)

- automatic_sso is renamed try_passive_sso to be clearer on the goal of
  the method,
- test for possible passive sso is now done before rendering the current
  page,
- on a succesfull SSO if <idp_session_cookie_name> cookie is present,
  its value is saved in the Quixote session,
- behaviour of try_passive_sso is changed:
  - if user is logged in or <idp_session_cookie_name> cookie value
    differs from the value in '*-passive-auth-tried' cookie, the
    '*-passive-auth-tried' cookie is expired,
  - if user is logged and <idp_session_cookie_name> cookie cookie value
    differs from the one saved in the Quixote session, user is logged
    out, if not treatment stop here.
  - if the <idp_session_cookie_name> cookie cookie is not valued or if
    its value is equal to '*-passive-auth-tried' cookie, treatment stops
here,
  - if the <idp_session_cookie_name> cookie is valued and its value
    differs from the '*-passive-auth-tried' cookie cookie value, then
    '*-passive-auth-tried' cookie is set to the value of
    <idp_session_cookie_name> cookie and a passive SSO is tried.
 tests/test_saml_auth.py | 40 ++++++++++++++++++-
 wcs/qommon/saml2.py     |  5 +++
 wcs/qommon/sessions.py  |  1 +
 wcs/root.py             | 87 +++++++++++++++++++++++++----------------
 4 files changed, 99 insertions(+), 34 deletions(-)
tests/test_saml_auth.py
22 22
from wcs.qommon.misc import get_lasso_server
23 23
from wcs.qommon.saml2 import Saml2Directory, SOAPException
24 24

  
25
from .test_fc_auth import get_session
25 26
from .test_hobo_notify import PROFILE
26 27
from .utilities import clean_temporary_pub, create_temporary_pub, get_app
27 28

  
......
604 605
    cookie_store = http.cookies.SimpleCookie()
605 606
    cookie_store.load(resp.headers['Set-Cookie'])
606 607
    assert list(cookie_store.keys()) == [cookie_name]
607
    assert 'Secure' in resp.headers['Set-Cookie']
608 608
    assert 'HttpOnly' in resp.headers['Set-Cookie']
609 609
    assert 'SameSite=None' in resp.headers['Set-Cookie']
610 610
    assert 'Path=/' in resp.headers['Set-Cookie']
......
615 615
    )
616 616
    assert cookie_name in app.cookies
617 617

  
618
    # if we try again, no passive authentication occurs
619
    resp = app.get('/?parameter=value')
620
    assert resp.status_int != 302
621

  
622
    # if IDP_OPENED_SESSION is modified, then passive authentication is tried again
623
    app.set_cookie('IDP_OPENED_SESSION', '2')
624
    resp = app.get('/?parameter=value')
625
    assert resp.status_int == 302
626

  
627
    # simulate a saml login
628
    user = pub.user_class()
629
    user.store()
630
    request = mock.Mock()
631
    request.get_environ.return_value = '1.1.1.1'
632
    with mock.patch('quixote.session.get_request', return_value=request), mock.patch(
633
        'wcs.qommon.saml2', return_value=mock.Mock(cookies={'IDP_OPENED_SESSION': '2'})
634
    ):
635
        session = get_session_manager().session_class(id=None)
636
        session.set_user(user.id)
637
    session.opened_session_value = '2'
638
    session.id = 'abcd'
639
    session.store()
640
    app.set_cookie(pub.config.session_cookie_name, session.id)
641
    assert get_session(app).opened_session_value == '2'
642

  
643
    resp = app.get('/?parameter=value')
644
    assert resp.status_int == 200
645
    assert get_session(app).opened_session_value == '2'
646
    assert get_session(app).user == user.id
647
    # '*-passive-auth-tried' cookie was removed, since we logged in.
648
    assert cookie_name not in app.cookies
649

  
650
    # if the IDP_OPENED_SESSION cookie change then we are logged out
651
    app.set_cookie('IDP_OPENED_SESSION', '3')
652
    resp = app.get('/?parameter=value')
653
    assert not get_session(app)
654
    assert not get_session_manager().session_class.get(session.id, ignore_errors=True)
655

  
618 656

  
619 657
def test_no_opened_session_cookie(pub):
620 658
    app = get_app(pub)
wcs/qommon/saml2.py
377 377
        user = self.lookup_user(session, login)
378 378
        if user:
379 379
            session.set_user(user.id)
380
            # save value of idp_session_cookie_name for wcs.root.RootDirectory.try_passive_sso()
381
            idp_session_cookie_name = get_publisher().get_site_option('idp_session_cookie_name')
382
            if idp_session_cookie_name:
383
                if idp_session_cookie_name in get_request().cookies:
384
                    session.opened_session_value = get_request().cookies[idp_session_cookie_name]
380 385
        else:
381 386
            return error_page('Error associating user on SSO')
382 387
        session.lasso_identity_provider_id = login.remoteProviderId
wcs/qommon/sessions.py
88 88
    forced = False
89 89
    # should only be overwritten by authentication methods
90 90
    extra_user_variables = None
91
    opened_session_value = None
91 92

  
92 93
    username = None  # only set on password authentication
93 94

  
wcs/root.py
287 287
        self.forced_language = False
288 288
        self.feed_substitution_parts()
289 289

  
290
        output = self.try_passive_sso()
291
        if output:
292
            return output
293

  
290 294
        response = get_response()
291 295
        if not hasattr(response, 'filter'):
292 296
            response.filter = {}
......
304 308
        except errors.TraversalError:
305 309
            pass
306 310

  
307
        output = root.RootDirectory()._q_traverse(path)
308
        return self.automatic_sso(output)
311
        return root.RootDirectory()._q_traverse(path)
312

  
313
    def try_passive_sso(self):
314
        publisher = get_publisher()
315
        idp_session_cookie_name = publisher.get_site_option('idp_session_cookie_name')
316
        passive_tried_cookie_name = '%s-passive-auth-tried' % publisher.config.session_cookie_name
317

  
318
        if not idp_session_cookie_name:
319
            return
320
        ident_methods = get_cfg('identification', {}).get('methods', [])
321
        idps = get_cfg('idp', {})
322
        if len(idps) != 1:
323
            return
324
        if ident_methods and 'idp' not in ident_methods:
325
            return
309 326

  
310
    def automatic_sso(self, output):
311 327
        request = get_request()
328
        cookies = request.cookies
312 329
        response = get_response()
313 330

  
314
        publisher = get_publisher()
315
        OPENED_SESSION_COOKIE = publisher.get_site_option('idp_session_cookie_name')
316
        PASSIVE_TRIED_COOKIE = '%s-passive-auth-tried' % publisher.config.session_cookie_name
317
        if OPENED_SESSION_COOKIE not in request.cookies and PASSIVE_TRIED_COOKIE in request.cookies:
318
            response.expire_cookie(PASSIVE_TRIED_COOKIE)
319
            return output
320
        elif OPENED_SESSION_COOKIE in request.cookies and PASSIVE_TRIED_COOKIE not in request.cookies:
321
            ident_methods = get_cfg('identification', {}).get('methods', [])
322
            idps = get_cfg('idp', {})
323
            if request.user:
324
                return output
325
            if len(idps) != 1:
326
                return output
327
            if ident_methods and 'idp' not in ident_methods:
328
                return output
329
            response.set_cookie(
330
                PASSIVE_TRIED_COOKIE,
331
                '1',
332
                secure=1,
333
                httponly=1,
334
                path=publisher.config.session_cookie_path,
335
                domain=publisher.config.session_cookie_domain,
336
            )
337
            url = request.get_url()
338
            query = request.get_query()
339
            if query:
340
                url += '?' + query
341
            return root.tryauth(url)
342
        else:
343
            return output
331
        # expire passive_tried_cookie_name if already logged or if not equal to passive_tried_cookie_name
332
        if passive_tried_cookie_name in cookies and (
333
            request.user or cookies.get(passive_tried_cookie_name) != cookies.get(idp_session_cookie_name)
334
        ):
335
            response.expire_cookie(passive_tried_cookie_name)
336

  
337
        if request.user:
338
            if request.session.opened_session_value and request.session.opened_session_value != cookies.get(
339
                idp_session_cookie_name
340
            ):
341
                # logout current user if saved value for idp_session_cookie_name differs from the current one
342
                get_session_manager().expire_session()
343
                get_request()._user = ()
344
            else:
345
                # already logged, stop here.
346
                return
347
        if idp_session_cookie_name not in cookies or cookies.get(idp_session_cookie_name) == cookies.get(
348
            passive_tried_cookie_name
349
        ):
350
            # no session on the idp or passive sso already tried, stop here.
351
            return
352
        response.set_cookie(
353
            passive_tried_cookie_name,
354
            cookies.get(idp_session_cookie_name),
355
            secure=request.scheme == 'https',
356
            httponly=1,
357
            path=publisher.config.session_cookie_path,
358
            domain=publisher.config.session_cookie_domain,
359
        )
360
        url = request.get_url()
361
        query = request.get_query()
362
        if query:
363
            url += '?' + query
364
        return root.tryauth(url)
344 365

  
345 366
    def _q_lookup(self, component):
346 367
        if (
347
-