Projet

Général

Profil

0002-prevent-redirection-loop-on-artifact-resolution-erro.patch

Benjamin Dauvergne, 02 mars 2019 17:10

Télécharger (12,4 ko)

Voir les différences:

Subject: [PATCH 2/2] prevent redirection loop on artifact resolution errors
 (fixes #14810)

Signature of method sso_failure() is changed to match the name name of
the context variable in template mellon/authentication_failed.html
(idp_message => reason).
 mellon/locale/fr/LC_MESSAGES/django.po        |  9 +--
 .../mellon/authentication_failed.html         |  2 +-
 mellon/views.py                               | 39 +++++++----
 tests/test_sso_slo.py                         | 65 +++++++++++++++++--
 4 files changed, 88 insertions(+), 27 deletions(-)
mellon/locale/fr/LC_MESSAGES/django.po
86 86
msgid "IdP is temporarily down, please try again later."
87 87
msgstr "Le fournisseur d'identités est temporairement inaccessible, veuillez réessayer plus tard."
88 88

  
89
#~ msgid ""
90
#~ "The authentication has failed, you can return to\n"
91
#~ "    the <a href=\"%(next_url)s\">last page</a> you where.\n"
92
#~ "    "
93
#~ msgstr ""
94
#~ "L'authentification a échoué, vous pouvez retourner à <a href="
95
#~ "\"%(next_url)s\">la dernière page</a> atteinte."
89
msgid "There were too many redirections with the identity provider."
90
msgstr "Il y a eu trop de redirections avec le fournisseur d'identité."
mellon/templates/mellon/authentication_failed.html
12 12
  <h2 class="mellon-message-header">{% trans "Authentication failed" %}</h2>
13 13
  <p class="mellon-message-body">
14 14
    {% blocktrans %}The authentication has failed.{% endblocktrans %}
15
    {% if idp_message %}<p class="mellon-idp-message">{% trans "Reason" %}&nbsp;: {{ idp_message }}</p>{% endif %}
15
    {% if reason %}<p class="mellon-reason">{% trans "Reason" %}&nbsp;: {{ reason }}</p>{% endif %}
16 16
  </p>
17 17
  <p class="mellon-message-continue">
18 18
    <a class="mellon-link" href="{{ next_url }}">{% trans "Continue" %}</a>
mellon/views.py
21 21

  
22 22
from . import app_settings, utils
23 23

  
24
RETRY_LOGIN_COOKIE = 'MELLON_RETRY_LOGIN'
24 25

  
25 26
lasso.setFlag('thin-sessions')
26 27

  
......
133 134
            if 'RelayState' in request.POST and utils.is_nonnull(request.POST['RelayState']):
134 135
                login.msgRelayState = request.POST['RelayState']
135 136
            return self.sso_success(request, login)
136
        return self.sso_failure(request, login, idp_message, status_codes)
137
        return self.sso_failure(request, reason=idp_message, status_codes=status_codes)
137 138

  
138
    def sso_failure(self, request, login, idp_message, status_codes):
139
    def sso_failure(self, request, reason='', status_codes=()):
139 140
        '''show error message to user after a login failure'''
141
        login = self.profile
140 142
        idp = utils.get_idp(login.remoteProviderId)
141 143
        error_url = utils.get_setting(idp, 'ERROR_URL')
142 144
        error_redirect_after_timeout = utils.get_setting(idp, 'ERROR_REDIRECT_AFTER_TIMEOUT')
143 145
        if error_url:
144 146
            error_url = resolve_url(error_url)
145
        next_url = error_url or resolve_url(settings.LOGIN_REDIRECT_URL)
147
        next_url = error_url or self.get_next_url(default=resolve_url(settings.LOGIN_REDIRECT_URL))
146 148
        return render(request, 'mellon/authentication_failed.html',
147 149
                      {
148 150
                          'debug': settings.DEBUG,
149
                          'idp_message': idp_message,
151
                          'reason': reason,
150 152
                          'status_codes': status_codes,
151 153
                          'issuer': login.remoteProviderId,
152 154
                          'next_url': next_url,
153
                          'error_url': error_url,
154 155
                          'relaystate': login.msgRelayState,
155 156
                          'error_redirect_after_timeout': error_redirect_after_timeout,
156 157
                      })
......
193 194
                attributes['authn_context_class_ref'] = \
194 195
                    authn_context.authnContextClassRef
195 196
        self.log.debug('trying to authenticate with attributes %r', attributes)
196
        return self.authenticate(request, login, attributes)
197
        response = self.authenticate(request, login, attributes)
198
        response.delete_cookie(RETRY_LOGIN_COOKIE)
199
        return response
197 200

  
198 201
    def authenticate(self, request, login, attributes):
199 202
        user = auth.authenticate(saml_attributes=attributes)
......
224 227
        return HttpResponseRedirect(next_url)
225 228

  
226 229
    def retry_login(self):
227
        '''Retry login if it failed for a temporary error'''
230
        '''Retry login if it failed for a temporary error.
231

  
232
           Use a cookie to prevent looping forever.
233
        '''
234
        if RETRY_LOGIN_COOKIE in self.request.COOKIES:
235
            response = self.sso_failure(
236
                self.request,
237
                reason=_('There were too many redirections with the identity provider.'))
238
            response.delete_cookie(RETRY_LOGIN_COOKIE)
239
            return response
228 240
        url = reverse('mellon_login')
229 241
        next_url = self.get_next_url()
230 242
        if next_url:
231 243
            url = '%s?%s' % (url, urlencode({REDIRECT_FIELD_NAME: next_url}))
232
        return HttpResponseRedirect(url)
244
        response = HttpResponseRedirect(url)
245
        response.set_cookie(RETRY_LOGIN_COOKIE, value='1', max_age=None)
246
        return response
233 247

  
234 248
    def continue_sso_artifact(self, request, method):
235 249
        idp_message = None
......
271 285
                                   verify=verify_ssl_certificate)
272 286
        except RequestException as e:
273 287
            self.log.warning('unable to reach %r: %s', login.msgUrl, e)
274
            return self.sso_failure(request, login, _('IdP is temporarily down, please try again '
275
                                                      'later.'), status_codes)
288
            return self.sso_failure(request,
289
                                    reason=_('IdP is temporarily down, please try again ' 'later.'),
290
                                    status_codes=status_codes)
276 291
        if result.status_code != 200:
277 292
            self.log.warning('SAML authentication failed: IdP returned %s when given artifact: %r',
278 293
                             result.status_code, result.content)
279
            return self.sso_failure(request, login, idp_message, status_codes)
294
            return self.sso_failure(request, reason=idp_message, status_codes=status_codes)
280 295

  
281 296
        self.log.info('Got SAML Artifact Response', extra={'saml_response': result.content})
282 297
        result.encoding = utils.get_xml_encoding(result.content)
......
318 333
            return HttpResponseBadRequest('error processing the authentication response: %r' % e)
319 334
        else:
320 335
            return self.sso_success(request, login)
321
        return self.sso_failure(request, login, idp_message, status_codes)
336
        return self.sso_failure(request, login, reason=idp_message, status_codes=status_codes)
322 337

  
323 338
    def request_discovery_service(self, request, is_passive=False):
324 339
        self_url = request.build_absolute_uri(request.path)
tests/test_sso_slo.py
1
import re
1 2
import base64
2 3
import zlib
3 4

  
......
129 130
    response = app.post(reverse('mellon_login'), params={'SAMLResponse': body, 'RelayState': relay_state})
130 131
    assert 'created new user' in caplog.text
131 132
    assert 'logged in using SAML' in caplog.text
132
    assert response['Location'].endswith('/whatever/')
133
    assert urlparse.urlparse(response['Location']).path == '/whatever/'
133 134

  
134 135

  
135 136
def test_sso(db, app, idp, caplog, sp_settings):
......
140 141
    response = app.post(reverse('mellon_login'), params={'SAMLResponse': body, 'RelayState': relay_state})
141 142
    assert 'created new user' in caplog.text
142 143
    assert 'logged in using SAML' in caplog.text
143
    assert response['Location'].endswith(sp_settings.LOGIN_REDIRECT_URL)
144
    assert urlparse.urlparse(response['Location']).path == sp_settings.LOGIN_REDIRECT_URL
144 145

  
145 146

  
146 147
def test_sso_request_denied(db, app, idp, caplog, sp_settings):
......
173 174
        response = app.get(acs_artifact_url, params={'RelayState': relay_state})
174 175
    assert 'created new user' in caplog.text
175 176
    assert 'logged in using SAML' in caplog.text
176
    assert response['Location'].endswith('/whatever/')
177
    # force delog
177
    assert urlparse.urlparse(response['Location']).path == '/whatever/'
178
    # force delog, but keep session information for relaystate handling
179
    assert app.session
178 180
    del app.session['_auth_user_id']
179 181
    assert 'dead artifact' not in caplog.text
180 182
    with HTTMock(idp.mock_artifact_resolver()):
181 183
        response = app.get(acs_artifact_url, params={'RelayState': relay_state})
182 184
    # verify retry login was asked
183 185
    assert 'dead artifact' in caplog.text
184
    assert response.status_code == 302
185
    assert reverse('mellon_login') in url
186
    assert urlparse.urlparse(response['Location']).path == reverse('mellon_login')
186 187
    response = response.follow()
187 188
    url, body, relay_state = idp.process_authn_request_redirect(response['Location'])
188 189
    assert relay_state
......
197 198
        response = app.get(acs_artifact_url, params={'RelayState': relay_state})
198 199
    assert 'created new user' in caplog.text
199 200
    assert 'logged in using SAML' in caplog.text
200
    assert response['Location'].endswith('/whatever/')
201
    assert urlparse.urlparse(response['Location']).path == '/whatever/'
201 202

  
202 203

  
203 204
def test_sso_slo_pass_next_url(db, app, idp, caplog, sp_settings):
......
210 211
    assert 'created new user' in caplog.text
211 212
    assert 'logged in using SAML' in caplog.text
212 213
    assert response['Location'].endswith('/whatever/')
214

  
215

  
216
def test_sso_artifact_no_loop(db, app, caplog, sp_settings, idp_metadata, idp_private_key, rf):
217
    sp_settings.MELLON_DEFAULT_ASSERTION_CONSUMER_BINDING = 'artifact'
218
    request = rf.get('/')
219
    sp_metadata = create_metadata(request)
220
    idp = MockIdp(idp_metadata, idp_private_key, sp_metadata)
221
    response = app.get(reverse('mellon_login'))
222
    url, body, relay_state = idp.process_authn_request_redirect(response['Location'])
223
    assert body is None
224
    assert reverse('mellon_login') in url
225
    assert 'SAMLart' in url
226
    acs_artifact_url = url.split('testserver', 1)[1]
227

  
228
    # forget the artifact
229
    idp.artifact = ''
230

  
231
    with HTTMock(idp.mock_artifact_resolver()):
232
        response = app.get(acs_artifact_url)
233
    assert 'MELLON_RETRY_LOGIN=1;' in response['Set-Cookie']
234

  
235
    # first error, we retry
236
    assert urlparse.urlparse(response['Location']).path == reverse('mellon_login')
237

  
238
    # check we are not logged
239
    assert not app.session
240

  
241
    # redo
242
    response = app.get(reverse('mellon_login'))
243
    url, body, relay_state = idp.process_authn_request_redirect(response['Location'])
244
    assert body is None
245
    assert reverse('mellon_login') in url
246
    assert 'SAMLart' in url
247
    acs_artifact_url = url.split('testserver', 1)[1]
248

  
249
    # forget the artifact
250
    idp.artifact = ''
251
    with HTTMock(idp.mock_artifact_resolver()):
252
        response = app.get(acs_artifact_url)
253

  
254
    # check cookie is deleted after failed retry
255
    # Py3-Dj111 variation
256
    assert re.match(r'.*MELLON_RETRY_LOGIN=("")?;', response['Set-Cookie'])
257
    assert 'Location' not in response
258

  
259
    # check we are still not logged
260
    assert not app.session
261

  
262
    # check return url is in page
263
    assert '"%s"' % sp_settings.LOGIN_REDIRECT_URL in response.text
213
-