Projet

Général

Profil

0001-qommon-saml2-on-artifact-resolution-error-show-an-er.patch

Benjamin Dauvergne, 18 mai 2021 16:44

Télécharger (8,44 ko)

Voir les différences:

Subject: [PATCH] qommon/saml2: on artifact resolution error, show an error
 page with a retry button (#53362)

 tests/test_saml_auth.py                     | 58 +++++++++++++++++++--
 wcs/qommon/saml2.py                         | 27 +++++++---
 wcs/qommon/templates/qommon/saml-error.html | 10 ++++
 3 files changed, 84 insertions(+), 11 deletions(-)
 create mode 100644 wcs/qommon/templates/qommon/saml-error.html
tests/test_saml_auth.py
10 10
except ImportError:
11 11
    lasso = None
12 12

  
13
import mock
13 14
import pytest
14 15
from quixote import get_session_manager
15 16
from quixote.errors import RequestError
......
18 19
from wcs.qommon.http_request import HTTPRequest
19 20
from wcs.qommon.ident.idp import MethodAdminDirectory
20 21
from wcs.qommon.misc import get_lasso_server
21
from wcs.qommon.saml2 import Saml2Directory
22
from wcs.qommon.saml2 import Saml2Directory, SOAPException
22 23

  
23 24
from .test_hobo_notify import PROFILE
24 25
from .utilities import clean_temporary_pub, create_temporary_pub, get_app
......
115 116
    assert 'rsa-sha256' in req.response.headers['location']
116 117

  
117 118

  
118
def get_authn_response_msg(pub, ni_format=lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT):
119
def get_authn_response_msg(
120
    pub,
121
    ni_format=lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT,
122
    protocol_binding=lasso.SAML2_METADATA_BINDING_POST,
123
):
119 124
    idp_metadata_filepath = os.path.join(pub.app_dir, 'idp-http-sso.example.net-saml2-metadata-metadata.xml')
120 125
    idp_key_filepath = os.path.join(pub.app_dir, 'idp-http-sso.example.net-saml2-metadata-privatekey.pem')
121 126
    idp = lasso.Server(idp_metadata_filepath, idp_key_filepath, None, None)
......
128 133
    login.initIdpInitiatedAuthnRequest(pub.cfg['sp']['saml2_providerid'])
129 134
    login.request.nameIDPolicy.format = ni_format
130 135
    login.request.nameIDPolicy.allowCreate = True
131
    login.request.protocolBinding = lasso.SAML2_METADATA_BINDING_POST
136
    login.request.protocolBinding = protocol_binding
132 137
    login.processAuthnRequestMsg(None)
133 138
    login.validateRequestMsg(True, True)
134 139
    login.buildAssertion(
......
178 183
    attributes.append(role_slug_attribute)
179 184
    login.assertion.attributeStatement[0].attribute = attributes
180 185

  
181
    login.buildAuthnResponseMsg()
182
    return login.msgBody
186
    if protocol_binding == lasso.SAML2_METADATA_BINDING_POST:
187
        login.buildAuthnResponseMsg()
188
        return login.msgBody
189
    else:
190
        login.buildArtifactMsg(lasso.HTTP_METHOD_ARTIFACT_GET)
191
        return login.msgUrl
183 192

  
184 193

  
185 194
def get_assertion_consumer_request(pub, ni_format=lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT):
......
340 349
        saml2.assertionConsumerPost()
341 350

  
342 351

  
352
def test_assertion_consumer_artifact_error(pub):
353
    def get_assertion_consumer_request(pub, ni_format=lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT):
354
        msg_url = get_authn_response_msg(pub, protocol_binding=lasso.SAML2_METADATA_BINDING_ARTIFACT)
355
        artifact = urllib.parse.parse_qs(urllib.parse.urlparse(msg_url).query)['SAMLart'][0]
356
        req = HTTPRequest(
357
            None,
358
            {
359
                'SERVER_NAME': 'example.net',
360
                'SCRIPT_NAME': '',
361
                'PATH_INFO': '/saml/assertionConsumerArtifact',
362
                'QUERY_STRING': urllib.parse.urlencode(
363
                    {'SAMLart': artifact, 'RelayState': '/foobar/?test=ok'}
364
                ),
365
            },
366
        )
367
        req.process_inputs()
368
        pub._set_request(req)
369
        pub.session_class.wipe()
370
        req.session = pub.session_class(id=1)
371
        assert req.session.user is None
372
        return req
373

  
374
    with mock.patch('wcs.qommon.saml2.soap_call', side_effet=SOAPException()):
375
        req = get_assertion_consumer_request(pub)
376
    saml2 = Saml2Directory()
377
    saml2.assertionConsumerArtifact()
378
    assert req.response.status_code == 302
379
    assert req.response.headers['location'] == 'http://example.net/saml/error?RelayState=/foobar/%3Ftest%3Dok'
380

  
381

  
382
def test_saml_error_page(pub):
383
    resp = get_app(pub).get('/saml/error?RelayState=/foobar/%3Ftest%3Dok')
384
    resp = resp.form.submit()
385
    assert resp.status_int == 302
386
    assert urllib.parse.parse_qs(urllib.parse.urlparse(resp.location).query)['RelayState'] == [
387
        '/foobar/?test=ok'
388
    ]
389

  
390

  
343 391
def test_saml_login_page(pub):
344 392
    resp = get_app(pub).get('/login/')
345 393
    assert resp.status_int == 302
wcs/qommon/saml2.py
41 41

  
42 42
from . import _, errors, force_str, misc
43 43
from .publisher import get_cfg, get_logger
44
from .template import error_page
44
from .template import QommonTemplateResponse, error_page, html_top
45 45

  
46 46

  
47 47
class SOAPException(Exception):
......
120 120
        'metadata',
121 121
        ('metadata.xml', 'metadata'),
122 122
        'public_key',
123
        'error',
123 124
    ]
124 125

  
125 126
    def _q_traverse(self, path):
......
168 169
    def login(self):
169 170
        return self.perform_login()
170 171

  
171
    def perform_login(self, idp=None):
172
    def perform_login(self, idp=None, relay_state=None):
172 173
        server = misc.get_lasso_server()
173 174
        if not server:
174 175
            return error_page(_('SAML 2.0 support not yet configured.'))
......
185 186
        login.request.forceAuthn = get_request().form.get('forceAuthn') == 'true'
186 187
        login.request.isPassive = get_request().form.get('IsPassive') == 'true'
187 188
        login.request.consent = 'urn:oasis:names:tc:SAML:2.0:consent:current-implicit'
188
        if isinstance(get_request().form.get('next'), str):
189
            login.msgRelayState = get_request().form.get('next')
190 189

  
191
        next_url = login.msgRelayState or get_publisher().get_frontoffice_url()
190
        if not relay_state and isinstance(get_request().form.get('next'), str):
191
            relay_state = get_request().form.get('next')
192
        if relay_state:
193
            login.msgRelayState = relay_state
194

  
195
        next_url = relay_state or get_publisher().get_frontoffice_url()
192 196
        parsed_url = urllib.parse.urlparse(next_url)
193 197
        request = get_request()
194 198
        scheme = parsed_url.scheme or request.get_scheme()
......
242 246
        try:
243 247
            soap_answer = soap_call(login.msgUrl, login.msgBody, client_cert=client_cert)
244 248
        except SOAPException:
245
            return error_page(_('Failure to communicate with identity provider'))
249
            relay_state = request.form.get('RelayState', None)
250
            path = '/saml/error'
251
            if relay_state:
252
                path += '?RelayState=' + urllib.parse.quote(relay_state)
253
            return redirect(path)
246 254

  
247 255
        try:
248 256
            login.processResponseMsg(force_str(soap_answer))
......
786 794
        publickey = open(misc.get_abs_path(get_cfg('sp')['publickey'])).read()
787 795
        return publickey
788 796

  
797
    def error(self):
798
        request = get_request()
799
        if request.get_method() == 'POST':
800
            return self.perform_login(relay_state=request.form.get('RelayState'))
801
        html_top(title=_('Authentication error'))
802
        return QommonTemplateResponse(templates=['qommon/saml-error.html'], context={})
803

  
789 804
    # retain compatibility with old metadatas
790 805
    singleSignOnArtifact = assertionConsumerArtifact
791 806
    singleSignOnPost = assertionConsumerPost
wcs/qommon/templates/qommon/saml-error.html
1
{% extends template_base %}
2
{% load i18n %}
3

  
4
{% block body %}
5
<div class="warningnotice">{% trans "There was a temporary error during your authentication, please retry later." %}</div>
6

  
7
<form method="post">
8
    <button>{% trans "Retry" %}</button>
9
</form>
10
{% endblock %}
0
-