Project

General

Profile

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

Benjamin Dauvergne, 07 Mar 2018 05:20 PM

Download (11.9 KB)

View differences:

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

Signature of method sso_failure() is changed as name of a context variable for
template mellon/authentication_failed.html (idp_message => reason).
 mellon/locale/fr/LC_MESSAGES/django.po             |  9 +---
 mellon/templates/mellon/authentication_failed.html |  2 +-
 mellon/views.py                                    | 39 +++++++++-----
 tests/test_sso_slo.py                              | 62 +++++++++++++++++++---
 4 files changed, 86 insertions(+), 26 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
19 19

  
20 20
from . import app_settings, utils
21 21

  
22
RETRY_LOGIN_COOKIE = 'MELLON_RETRY_LOGIN'
22 23

  
23 24
lasso.setFlag('thin-sessions')
24 25

  
......
126 127
            if 'RelayState' in request.POST and utils.is_nonnull(request.POST['RelayState']):
127 128
                login.msgRelayState = request.POST['RelayState']
128 129
            return self.sso_success(request, login)
129
        return self.sso_failure(request, login, idp_message, status_codes)
130
        return self.sso_failure(request, reason=idp_message, status_codes=status_codes)
130 131

  
131
    def sso_failure(self, request, login, idp_message, status_codes):
132
    def sso_failure(self, request, reason='', status_codes=()):
132 133
        '''show error message to user after a login failure'''
134
        login = self.profile
133 135
        idp = utils.get_idp(login.remoteProviderId)
134 136
        error_url = utils.get_setting(idp, 'ERROR_URL')
135 137
        error_redirect_after_timeout = utils.get_setting(idp, 'ERROR_REDIRECT_AFTER_TIMEOUT')
136 138
        if error_url:
137 139
            error_url = resolve_url(error_url)
138
        next_url = error_url or login.msgRelayState or resolve_url(settings.LOGIN_REDIRECT_URL)
140
        next_url = error_url or self.get_next_url(default=resolve_url(settings.LOGIN_REDIRECT_URL))
139 141
        return render(request, 'mellon/authentication_failed.html',
140 142
                      {
141 143
                          'debug': settings.DEBUG,
142
                          'idp_message': idp_message,
144
                          'reason': reason,
143 145
                          'status_codes': status_codes,
144 146
                          'issuer': login.remoteProviderId,
145 147
                          'next_url': next_url,
146
                          'error_url': error_url,
147 148
                          'relaystate': login.msgRelayState,
148 149
                          'error_redirect_after_timeout': error_redirect_after_timeout,
149 150
                      })
......
186 187
                attributes['authn_context_class_ref'] = \
187 188
                    authn_context.authnContextClassRef
188 189
        self.log.debug('trying to authenticate with attributes %r', attributes)
189
        return self.authenticate(request, login, attributes)
190
        response = self.authenticate(request, login, attributes)
191
        response.delete_cookie(RETRY_LOGIN_COOKIE)
192
        return response
190 193

  
191 194
    def authenticate(self, request, login, attributes):
192 195
        user = auth.authenticate(saml_attributes=attributes)
......
217 220
        return HttpResponseRedirect(next_url)
218 221

  
219 222
    def retry_login(self):
220
        '''Retry login if it failed for a temporary error'''
223
        '''Retry login if it failed for a temporary error.
224

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

  
227 241
    def continue_sso_artifact(self, request, method):
228 242
        idp_message = None
......
264 278
                                   verify=verify_ssl_certificate)
265 279
        except RequestException as e:
266 280
            self.log.warning('unable to reach %r: %s', login.msgUrl, e)
267
            return self.sso_failure(request, login, _('IdP is temporarily down, please try again '
268
                                                      'later.'), status_codes)
281
            return self.sso_failure(request,
282
                                    reason=_('IdP is temporarily down, please try again ' 'later.'),
283
                                    status_codes=status_codes)
269 284
        if result.status_code != 200:
270 285
            self.log.warning('SAML authentication failed: IdP returned %s when given artifact: %r',
271 286
                             result.status_code, result.content)
272
            return self.sso_failure(request, login, idp_message, status_codes)
287
            return self.sso_failure(request, reason=idp_message, status_codes=status_codes)
273 288

  
274 289
        self.log.info('Got SAML Artifact Response', extra={'saml_response': result.content})
275 290
        try:
......
310 325
            return HttpResponseBadRequest('error processing the authentication response: %r' % e)
311 326
        else:
312 327
            return self.sso_success(request, login)
313
        return self.sso_failure(request, login, idp_message, status_codes)
328
        return self.sso_failure(request, login, reason=idp_message, status_codes=status_codes)
314 329

  
315 330
    def request_discovery_service(self, request, is_passive=False):
316 331
        self_url = request.build_absolute_uri(request.path)
tests/test_sso_slo.py
1 1
import lasso
2
from urlparse import urlparse
2 3

  
3 4
from pytest import fixture
4 5

  
......
110 111
    response = app.post(reverse('mellon_login'), params={'SAMLResponse': body})
111 112
    assert 'created new user' in caplog.text
112 113
    assert 'logged in using SAML' in caplog.text
113
    assert response['Location'].endswith(sp_settings.LOGIN_REDIRECT_URL)
114
    assert urlparse(response['Location']).path == sp_settings.LOGIN_REDIRECT_URL
114 115

  
115 116

  
116 117
def test_sso(db, app, idp, caplog, sp_settings):
......
120 121
    response = app.post(reverse('mellon_login'), params={'SAMLResponse': body})
121 122
    assert 'created new user' in caplog.text
122 123
    assert 'logged in using SAML' in caplog.text
123
    assert response['Location'].endswith(sp_settings.LOGIN_REDIRECT_URL)
124
    assert urlparse(response['Location']).path == sp_settings.LOGIN_REDIRECT_URL
124 125

  
125 126

  
126 127
def test_sso_request_denied(db, app, idp, caplog, sp_settings):
......
147 148
        response = app.get(acs_artifact_url)
148 149
    assert 'created new user' in caplog.text
149 150
    assert 'logged in using SAML' in caplog.text
150
    assert response['Location'].endswith(sp_settings.LOGIN_REDIRECT_URL)
151
    assert urlparse(response['Location']).path == sp_settings.LOGIN_REDIRECT_URL
151 152
    # force delog
153
    assert app.session
152 154
    app.session.flush()
153 155
    assert 'dead artifact' not in caplog.text
154 156
    with HTTMock(idp.mock_artifact_resolver()):
155 157
        response = app.get(acs_artifact_url)
156 158
    # verify retry login was asked
157 159
    assert 'dead artifact' in caplog.text
158
    assert response.status_code == 302
159
    assert reverse('mellon_login') in url
160
    assert urlparse(response['Location']).path == reverse('mellon_login')
160 161
    response = response.follow()
161 162
    url, body = idp.process_authn_request_redirect(response['Location'])
162 163
    reset_caplog(caplog)
......
170 171
        response = app.get(acs_artifact_url)
171 172
    assert 'created new user' in caplog.text
172 173
    assert 'logged in using SAML' in caplog.text
173
    assert response['Location'].endswith(sp_settings.LOGIN_REDIRECT_URL)
174
    assert urlparse(response['Location']).path == sp_settings.LOGIN_REDIRECT_URL
175

  
176

  
177
def test_sso_artifact_no_loop(db, app, caplog, sp_settings, idp_metadata, idp_private_key, rf):
178
    sp_settings.MELLON_DEFAULT_ASSERTION_CONSUMER_BINDING = 'artifact'
179
    request = rf.get('/')
180
    sp_metadata = create_metadata(request)
181
    idp = MockIdp(idp_metadata, idp_private_key, sp_metadata)
182
    response = app.get(reverse('mellon_login'))
183
    url, body = idp.process_authn_request_redirect(response['Location'])
184
    assert body is None
185
    assert reverse('mellon_login') in url
186
    assert 'SAMLart' in url
187
    acs_artifact_url = url.split('testserver', 1)[1]
188

  
189
    # forget the artifact
190
    idp.artifact = ''
191

  
192
    with HTTMock(idp.mock_artifact_resolver()):
193
        response = app.get(acs_artifact_url)
194
    assert 'MELLON_RETRY_LOGIN=1;' in response['Set-Cookie']
195

  
196
    # first error, we retry
197
    assert urlparse(response['Location']).path == reverse('mellon_login')
198

  
199
    # check we are not logged
200
    assert not app.session
201

  
202
    # redo
203
    response = app.get(reverse('mellon_login'))
204
    url, body = idp.process_authn_request_redirect(response['Location'])
205
    assert body is None
206
    assert reverse('mellon_login') in url
207
    assert 'SAMLart' in url
208
    acs_artifact_url = url.split('testserver', 1)[1]
209

  
210
    # forget the artifact
211
    idp.artifact = ''
212
    with HTTMock(idp.mock_artifact_resolver()):
213
        response = app.get(acs_artifact_url)
214

  
215
    # check cookie is deleted after failed retry
216
    assert 'MELLON_RETRY_LOGIN=;' in response['Set-Cookie']
217
    assert 'Location' not in response
218

  
219
    # check we are still not logged
220
    assert not app.session
221

  
222
    # check return url is in page
223
    assert '"%s"' % sp_settings.LOGIN_REDIRECT_URL in response.content
174
-