def do_logout_service(request, data, binding, config_loader_path=None, next_page=None, logout_error_template='djangosaml2/logout_error.html'): """SAML Logout Response endpoint The IdP will send the logout response to this view, which will process it with pysaml2 help and log the user out. Note that the IdP can request a logout even when we didn't initiate the process as a single logout request started by another SP. """ logger.debug('Logout service started') conf = get_config(config_loader_path, request) state = StateCache(request.session) client = Saml2Client(conf, state_cache=state, identity_cache=IdentityCache(request.session)) if 'SAMLResponse' in data: # we started the logout logger.debug('Receiving a logout response from the IdP') response = client.parse_logout_request_response( data['SAMLResponse'], binding) state.sync() return finish_logout(request, response, next_page=next_page) elif 'SAMLRequest' in data: # logout started by the IdP logger.debug('Receiving a logout request from the IdP') subject_id = _get_subject_id(request.session) if subject_id is None: logger.warning( 'The session does not contain the subject id for user %s. Performing local logout', request.user) auth.logout(request) return render(request, logout_error_template, {}) else: http_info = client.handle_logout_request(data['SAMLRequest'], subject_id, binding, relay_state=data.get( 'RelayState', '')) state.sync() auth.logout(request) return HttpResponseRedirect(get_location(http_info)) else: logger.error('No SAMLResponse or SAMLRequest parameter found') raise Http404('No SAMLResponse or SAMLRequest parameter found')
def logout(request, config_loader_path=None): """SAML Logout Request initiator This view initiates the SAML2 Logout request using the pysaml2 library to create the LogoutRequest. """ logger.debug('Logout process started') state = StateCache(request.session) conf = get_config(config_loader_path, request) client = Saml2Client(conf, state_cache=state, identity_cache=IdentityCache(request.session)) subject_id = _get_subject_id(request.session) if subject_id is None: logger.warning( 'The session does not contains the subject id for user %s', request.user) result = client.global_logout(subject_id) state.sync() if not result: logger.error("Looks like the user %s is not logged in any IdP/AA", subject_id) return HttpResponseBadRequest("You are not logged in any IdP/AA") if len(result) > 1: logger.error('Sorry, I do not know how to logout from several sources. I will logout just from the first one') for entityid, logout_info in result.items(): if isinstance(logout_info, tuple): binding, http_info = logout_info if binding == BINDING_HTTP_POST: logger.debug('Returning form to the IdP to continue the logout process') body = ''.join(http_info['data']) return HttpResponse(body) elif binding == BINDING_HTTP_REDIRECT: logger.debug('Redirecting to the IdP to continue the logout process') return HttpResponseRedirect(get_location(http_info)) else: logger.error('Unknown binding: %s', binding) return HttpResponseServerError('Failed to log out') else: # We must have had a soap logout return finish_logout(request, logout_info) logger.error('Could not logout because there only the HTTP_REDIRECT is supported') return HttpResponseServerError('Logout Binding not supported')
def do_logout_service(request, data, binding, config_loader_path=None, next_page=None, logout_error_template='djangosaml2/logout_error.html'): """ SAML Logout Response endpoint The IdP will send the logout response to this view, which will process it with pysaml2 help and log the user out. Note that the IdP can request a logout even when we didn't initiate the process as a single logout request started by another SP. """ logger.debug('Logout service started') conf = get_config(config_loader_path, request) state = StateCache(request.session) client = Saml2Client(conf, state_cache=state, identity_cache=IdentityCache(request.session)) if 'SAMLResponse' in data: # we started the logout logger.debug('Receiving a logout response from the IdP') try: response = client.parse_logout_request_response(data['SAMLResponse'], binding) state.sync() except: traceback_info = '\n'.join(traceback.format_exception(*(sys.exc_info()))) logger.warning('Encountered the following error when calling Saml2Client.global_logout(): %s' % traceback_info) auth.logout(request) return HttpResponseRedirect(settings.LOGOUT_REDIRECT_URL) else: return finish_logout(request, response, next_page=next_page) elif 'SAMLRequest' in data: # logout started by the IdP logger.debug('Receiving a logout request from the IdP') subject_id = _get_subject_id(request.session) if subject_id is None: logger.warning('The session does not contain the subject id for user %s. Performing local logout' % request.user) auth.logout(request) return HttpResponseRedirect(settings.LOGOUT_REDIRECT_URL) else: http_info = client.handle_logout_request(data['SAMLRequest'], subject_id, binding) state.sync() auth.logout(request) redirect_location = settings.LOGOUT_REDIRECT_URL try: redirect_location = get_location(http_info) except: traceback_info = '\n'.join(traceback.format_exception(*(sys.exc_info()))) logger.error('Encountered the following error calling get_location(http_info) with an http_info value of %s: %s' % (http_info, traceback_info)) return HttpResponseRedirect(redirect_location) else: logger.error('No SAMLResponse or SAMLRequest parameter found') return failure_redirect("No SAMLResponse or SAMLRequest parameter found.")
def do_logout_service(request, data, binding, config_loader_path=None, next_page=None, logout_error_template='djangosaml2/logout_error.html'): """SAML Logout Response endpoint The IdP will send the logout response to this view, which will process it with pysaml2 help and log the user out. Note that the IdP can request a logout even when we didn't initiate the process as a single logout request started by another SP. """ logger.debug('Logout service started') conf = get_config(config_loader_path, request) state = StateCache(request.session) client = Saml2Client(conf, state_cache=state, identity_cache=IdentityCache(request.session)) if 'SAMLResponse' in data: # we started the logout logger.debug('Receiving a logout response from the IdP') response = client.parse_logout_request_response(data['SAMLResponse'], binding) state.sync() return finish_logout(request, response, next_page=next_page) elif 'SAMLRequest' in data: # logout started by the IdP logger.debug('Receiving a logout request from the IdP') subject_id = _get_subject_id(request.session) if subject_id is None: logger.warning( 'The session does not contain the subject id for user %s. Performing local logout', request.user) auth.logout(request) return render_to_response(logout_error_template, {}, context_instance=RequestContext(request)) else: http_info = client.handle_logout_request( data['SAMLRequest'], subject_id, binding) state.sync() auth.logout(request) return HttpResponseRedirect(get_location(http_info)) else: logger.error('No SAMLResponse or SAMLRequest parameter found') raise Http404('No SAMLResponse or SAMLRequest parameter found')
def get(self, request): state = StateCache(request.session) conf = get_config(request=request) client = Saml2Client(conf, state_cache=state, identity_cache=IdentityCache(request.session)) subject_id = _get_subject_id(request.session) if subject_id is None: return logout_failed(_('You cannot be logged out.')) try: result = client.global_logout(subject_id) except KeyError: return logout_failed(_('You are not logged in any IdP/AA.')) state.sync() if not result: return logout_failed(_('You are not logged in any IdP/AA.')) # Logout is supported only from 1 IdP binding, http_info = result.values()[0] return HttpResponseRedirect(get_location(http_info))
def logout(self, request, data, binding): conf = get_config(request=request) state = StateCache(request.session) client = Saml2Client(conf, state_cache=state, identity_cache=IdentityCache(request.session)) if 'SAMLResponse' in data: # Logout started by us client.parse_logout_request_response(data['SAMLResponse'], binding) http_response = logout_completed() else: # Logout started by IdP subject_id = _get_subject_id(request.session) if subject_id is None: http_response = logout_completed() else: http_info = client.handle_logout_request( data['SAMLRequest'], subject_id, binding, relay_state=data.get('RelayState', ''), ) http_response = HttpResponseRedirect(get_location(http_info)) state.sync() user = request.user if user.is_anonymous: return http_response Token.objects.get(user=user).delete() auth.logout(request) event_logger.saml2_auth.info( 'User {user_username} with full name {user_full_name} logged out successfully with SAML2.', event_type='auth_logged_out_with_saml2', event_context={'user': user}, ) return http_response
def post(self, request): if not self.request.user.is_anonymous: error_message = _('This endpoint is for anonymous users only.') return JsonResponse({'error_message': error_message}, status_code=400) serializer = self.serializer_class(data=request.data) serializer.is_valid(raise_exception=True) idp = serializer.validated_data.get('idp') conf = get_config(request=request) # ensure our selected binding is supported by the IDP supported_bindings = utils.get_idp_sso_supported_bindings(idp, config=conf) default_binding = settings.WALDUR_AUTH_SAML2.get('DEFAULT_BINDING') if default_binding in supported_bindings: binding = default_binding elif BINDING_HTTP_POST in supported_bindings: binding = BINDING_HTTP_POST elif BINDING_HTTP_REDIRECT in supported_bindings: binding = BINDING_HTTP_REDIRECT else: error_message = _('Identity provider does not support available bindings.') return JsonResponse({'error_message': error_message}, status_code=400) client = Saml2Client(conf) kwargs = {} sign_requests = getattr(conf, '_sp_authn_requests_signed', False) if sign_requests: signature_algorithm = settings.WALDUR_AUTH_SAML2.get('signature_algorithm') or SIG_RSA_SHA1 digest_algorithm = settings.WALDUR_AUTH_SAML2.get('digest_algorithm') or DIGEST_SHA1 kwargs['sign'] = True kwargs['sigalg'] = signature_algorithm kwargs['sign_alg'] = signature_algorithm kwargs['digest_alg'] = digest_algorithm nameid_format = settings.WALDUR_AUTH_SAML2.get('nameid_format') if nameid_format or nameid_format == "": # "" is a valid setting in pysaml2 kwargs['nameid_format'] = nameid_format if binding == BINDING_HTTP_REDIRECT: session_id, result = client.prepare_for_authenticate( entityid=idp, binding=binding, **kwargs) data = { 'binding': 'redirect', 'url': get_location(result), } elif binding == BINDING_HTTP_POST: try: location = client.sso_location(idp, binding) except TypeError: error_message = _('Invalid identity provider specified.') return JsonResponse({'error_message': error_message}, status_code=400) session_id, request_xml = client.create_authn_request(location, binding=binding, **kwargs) data = { 'binding': 'post', 'url': location, 'request': base64.b64encode(request_xml.encode('UTF-8')), } # save session_id oq_cache = OutstandingQueriesCache(request.session) oq_cache.set(session_id, '') return JsonResponse(data)
def login(request, config_loader_path=None, wayf_template='djangosaml2/wayf.html', authorization_error_template='djangosaml2/auth_error.html', post_binding_form_template='djangosaml2/post_binding_form.html'): """SAML Authorization Request initiator This view initiates the SAML2 Authorization handshake using the pysaml2 library to create the AuthnRequest. It uses the SAML 2.0 Http Redirect protocol binding. * post_binding_form_template - path to a template containing HTML form with hidden input elements, used to send the SAML message data when HTTP POST binding is being used. You can customize this template to include custom branding and/or text explaining the automatic redirection process. Please see the example template in templates/djangosaml2/example_post_binding_form.html If set to None or nonexistent template, default form from the saml2 library will be rendered. """ logger.debug('Login process started') came_from = request.GET.get('next', settings.LOGIN_REDIRECT_URL) if not came_from: logger.warning('The next parameter exists but is empty') came_from = settings.LOGIN_REDIRECT_URL # Ensure the user-originating redirection url is safe. if not is_safe_url(url=came_from, host=request.get_host()): came_from = settings.LOGIN_REDIRECT_URL # if the user is already authenticated that maybe because of two reasons: # A) He has this URL in two browser windows and in the other one he # has already initiated the authenticated session. # B) He comes from a view that (incorrectly) send him here because # he does not have enough permissions. That view should have shown # an authorization error in the first place. # We can only make one thing here and that is configurable with the # SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN setting. If that setting # is True (default value) we will redirect him to the came_from view. # Otherwise, we will show an (configurable) authorization error. if not request.user.is_anonymous(): try: redirect_authenticated_user = settings.SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN except AttributeError: redirect_authenticated_user = True if redirect_authenticated_user: return HttpResponseRedirect(came_from) else: logger.debug('User is already logged in') return render(request, authorization_error_template, { 'came_from': came_from, }) selected_idp = request.GET.get('idp', None) conf = get_config(config_loader_path, request) # is a embedded wayf needed? idps = available_idps(conf) if selected_idp is None and len(idps) > 1: logger.debug('A discovery process is needed') return render(request, wayf_template, { 'available_idps': idps.items(), 'came_from': came_from, }) # Choose binding (REDIRECT vs. POST). # When authn_requests_signed is turned on, HTTP Redirect binding cannot be # used the same way as without signatures; proper usage in this case involves # stripping out the signature from SAML XML message and creating a new # signature, following precise steps defined in the SAML2.0 standard. # # It is not feasible to implement this since we wouldn't be able to use an # external (xmlsec1) library to handle the signatures - more (higher level) # context is needed in order to create such signature (like the value of # RelayState parameter). # # Therefore it is much easier to use the HTTP POST binding in this case, as # it can relay the whole signed SAML message as is, without the need to # manipulate the signature or the XML message itself. # # Read more in the official SAML2 specs (3.4.4.1): # http://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf binding = BINDING_HTTP_POST if getattr(conf, '_sp_authn_requests_signed', False) else BINDING_HTTP_REDIRECT client = Saml2Client(conf) try: (session_id, result) = client.prepare_for_authenticate( entityid=selected_idp, relay_state=came_from, binding=binding, ) except TypeError as e: logger.error('Unable to know which IdP to use') return HttpResponse(unicode(e)) logger.debug('Saving the session_id in the OutstandingQueries cache') oq_cache = OutstandingQueriesCache(request.session) oq_cache.set(session_id, came_from) logger.debug('Redirecting user to the IdP via %s binding.', binding.split(':')[-1]) if binding == BINDING_HTTP_REDIRECT: return HttpResponseRedirect(get_location(result)) elif binding == BINDING_HTTP_POST: if not post_binding_form_template: return HttpResponse(result['data']) try: params = get_hidden_form_inputs(result['data'][3]) return render(request, post_binding_form_template, { 'target_url': result['url'], 'params': params, }) except (DTDForbidden, EntitiesForbidden, ExternalReferenceForbidden): raise PermissionDenied except TemplateDoesNotExist: return HttpResponse(result['data']) else: raise NotImplementedError('Unsupported binding: %s', binding)
def login(request, config_loader_path=None, wayf_template='djangosaml2/wayf.html', authorization_error_template='djangosaml2/auth_error.html', post_binding_form_template='djangosaml2/post_binding_form.html'): """SAML Authorization Request initiator This view initiates the SAML2 Authorization handshake using the pysaml2 library to create the AuthnRequest. It uses the SAML 2.0 Http Redirect protocol binding. * post_binding_form_template - path to a template containing HTML form with hidden input elements, used to send the SAML message data when HTTP POST binding is being used. You can customize this template to include custom branding and/or text explaining the automatic redirection process. Please see the example template in templates/djangosaml2/example_post_binding_form.html If set to None or nonexistent template, default form from the saml2 library will be rendered. """ logger.debug('Login process started') came_from = request.GET.get('next', settings.LOGIN_REDIRECT_URL) if not came_from: logger.warning('The next parameter exists but is empty') came_from = settings.LOGIN_REDIRECT_URL # if the user is already authenticated that maybe because of two reasons: # A) He has this URL in two browser windows and in the other one he # has already initiated the authenticated session. # B) He comes from a view that (incorrectly) send him here because # he does not have enough permissions. That view should have shown # an authorization error in the first place. # We can only make one thing here and that is configurable with the # SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN setting. If that setting # is True (default value) we will redirect him to the came_from view. # Otherwise, we will show an (configurable) authorization error. if not request.user.is_anonymous(): try: redirect_authenticated_user = settings.SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN except AttributeError: redirect_authenticated_user = True if redirect_authenticated_user: return HttpResponseRedirect(came_from) else: logger.debug('User is already logged in') return render_to_response(authorization_error_template, { 'came_from': came_from, }, context_instance=RequestContext(request)) selected_idp = request.GET.get('idp', None) conf = get_config(config_loader_path, request) # is a embedded wayf needed? idps = available_idps(conf) if selected_idp is None and len(idps) > 1: logger.debug('A discovery process is needed') return render_to_response(wayf_template, { 'available_idps': idps.items(), 'came_from': came_from, }, context_instance=RequestContext(request)) # Choose binding (REDIRECT vs. POST). # When authn_requests_signed is turned on, HTTP Redirect binding cannot be # used the same way as without signatures; proper usage in this case involves # stripping out the signature from SAML XML message and creating a new # signature, following precise steps defined in the SAML2.0 standard. # # It is not feasible to implement this since we wouldn't be able to use an # external (xmlsec1) library to handle the signatures - more (higher level) # context is needed in order to create such signature (like the value of # RelayState parameter). # # Therefore it is much easier to use the HTTP POST binding in this case, as # it can relay the whole signed SAML message as is, without the need to # manipulate the signature or the XML message itself. # # Read more in the official SAML2 specs (3.4.4.1): # http://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf binding = BINDING_HTTP_POST if getattr(conf, '_sp_authn_requests_signed', False) else BINDING_HTTP_REDIRECT client = Saml2Client(conf) try: (session_id, result) = client.prepare_for_authenticate( entityid=selected_idp, relay_state=came_from, binding=binding, ) except TypeError as e: logger.error('Unable to know which IdP to use') return HttpResponse(unicode(e)) logger.debug('Saving the session_id in the OutstandingQueries cache') oq_cache = OutstandingQueriesCache(request.session) oq_cache.set(session_id, came_from) logger.debug('Redirecting user to the IdP via %s binding.', binding.split(':')[-1]) if binding == BINDING_HTTP_REDIRECT: return HttpResponseRedirect(get_location(result)) elif binding == BINDING_HTTP_POST: if not post_binding_form_template: return HttpResponse(result['data']) try: params = get_hidden_form_inputs(result['data'][3]) return render_to_response(post_binding_form_template, { 'target_url': result['url'], 'params': params, }, context_instance=RequestContext(request)) except TemplateDoesNotExist: return HttpResponse(result['data']) else: raise NotImplementedError('Unsupported binding: %s', binding)
try: (session_id, result) = client.prepare_for_authenticate( entityid=selected_idp, relay_state=came_from, binding=BINDING_HTTP_REDIRECT, ) except TypeError, e: logger.error('Unable to know which IdP to use') return HttpResponse(unicode(e)) logger.debug('Saving the session_id in the OutstandingQueries cache') oq_cache = OutstandingQueriesCache(request.session) oq_cache.set(session_id, came_from) logger.debug('Redirecting the user to the IdP') return HttpResponseRedirect(get_location(result)) @require_POST @csrf_exempt def assertion_consumer_service(request, config_loader_path=None, attribute_mapping=None, create_unknown_user=None): """SAML Authorization Response endpoint The IdP will send its response to this view, which will process it with pysaml2 help and log the user in using the custom Authorization backend djangosaml2.backends.Saml2Backend that should be enabled in the settings.py
def login(request, config_loader_path=None, wayf_template='djangosaml2/wayf.html', authorization_error_template='djangosaml2/auth_error.html', post_binding_form_template='djangosaml2/post_binding_form.html'): """SAML Authorization Request initiator This view initiates the SAML2 Authorization handshake using the pysaml2 library to create the AuthnRequest. It uses the SAML 2.0 Http Redirect protocol binding. * post_binding_form_template - path to a template containing HTML form with hidden input elements, used to send the SAML message data when HTTP POST binding is being used. You can customize this template to include custom branding and/or text explaining the automatic redirection process. Please see the example template in templates/djangosaml2/example_post_binding_form.html If set to None or nonexistent template, default form from the saml2 library will be rendered. """ logger.debug('Login process started') came_from = request.GET.get('next', settings.LOGIN_REDIRECT_URL) if not came_from: logger.warning('The next parameter exists but is empty') came_from = settings.LOGIN_REDIRECT_URL # Ensure the user-originating redirection url is safe. if not is_safe_url(url=came_from, host=request.get_host()): came_from = settings.LOGIN_REDIRECT_URL # if the user is already authenticated that maybe because of two reasons: # A) He has this URL in two browser windows and in the other one he # has already initiated the authenticated session. # B) He comes from a view that (incorrectly) send him here because # he does not have enough permissions. That view should have shown # an authorization error in the first place. # We can only make one thing here and that is configurable with the # SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN setting. If that setting # is True (default value) we will redirect him to the came_from view. # Otherwise, we will show an (configurable) authorization error. if not request.user.is_anonymous(): try: redirect_authenticated_user = settings.SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN except AttributeError: redirect_authenticated_user = True if redirect_authenticated_user: return HttpResponseRedirect(came_from) else: logger.debug('User is already logged in') return render(request, authorization_error_template, { 'came_from': came_from, }) selected_idp = request.GET.get('idp', None) conf = get_config(config_loader_path, request) # is a embedded wayf needed? idps = available_idps(conf) if selected_idp is None and len(idps) > 1: logger.debug('A discovery process is needed') return render(request, wayf_template, { 'available_idps': idps.items(), 'came_from': came_from, }) # choose a binding to try first sign_requests = getattr(conf, '_sp_authn_requests_signed', False) binding = BINDING_HTTP_POST if sign_requests else BINDING_HTTP_REDIRECT logger.debug('Trying binding %s for IDP %s', binding, selected_idp) # ensure our selected binding is supported by the IDP supported_bindings = get_idp_sso_supported_bindings(selected_idp, config=conf) if binding not in supported_bindings: logger.debug('Binding %s not in IDP %s supported bindings: %s', binding, selected_idp, supported_bindings) if binding == BINDING_HTTP_POST: logger.warning('IDP %s does not support %s, trying %s', selected_idp, binding, BINDING_HTTP_REDIRECT) binding = BINDING_HTTP_REDIRECT else: logger.warning('IDP %s does not support %s, trying %s', selected_idp, binding, BINDING_HTTP_POST) binding = BINDING_HTTP_POST # if switched binding still not supported, give up if binding not in supported_bindings: raise UnsupportedBinding('IDP %s does not support %s or %s', selected_idp, BINDING_HTTP_POST, BINDING_HTTP_REDIRECT) client = Saml2Client(conf) http_response = None logger.debug('Redirecting user to the IdP via %s binding.', binding) if binding == BINDING_HTTP_REDIRECT: try: # do not sign the xml itself, instead us the sigalg to # generate the signature as a URL param sigalg = SIG_RSA_SHA1 if sign_requests else None session_id, result = client.prepare_for_authenticate( entityid=selected_idp, relay_state=came_from, binding=binding, sign=False, sigalg=sigalg) except TypeError as e: logger.error('Unable to know which IdP to use') return HttpResponse(text_type(e)) else: http_response = HttpResponseRedirect(get_location(result)) elif binding == BINDING_HTTP_POST: # use the html provided by pysaml2 if no template specified if not post_binding_form_template: try: session_id, result = client.prepare_for_authenticate( entityid=selected_idp, relay_state=came_from, binding=binding) except TypeError as e: logger.error('Unable to know which IdP to use') return HttpResponse(text_type(e)) else: http_response = HttpResponse(result['data']) # get request XML to build our own html based on the template else: try: location = client.sso_location(selected_idp, binding) except TypeError as e: logger.error('Unable to know which IdP to use') return HttpResponse(text_type(e)) session_id, request_xml = client.create_authn_request( location, binding=binding) http_response = render( request, post_binding_form_template, { 'target_url': location, 'params': { 'SAMLRequest': base64.b64encode( binary_type(request_xml)), 'RelayState': came_from, }, }) else: raise UnsupportedBinding('Unsupported binding: %s', binding) # success, so save the session ID and return our response logger.debug('Saving the session_id in the OutstandingQueries cache') oq_cache = OutstandingQueriesCache(request.session) oq_cache.set(session_id, came_from) return http_response
def login(request, config_loader_path=None, wayf_template='djangosaml2/wayf.html', authorization_error_template='djangosaml2/auth_error.html'): """SAML Authorization Request initiator This view initiates the SAML2 Authorization handshake using the pysaml2 library to create the AuthnRequest. It uses the SAML 2.0 Http Redirect protocol binding. """ logger.debug('Login process started') came_from = request.GET.get('next', settings.LOGIN_REDIRECT_URL) if not came_from: logger.warning('The next parameter exists but is empty') came_from = settings.LOGIN_REDIRECT_URL # if the user is already authenticated that maybe because of two reasons: # A) He has this URL in two browser windows and in the other one he # has already initiated the authenticated session. # B) He comes from a view that (incorrectly) send him here because # he does not have enough permissions. That view should have shown # an authorization error in the first place. # We can only make one thing here and that is configurable with the # SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN setting. If that setting # is True (default value) we will redirect him to the came_from view. # Otherwise, we will show an (configurable) authorization error. if not request.user.is_anonymous(): try: redirect_authenticated_user = settings.SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN except AttributeError: redirect_authenticated_user = True if redirect_authenticated_user: return HttpResponseRedirect(came_from) else: logger.debug('User is already logged in') return render_to_response(authorization_error_template, { 'came_from': came_from, }, context_instance=RequestContext(request)) selected_idp = request.GET.get('idp', None) conf = get_config(config_loader_path, request) # is a embedded wayf needed? idps = available_idps(conf) if selected_idp is None and len(idps) > 1: logger.debug('A discovery process is needed') return render_to_response(wayf_template, { 'available_idps': idps.items(), 'came_from': came_from, }, context_instance=RequestContext(request)) client = Saml2Client(conf) try: (session_id, result) = client.prepare_for_authenticate( entityid=selected_idp, relay_state=came_from, binding=BINDING_HTTP_REDIRECT, ) except TypeError as e: logger.error('Unable to know which IdP to use') return HttpResponse(unicode(e)) logger.debug('Saving the session_id in the OutstandingQueries cache') oq_cache = OutstandingQueriesCache(request.session) oq_cache.set(session_id, came_from) logger.debug('Redirecting the user to the IdP') return HttpResponseRedirect(get_location(result))
def login(request, config_loader_path=None, wayf_template='djangosaml2/wayf.html', authorization_error_template='djangosaml2/auth_error.html', post_binding_form_template='djangosaml2/post_binding_form.html'): """SAML Authorization Request initiator This view initiates the SAML2 Authorization handshake using the pysaml2 library to create the AuthnRequest. It uses the SAML 2.0 Http Redirect protocol binding. * post_binding_form_template - path to a template containing HTML form with hidden input elements, used to send the SAML message data when HTTP POST binding is being used. You can customize this template to include custom branding and/or text explaining the automatic redirection process. Please see the example template in templates/djangosaml2/example_post_binding_form.html If set to None or nonexistent template, default form from the saml2 library will be rendered. """ logger.debug('Login process started') came_from = request.GET.get('next', settings.LOGIN_REDIRECT_URL) if not came_from: logger.warning('The next parameter exists but is empty') came_from = settings.LOGIN_REDIRECT_URL # Ensure the user-originating redirection url is safe. if not is_safe_url_compat(url=came_from, allowed_hosts={request.get_host()}): came_from = settings.LOGIN_REDIRECT_URL # if the user is already authenticated that maybe because of two reasons: # A) He has this URL in two browser windows and in the other one he # has already initiated the authenticated session. # B) He comes from a view that (incorrectly) send him here because # he does not have enough permissions. That view should have shown # an authorization error in the first place. # We can only make one thing here and that is configurable with the # SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN setting. If that setting # is True (default value) we will redirect him to the came_from view. # Otherwise, we will show an (configurable) authorization error. if callable_bool(request.user.is_authenticated): redirect_authenticated_user = getattr(settings, 'SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN', True) if redirect_authenticated_user: return HttpResponseRedirect(came_from) else: logger.debug('User is already logged in') return render(request, authorization_error_template, { 'came_from': came_from, }) selected_idp = request.GET.get('idp', None) conf = get_config(config_loader_path, request) # is a embedded wayf needed? idps = available_idps(conf) if selected_idp is None and len(idps) > 1: logger.debug('A discovery process is needed') return render(request, wayf_template, { 'available_idps': idps.items(), 'came_from': came_from, }) # choose a binding to try first sign_requests = getattr(conf, '_sp_authn_requests_signed', False) binding = BINDING_HTTP_POST if sign_requests else BINDING_HTTP_REDIRECT logger.debug('Trying binding %s for IDP %s', binding, selected_idp) # ensure our selected binding is supported by the IDP supported_bindings = get_idp_sso_supported_bindings(selected_idp, config=conf) if binding not in supported_bindings: logger.debug('Binding %s not in IDP %s supported bindings: %s', binding, selected_idp, supported_bindings) if binding == BINDING_HTTP_POST: logger.warning('IDP %s does not support %s, trying %s', selected_idp, binding, BINDING_HTTP_REDIRECT) binding = BINDING_HTTP_REDIRECT else: logger.warning('IDP %s does not support %s, trying %s', selected_idp, binding, BINDING_HTTP_POST) binding = BINDING_HTTP_POST # if switched binding still not supported, give up if binding not in supported_bindings: raise UnsupportedBinding('IDP %s does not support %s or %s', selected_idp, BINDING_HTTP_POST, BINDING_HTTP_REDIRECT) client = Saml2Client(conf) http_response = None logger.debug('Redirecting user to the IdP via %s binding.', binding) if binding == BINDING_HTTP_REDIRECT: try: # do not sign the xml itself, instead use the sigalg to # generate the signature as a URL param sig_alg_option_map = {'sha1': SIG_RSA_SHA1, 'sha256': SIG_RSA_SHA256} sig_alg_option = getattr(conf, '_sp_authn_requests_signed_alg', 'sha1') sigalg = sig_alg_option_map[sig_alg_option] if sign_requests else None nsprefix = get_namespace_prefixes() session_id, result = client.prepare_for_authenticate( entityid=selected_idp, relay_state=came_from, binding=binding, sign=False, sigalg=sigalg, nsprefix=nsprefix) except TypeError as e: logger.error('Unable to know which IdP to use') return HttpResponse(text_type(e)) else: http_response = HttpResponseRedirect(get_location(result)) elif binding == BINDING_HTTP_POST: if post_binding_form_template: # get request XML to build our own html based on the template try: location = client.sso_location(selected_idp, binding) except TypeError as e: logger.error('Unable to know which IdP to use') return HttpResponse(text_type(e)) session_id, request_xml = client.create_authn_request( location, binding=binding) try: if PY3: saml_request = base64.b64encode(binary_type(request_xml, 'UTF-8')) else: saml_request = base64.b64encode(binary_type(request_xml)) http_response = render(request, post_binding_form_template, { 'target_url': location, 'params': { 'SAMLRequest': saml_request, 'RelayState': came_from, }, }) except TemplateDoesNotExist: pass if not http_response: # use the html provided by pysaml2 if no template was specified or it didn't exist try: session_id, result = client.prepare_for_authenticate( entityid=selected_idp, relay_state=came_from, binding=binding) except TypeError as e: logger.error('Unable to know which IdP to use') return HttpResponse(text_type(e)) else: http_response = HttpResponse(result['data']) else: raise UnsupportedBinding('Unsupported binding: %s', binding) # success, so save the session ID and return our response logger.debug('Saving the session_id in the OutstandingQueries cache') oq_cache = OutstandingQueriesCache(request.session) oq_cache.set(session_id, came_from) return http_response
client = Saml2Client(conf) try: (session_id, result) = client.prepare_for_authenticate( entityid=selected_idp, relay_state=came_from, binding=BINDING_HTTP_REDIRECT, ) except TypeError, e: logger.error('Unable to know which IdP to use') return HttpResponse(unicode(e)) logger.debug('Saving the session_id in the OutstandingQueries cache') oq_cache = OutstandingQueriesCache(request.session) oq_cache.set(session_id, came_from) logger.debug('Redirecting the user to the IdP') return HttpResponseRedirect(get_location(result)) @require_POST @csrf_exempt def assertion_consumer_service(request, config_loader_path=None, attribute_mapping=None, create_unknown_user=None): """SAML Authorization Response endpoint The IdP will send its response to this view, which will process it with pysaml2 help and log the user in using the custom Authorization backend djangosaml2.backends.Saml2Backend that should be enabled in the settings.py
def logout(request, config_loader_path=None): """ SAML Logout Request initiator This view initiates the SAML2 Logout request using the pysaml2 library to create the LogoutRequest. """ logger.debug('Logout process started') initiate_global_logout = get_custom_setting('DJANGOSAML_INITIATE_GLOBAL_LOGOUT', default=True) # Handle SP that should NOT initiate a global logout if not initiate_global_logout: logger.debug('SP cannot initiate a global logout. Doing local logout for %s and redirecting to %s' % (request.user, settings.LOGOUT_REDIRECT_URL)) auth.logout(request) return HttpResponseRedirect(settings.LOGOUT_REDIRECT_URL) # Handle SP that should initiate a global logout if not request.user.is_authenticated(): logger.debug('User is not authenticated. Redirecting to settings.LOGOUT_REDIRECT_URL') return HttpResponseRedirect(settings.LOGOUT_REDIRECT_URL) state = StateCache(request.session) conf = get_config(config_loader_path, request) client = Saml2Client(conf, state_cache=state, identity_cache=IdentityCache(request.session)) subject_id = _get_subject_id(request.session) if subject_id is None: logger.warning('The session does not contains the subject id for user %s' % request.user) try: result = client.global_logout(subject_id) except Exception as e: traceback_info = '\n'.join(traceback.format_exception(*(sys.exc_info()))) logger.warning('Encountered the following error when calling Saml2Client.global_logout(): %s' % traceback_info) return failure_redirect('Encountered the following error when trying to do a global logout: %s.' % e) state.sync() if not result: logger.error("Looks like the user %s is not logged in any IdP/AA" % subject_id) return failure_redirect("You are not logged into any IdP/AA") if len(result) > 1: logger.error('Sorry, I do not know how to logout from several sources. I will logout just from the first one') for entityid, logout_info in result.items(): if isinstance(logout_info, tuple): binding, http_info = logout_info if binding == BINDING_HTTP_POST: logger.debug('Returning form to the IdP to continue the logout process') body = ''.join(http_info['data']) logger.debug("Body = %s" % body) return HttpResponse(body) elif binding == BINDING_HTTP_REDIRECT: logger.debug('Redirecting to the IdP to continue the logout process') return HttpResponseRedirect(get_location(http_info)) else: logger.error('Unknown binding: %s', binding) return failure_redirect("Failed to log out.") else: # We must have had a soap logout return finish_logout(request, logout_info) logger.error('Could not logout because there only the HTTP_REDIRECT is supported') return failure_redirect("Logout Binding not supported")
def login(request, config_loader_path=None, wayf_template='djangosaml2/wayf.html', authorization_error_template='djangosaml2/auth_error.html'): """SAML Authorization Request initiator This view initiates the SAML2 Authorization handshake using the pysaml2 library to create the AuthnRequest. It uses the SAML 2.0 Http Redirect protocol binding. """ logger.debug('Login process started') came_from = request.GET.get('next', settings.LOGIN_REDIRECT_URL) if not came_from: logger.warning('The next parameter exists but is empty') came_from = settings.LOGIN_REDIRECT_URL # if the user is already authenticated that maybe because of two reasons: # A) He has this URL in two browser windows and in the other one he # has already initiated the authenticated session. # B) He comes from a view that (incorrectly) send him here because # he does not have enough permissions. That view should have shown # an authorization error in the first place. # We can only make one thing here and that is configurable with the # SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN setting. If that setting # is True (default value) we will redirect him to the came_from view. # Otherwise, we will show an (configurable) authorization error. if not request.user.is_anonymous(): try: redirect_authenticated_user = settings.SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN except AttributeError: redirect_authenticated_user = True if redirect_authenticated_user: return HttpResponseRedirect(came_from) else: logger.debug('User is already logged in') return render_to_response(authorization_error_template, { 'came_from': came_from, }, context_instance=RequestContext(request)) selected_idp = request.GET.get('idp', None) conf = get_config(config_loader_path, request) # is a embedded wayf needed? idps = available_idps(conf) if selected_idp is None and len(idps) > 1: logger.debug('A discovery process is needed') return render_to_response(wayf_template, { 'available_idps': idps.items(), 'came_from': came_from, }, context_instance=RequestContext(request)) client = Saml2Client(conf) try: (session_id, result) = client.prepare_for_authenticate( entityid=selected_idp, relay_state=came_from, binding=BINDING_HTTP_REDIRECT, ) except TypeError as e: logger.error('Unable to know which IdP to use') return HttpResponse(str(e)) logger.debug('Saving the session_id in the OutstandingQueries cache') oq_cache = OutstandingQueriesCache(request.session) oq_cache.set(session_id, came_from) logger.debug('Redirecting the user to the IdP') return HttpResponseRedirect(get_location(result))