0001-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 | ||
---|---|---|
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 |
- |