0002-prevent-redirection-loop-on-artifact-resolution-erro.patch
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" %} : {{ idp_message }}</p>{% endif %}
|
|
15 |
{% if reason %}<p class="mellon-reason">{% trans "Reason" %} : {{ 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 |
- |