22 |
22 |
from io import StringIO
|
23 |
23 |
from xml.sax.saxutils import escape
|
24 |
24 |
|
25 |
|
import django.http
|
26 |
25 |
import lasso
|
27 |
26 |
import requests
|
28 |
27 |
from django.conf import settings
|
... | ... | |
57 |
56 |
User = get_user_model()
|
58 |
57 |
|
59 |
58 |
|
60 |
|
class HttpResponseBadRequest(django.http.HttpResponseBadRequest):
|
61 |
|
def __init__(self, *args, **kwargs):
|
62 |
|
kwargs['content_type'] = kwargs.get('content_type', 'text/plain')
|
63 |
|
super().__init__(*args, **kwargs)
|
64 |
|
self['X-Content-Type-Options'] = 'nosniff'
|
65 |
|
|
66 |
|
|
67 |
59 |
class LogMixin:
|
68 |
60 |
"""Initialize a module logger in new objects"""
|
69 |
61 |
|
... | ... | |
93 |
85 |
class ProfileMixin:
|
94 |
86 |
profile = None
|
95 |
87 |
|
|
88 |
def dispatch(self, request, *args, **kwargs):
|
|
89 |
try:
|
|
90 |
return super().dispatch(request, *args, **kwargs)
|
|
91 |
# handle common errors
|
|
92 |
except utils.CreateServerError:
|
|
93 |
return self.failure(
|
|
94 |
request,
|
|
95 |
reason=_(
|
|
96 |
'Unable to initialize a SAML server object, the private key '
|
|
97 |
'is maybe invalid or unreadable, please check its access '
|
|
98 |
'rights and content.'
|
|
99 |
),
|
|
100 |
)
|
|
101 |
|
96 |
102 |
def set_next_url(self, next_url):
|
97 |
103 |
if not check_next_url(self.request, next_url):
|
98 |
104 |
return
|
... | ... | |
124 |
130 |
def get_next_url(self, default=None):
|
125 |
131 |
return self.get_state('next_url', default=default)
|
126 |
132 |
|
127 |
|
def show_message_status_is_not_success(self, profile, prefix):
|
|
133 |
@property
|
|
134 |
def template_base(self):
|
|
135 |
return self.kwargs.get('template_base', 'base.html')
|
|
136 |
|
|
137 |
def render(self, request, template_names, context, status=None):
|
|
138 |
context['template_base'] = self.template_base
|
|
139 |
if 'context_hook' in self.kwargs:
|
|
140 |
self.kwargs['context_hook'](context)
|
|
141 |
return render(request, template_names, context, status=status)
|
|
142 |
|
|
143 |
def failure_status_is_not_success(self, request, profile, prefix):
|
128 |
144 |
status_codes, idp_message = utils.get_status_codes_and_message(profile)
|
129 |
145 |
args = ['%s: status is not success codes: %r', prefix, status_codes]
|
130 |
146 |
if idp_message:
|
131 |
147 |
args[0] += ' message: %s'
|
132 |
148 |
args.append(idp_message)
|
133 |
149 |
self.log.warning(*args)
|
|
150 |
return self.failure(
|
|
151 |
request,
|
|
152 |
reasons=[
|
|
153 |
_('%s refused by identity provider') % prefix,
|
|
154 |
idp_message,
|
|
155 |
_('Status codes: %s') % ', '.join([str(code) for code in status_codes]),
|
|
156 |
],
|
|
157 |
status_codes=status_codes,
|
|
158 |
)
|
134 |
159 |
|
135 |
|
def dispatch(self, request, *args, **kwargs):
|
136 |
|
try:
|
137 |
|
return super().dispatch(request, *args, **kwargs)
|
138 |
|
except utils.CreateServerError:
|
139 |
|
return self.failure(
|
140 |
|
request,
|
141 |
|
reason=_(
|
142 |
|
'Unable to initialize a SAML server object, the private key '
|
143 |
|
'is maybe invalid or unreadable, please check its access '
|
144 |
|
'rights and content.'
|
145 |
|
),
|
146 |
|
)
|
147 |
|
|
148 |
|
def failure(self, request, reason='', status_codes=()):
|
|
160 |
def failure(self, request, reason='', reasons=None, status_codes=(), exception=None, debug_details=None):
|
149 |
161 |
'''show error message to user after a login failure'''
|
150 |
|
login = self.profile
|
151 |
|
idp = utils.get_idp(login and login.remoteProviderId)
|
152 |
|
if not idp and login:
|
153 |
|
self.log.warning('entity id %r is unknown', login.remoteProviderId)
|
154 |
|
return HttpResponseBadRequest('entity id %r is unknown' % login.remoteProviderId)
|
|
162 |
reasons = reasons or []
|
|
163 |
reasons.append(reason)
|
|
164 |
profile = self.profile
|
|
165 |
idp = utils.get_idp(profile.remoteProviderId) if profile else {}
|
|
166 |
if not idp and profile and not isinstance(exception, lasso.ServerProviderNotFoundError):
|
|
167 |
self.log.warning('entity id %r is unknown', profile.remoteProviderId)
|
|
168 |
reasons.insert(
|
|
169 |
0,
|
|
170 |
_('Other error: %s')
|
|
171 |
% (_('the received message EntityID (%r) is unknown.') % profile.remoteProviderId),
|
|
172 |
)
|
|
173 |
if exception and isinstance(
|
|
174 |
exception,
|
|
175 |
(
|
|
176 |
lasso.DsError,
|
|
177 |
lasso.ProfileCannotVerifySignatureError,
|
|
178 |
lasso.LoginInvalidSignatureError,
|
|
179 |
lasso.LoginInvalidAssertionSignatureError,
|
|
180 |
),
|
|
181 |
):
|
|
182 |
reasons.append(
|
|
183 |
_(
|
|
184 |
'There was a problem with a signature. It usually means '
|
|
185 |
'that signature key has changed or is unknown. '
|
|
186 |
'The metadata of the identity provider may be out of date.'
|
|
187 |
)
|
|
188 |
)
|
|
189 |
if 'METADATA_URL' not in idp:
|
|
190 |
reasons.append(
|
|
191 |
_(
|
|
192 |
'Possible solution: you currently do not use METADATA_URL '
|
|
193 |
'to reference the metadata, use it to always keep the metadata up to date.'
|
|
194 |
)
|
|
195 |
)
|
155 |
196 |
error_url = utils.get_setting(idp, 'ERROR_URL')
|
156 |
197 |
error_redirect_after_timeout = utils.get_setting(idp, 'ERROR_REDIRECT_AFTER_TIMEOUT')
|
157 |
198 |
if error_url:
|
... | ... | |
162 |
203 |
'mellon/authentication_failed.html',
|
163 |
204 |
{
|
164 |
205 |
'debug': settings.DEBUG,
|
165 |
|
'reason': reason,
|
|
206 |
'reasons': reasons,
|
166 |
207 |
'status_codes': status_codes,
|
167 |
|
'issuer': login and login.remoteProviderId,
|
|
208 |
'issuer': profile.remoteProviderId if profile else None,
|
168 |
209 |
'next_url': next_url,
|
169 |
|
'relaystate': login and login.msgRelayState,
|
|
210 |
'relaystate': profile.msgRelayState if profile else None,
|
170 |
211 |
'error_redirect_after_timeout': error_redirect_after_timeout,
|
|
212 |
'debug_details': debug_details or {},
|
171 |
213 |
},
|
|
214 |
status=400,
|
172 |
215 |
)
|
173 |
216 |
|
174 |
217 |
|
... | ... | |
178 |
221 |
with self.capture_logs() if self.debug_login else nullcontext():
|
179 |
222 |
return super().dispatch(request, *args, **kwargs)
|
180 |
223 |
|
181 |
|
@property
|
182 |
|
def template_base(self):
|
183 |
|
return self.kwargs.get('template_base', 'base.html')
|
184 |
|
|
185 |
|
def render(self, request, template_names, context):
|
186 |
|
context['template_base'] = self.template_base
|
187 |
|
if 'context_hook' in self.kwargs:
|
188 |
|
self.kwargs['context_hook'](context)
|
189 |
|
return render(request, template_names, context)
|
190 |
|
|
191 |
|
def get_idp(self, request):
|
192 |
|
entity_id = request.POST.get('entityID') or request.GET.get('entityID')
|
|
224 |
def get_idp(self, entity_id):
|
193 |
225 |
if not entity_id:
|
194 |
226 |
for idp in utils.get_idps():
|
195 |
227 |
return idp
|
... | ... | |
202 |
234 |
'''Assertion consumer'''
|
203 |
235 |
if 'SAMLart' in request.POST:
|
204 |
236 |
if 'artifact' not in app_settings.ASSERTION_CONSUMER_BINDINGS:
|
205 |
|
raise Http404('artifact binding is not supported')
|
|
237 |
raise self.failure(request, _('Artifact binding is not supported'))
|
206 |
238 |
return self.continue_sso_artifact(request, lasso.HTTP_METHOD_ARTIFACT_POST)
|
207 |
239 |
if 'SAMLResponse' not in request.POST:
|
208 |
240 |
if 'post' not in app_settings.ASSERTION_CONSUMER_BINDINGS:
|
209 |
|
raise Http404('post binding is not supported')
|
|
241 |
raise self.failure(request, _('Post binding is not supported'))
|
210 |
242 |
return self.get(request, *args, **kwargs)
|
|
243 |
# prevent null characters in SAMLResponse
|
211 |
244 |
if not utils.is_nonnull(request.POST['SAMLResponse']):
|
212 |
|
return HttpResponseBadRequest('SAMLResponse contains a null character')
|
|
245 |
return self.failure(_('SAMLResponse contains a null character'))
|
213 |
246 |
self.log.info('Got SAML Response', extra={'saml_response': request.POST['SAMLResponse']})
|
214 |
247 |
self.profile = login = utils.create_login(request)
|
215 |
|
idp_message = None
|
216 |
|
status_codes = []
|
217 |
|
# prevent null characters in SAMLResponse
|
218 |
248 |
try:
|
219 |
249 |
login.processAuthnResponseMsg(request.POST['SAMLResponse'])
|
220 |
250 |
login.acceptSso()
|
221 |
|
except lasso.ProfileCannotVerifySignatureError:
|
222 |
|
self.log.warning(
|
223 |
|
'SAML authentication failed: signature validation failed for %r', login.remoteProviderId
|
224 |
|
)
|
225 |
|
except lasso.ParamError:
|
226 |
|
self.log.exception('lasso param error')
|
227 |
251 |
except (
|
228 |
252 |
lasso.LoginStatusNotSuccessError,
|
229 |
253 |
lasso.ProfileStatusNotSuccessError,
|
230 |
254 |
lasso.ProfileRequestDeniedError,
|
231 |
255 |
):
|
232 |
|
self.show_message_status_is_not_success(login, 'SAML authentication failed')
|
233 |
|
except lasso.Error as e:
|
234 |
|
return HttpResponseBadRequest('error processing the authentication response: %r' % e)
|
235 |
|
else:
|
236 |
|
if 'RelayState' in request.POST and utils.is_nonnull(request.POST['RelayState']):
|
237 |
|
login.msgRelayState = request.POST['RelayState']
|
238 |
|
return self.sso_success(request, login)
|
239 |
|
return self.failure(request, reason=idp_message, status_codes=status_codes)
|
|
256 |
return self.failure_status_is_not_success(request, login, _('Login'))
|
|
257 |
except Exception as e:
|
|
258 |
return self.failure(request, _('Technical error'), exception=e)
|
|
259 |
if 'RelayState' in request.POST and utils.is_nonnull(request.POST['RelayState']):
|
|
260 |
login.msgRelayState = request.POST['RelayState']
|
|
261 |
return self.sso_success(request, login)
|
240 |
262 |
|
241 |
263 |
def get_attribute_value(self, attribute, attribute_value):
|
242 |
264 |
# check attribute_value contains only text
|
... | ... | |
369 |
391 |
return response
|
370 |
392 |
|
371 |
393 |
def continue_sso_artifact(self, request, method):
|
372 |
|
idp_message = None
|
373 |
|
status_codes = []
|
374 |
|
|
375 |
394 |
if method == lasso.HTTP_METHOD_ARTIFACT_GET:
|
376 |
395 |
message = request.META['QUERY_STRING']
|
377 |
396 |
artifact = request.GET['SAMLart']
|
... | ... | |
386 |
405 |
login.msgRelayState = relay_state
|
387 |
406 |
try:
|
388 |
407 |
login.initRequest(message, method)
|
389 |
|
except lasso.ProfileInvalidArtifactError:
|
|
408 |
except lasso.ProfileInvalidArtifactError as e:
|
390 |
409 |
self.log.warning('artifact is malformed %r', artifact)
|
391 |
|
return HttpResponseBadRequest('artifact is malformed %r' % artifact)
|
392 |
|
except lasso.ServerProviderNotFoundError:
|
|
410 |
return self.failure(request, _('Artifact is invalid.'), exception=e)
|
|
411 |
except lasso.ServerProviderNotFoundError as e:
|
393 |
412 |
self.log.warning('no entity id found for artifact %s', artifact)
|
394 |
|
return HttpResponseBadRequest('no entity id found for this artifact %r' % artifact)
|
|
413 |
return self.failure(
|
|
414 |
request,
|
|
415 |
_('the received message EntityID (%r) is unknown.') % login.remoteProviderId,
|
|
416 |
exception=e,
|
|
417 |
)
|
395 |
418 |
idp = utils.get_idp(login.remoteProviderId)
|
396 |
419 |
if not idp:
|
397 |
|
return HttpResponseBadRequest('entity id %r is unknown' % login.remoteProviderId)
|
|
420 |
return self.failure(
|
|
421 |
request,
|
|
422 |
_('the received message EntityID (%r) is unknown.') % login.remoteProviderId,
|
|
423 |
)
|
398 |
424 |
verify_ssl_certificate = utils.get_setting(idp, 'VERIFY_SSL_CERTIFICATE')
|
399 |
425 |
login.buildRequestMsg()
|
400 |
426 |
try:
|
... | ... | |
407 |
433 |
)
|
408 |
434 |
except RequestException as e:
|
409 |
435 |
self.log.warning('unable to reach %r: %s', login.msgUrl, e)
|
410 |
|
return self.failure(
|
411 |
|
request,
|
412 |
|
reason=_('IdP is temporarily down, please try again ' 'later.'),
|
413 |
|
status_codes=status_codes,
|
414 |
|
)
|
|
436 |
return self.failure(request, reason=_('IdP is temporarily down, please try again later.'))
|
415 |
437 |
if result.status_code != 200:
|
416 |
438 |
self.log.warning(
|
417 |
439 |
'SAML authentication failed: IdP returned %s when given artifact: %r',
|
418 |
440 |
result.status_code,
|
419 |
441 |
result.content,
|
420 |
442 |
)
|
421 |
|
return self.failure(request, reason=idp_message, status_codes=status_codes)
|
|
443 |
return self.failure(request, reason=_('IdP is temporarily down, please try again later.'))
|
422 |
444 |
|
423 |
445 |
self.log.info('Got SAML Artifact Response', extra={'saml_response': result.content})
|
424 |
446 |
result.encoding = utils.get_xml_encoding(result.content)
|
... | ... | |
429 |
451 |
# artifact is invalid, idp returned no response
|
430 |
452 |
self.log.warning('ArtifactResolveResponse is empty: dead artifact %r', artifact)
|
431 |
453 |
return self.retry_login()
|
432 |
|
except lasso.ProfileInvalidMsgError:
|
|
454 |
except lasso.ProfileInvalidMsgError as e:
|
433 |
455 |
self.log.warning('ArtifactResolveResponse is malformed %r', result.content[:200])
|
434 |
|
if settings.DEBUG:
|
435 |
|
return HttpResponseBadRequest('ArtififactResolveResponse is malformed\n%r' % result.content)
|
436 |
|
else:
|
437 |
|
return HttpResponseBadRequest('ArtififactResolveResponse is malformed')
|
438 |
|
except lasso.ProfileCannotVerifySignatureError:
|
439 |
|
self.log.warning(
|
440 |
|
'SAML authentication failed: signature validation failed for %r', login.remoteProviderId
|
|
456 |
return self.failure(
|
|
457 |
request,
|
|
458 |
_('Could not process the ArtifactResolveResponse'),
|
|
459 |
exception=e,
|
441 |
460 |
)
|
442 |
|
except lasso.ParamError:
|
443 |
|
self.log.exception('lasso param error')
|
444 |
461 |
except (
|
445 |
462 |
lasso.LoginStatusNotSuccessError,
|
446 |
463 |
lasso.ProfileStatusNotSuccessError,
|
447 |
464 |
lasso.ProfileRequestDeniedError,
|
448 |
465 |
):
|
449 |
|
status = login.response.status
|
450 |
|
a = status
|
451 |
|
while a.statusCode:
|
452 |
|
status_codes.append(a.statusCode.value)
|
453 |
|
a = a.statusCode
|
454 |
|
args = ['SAML authentication failed: status is not success codes: %r', status_codes]
|
455 |
|
if status.statusMessage:
|
456 |
|
idp_message = lasso_decode(status.statusMessage)
|
457 |
|
args[0] += ' message: %r'
|
458 |
|
args.append(status.statusMessage)
|
459 |
|
self.log.warning(*args)
|
460 |
|
except lasso.Error as e:
|
461 |
|
self.log.exception('unexpected lasso error')
|
462 |
|
return HttpResponseBadRequest('error processing the authentication response: %r' % e)
|
|
466 |
return self.failure_status_is_not_success(request, login, _('Login'))
|
|
467 |
except Exception as e:
|
|
468 |
return self.failure(
|
|
469 |
request,
|
|
470 |
_('Technical error'),
|
|
471 |
exception=e,
|
|
472 |
debug_details={'ArtifactResolveResponse_content': result.content},
|
|
473 |
)
|
463 |
474 |
else:
|
464 |
475 |
return self.sso_success(request, login)
|
465 |
|
return self.failure(request, reason=idp_message, status_codes=status_codes)
|
466 |
476 |
|
467 |
477 |
def request_discovery_service(self, request, is_passive=False):
|
468 |
478 |
return_url = request.build_absolute_uri()
|
... | ... | |
495 |
505 |
return self.request_discovery_service(request, is_passive=request.GET.get('passive') == '1')
|
496 |
506 |
|
497 |
507 |
next_url = check_next_url(self.request, request.GET.get(REDIRECT_FIELD_NAME))
|
498 |
|
idp = self.get_idp(request)
|
|
508 |
entity_id = request.POST.get('entityID') or request.GET.get('entityID')
|
|
509 |
idp = self.get_idp(entity_id)
|
499 |
510 |
if not idp:
|
500 |
|
return HttpResponseBadRequest('no idp found')
|
|
511 |
return self.failure(request, _('No idp found for "%s"') % (entity_id or ''))
|
501 |
512 |
self.profile = login = utils.create_login(request)
|
502 |
513 |
self.log.debug('authenticating to %r', idp['ENTITY_ID'])
|
503 |
514 |
try:
|
... | ... | |
550 |
561 |
self.add_login_hints(idp, authn_request, request=request, next_url=next_url or '/')
|
551 |
562 |
login.buildAuthnRequestMsg()
|
552 |
563 |
except lasso.Error as e:
|
553 |
|
return HttpResponseBadRequest('error initializing the authentication request: %r' % e)
|
|
564 |
return self.failure(request, _('Could not initialize the authentication request'), exception=e)
|
554 |
565 |
self.log.debug('sending authn request %r', authn_request.dump())
|
555 |
566 |
self.log.debug('to url %r', login.msgUrl)
|
556 |
567 |
return HttpResponseRedirect(login.msgUrl)
|
... | ... | |
661 |
672 |
try:
|
662 |
673 |
logout.processRequestMsg(msg)
|
663 |
674 |
except lasso.Error as e:
|
664 |
|
return HttpResponseBadRequest('error processing logout request: %r' % e)
|
|
675 |
return self.failure(request, _('Could not process the logout request'), exception=e)
|
665 |
676 |
|
666 |
677 |
entity_id = force_text(logout.remoteProviderId)
|
667 |
678 |
session_indexes = {force_text(sessionIndex) for sessionIndex in logout.request.sessionIndexes}
|
... | ... | |
706 |
717 |
try:
|
707 |
718 |
logout.buildResponseMsg()
|
708 |
719 |
except lasso.Error as e:
|
709 |
|
return HttpResponseBadRequest('error processing logout request: %r' % e)
|
|
720 |
return self.failure(request, _('Could not process the logout response'), exception=e)
|
710 |
721 |
if logout.msgBody:
|
711 |
722 |
return HttpResponse(force_text(logout.msgBody), content_type='text/xml')
|
712 |
723 |
else:
|
... | ... | |
759 |
770 |
try:
|
760 |
771 |
logout.processResponseMsg(request.META['QUERY_STRING'])
|
761 |
772 |
except lasso.ProfileStatusNotSuccessError:
|
762 |
|
self.show_message_status_is_not_success(logout, 'SAML logout failed')
|
|
773 |
return self.failure_status_is_not_success(request, logout, _('Logout'))
|
763 |
774 |
except lasso.LogoutPartialLogoutError:
|
764 |
775 |
self.log.warning('partial logout')
|
765 |
776 |
except lasso.Error as e:
|