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. """ 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 contain the subject id for user %s', request.user) sign_requests = getattr(conf, '_sp_logout_requests_signed', False) sign_alg = getattr(conf, 'signing_algorithm', SIG_RSA_SHA1) digest_alg = getattr(conf, 'digest_algorithm', DIGEST_SHA1) result = client.global_logout(subject_id, sign=sign_requests, sign_alg=sign_alg, digest_alg=digest_alg) 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') 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, status=403) else: http_info = client.handle_logout_request(data['SAMLRequest'], subject_id, binding, relay_state=data.get( 'RelayState', '')) state.sync() auth.logout(request) if (http_info.get('method', 'GET') == 'POST' and 'data' in http_info and ('Content-type', 'text/html') in http_info.get('headers', [])): # need to send back to the IDP a signed POST response with user session # return HTML form content to browser with auto form validation # to finally send request to the IDP return HttpResponse(http_info['data']) else: 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. """ 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) 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 contain 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 echo_attributes(request, config_loader_path=None, template='djangosaml2/echo_attributes.html'): """Example view that echo the SAML attributes of an user""" 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) try: identity = client.users.get_identity(subject_id, check_not_on_or_after=False) except AttributeError: return HttpResponse("No active SAML identity found. Are you sure you have logged in via SAML?") return render(request, template, {'attributes': identity[0]})
def spid_logout(request, config_loader_path=None, **kwargs): """SAML Logout Request initiator This view initiates the SAML2 Logout request using the pysaml2 library to create the LogoutRequest. """ if not request.user.is_authenticated: return HttpResponseRedirect(settings.LOGOUT_REDIRECT_URL) state = StateCache(request.saml_session) conf = get_config(config_loader_path, request) client = Saml2Client(conf, state_cache=state, identity_cache=IdentityCache(request.saml_session)) # whatever happens, however, the user will be logged out of this sp auth.logout(request) state.sync() subject_id = djangosaml2_views._get_subject_id(request.saml_session) if subject_id is None: logger.warning( f"The session does not contain the subject id for user {request.user}" ) logger.error( f"Looks like the user {subject_id} is not logged in any IdP/AA") return HttpResponseBadRequest("You are not logged in any IdP/AA") slo_req = saml2.samlp.LogoutRequest() slo_req.destination = subject_id.name_qualifier # spid-testenv2 preleva l'attribute consumer service dalla authnRequest (anche se questo sta già nei metadati...) slo_req.attribute_consuming_service_index = "0" issuer = saml2.saml.Issuer() issuer.name_qualifier = client.config.entityid issuer.text = client.config.entityid issuer.format = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity" slo_req.issuer = issuer # message id slo_req.id = saml2.s_utils.sid() slo_req.version = saml2.VERSION # "2.0" slo_req.issue_instant = saml2.time_util.instant() # oggetto slo_req.name_id = subject_id try: session_info = client.users.get_info_from(slo_req.name_id, subject_id.name_qualifier, False) except KeyError as e: logger.error(f"SPID Logout error: {e}") return HttpResponseRedirect("/") session_indexes = [session_info["session_index"]] # aggiungere session index if session_indexes: sis = [] for si in session_indexes: if isinstance(si, saml2.samlp.SessionIndex): sis.append(si) else: sis.append(saml2.samlp.SessionIndex(text=si)) slo_req.session_index = sis slo_req.protocol_binding = SAML2_DEFAULT_BINDING assertion_consumer_service_url = client.config._sp_endpoints[ "assertion_consumer_service"][0][0] slo_req.assertion_consumer_service_url = assertion_consumer_service_url slo_req_signed = client.sign( slo_req, sign_prepare=False, sign_alg=settings.SPID_SIG_ALG, digest_alg=settings.SPID_DIG_ALG, ) _req_str = slo_req_signed logger.debug( f"LogoutRequest to {subject_id.name_qualifier}: {repr_saml_request(_req_str)}" ) slo_location = client.metadata.single_logout_service( subject_id.name_qualifier, SAML2_DEFAULT_BINDING, "idpsso")[0]["location"] if not slo_location: error_message = f"Unable to know SLO endpoint in {subject_id.name_qualifier}" logger.error(error_message) return HttpResponse(error_message) http_info = client.apply_binding( SAML2_DEFAULT_BINDING, _req_str, slo_location, sign=True, sigalg=settings.SPID_SIG_ALG, ) state.sync() return HttpResponse(http_info["data"])
def spid_sp_authn_request(conf, selected_idp, next_url=""): client = Saml2Client(conf) logger.debug(f"Redirecting user to the IdP via {SAML2_DEFAULT_BINDING} binding.") # use the html provided by pysaml2 if no template was specified or it didn't exist # SPID want the fqdn of the IDP, not the SSO endpoint location_fixed = selected_idp location = client.sso_location(selected_idp, SAML2_DEFAULT_BINDING) authn_req = saml2.samlp.AuthnRequest() authn_req.destination = location_fixed # spid-testenv2 preleva l'attribute consumer service dalla authnRequest (anche se questo sta già nei metadati...) authn_req.attribute_consuming_service_index = "0" # issuer issuer = saml2.saml.Issuer() issuer.name_qualifier = client.config.entityid issuer.text = client.config.entityid issuer.format = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity" authn_req.issuer = issuer # message id authn_req.id = saml2.s_utils.sid() authn_req.version = saml2.VERSION # "2.0" authn_req.issue_instant = saml2.time_util.instant() name_id_policy = saml2.samlp.NameIDPolicy() name_id_policy.format = settings.SPID_NAMEID_FORMAT authn_req.name_id_policy = name_id_policy authn_context = requested_authn_context(class_ref=settings.SPID_AUTH_CONTEXT) authn_req.requested_authn_context = authn_context # if SPID authentication level is > 1 then forceauthn must be True authn_req.force_authn = settings.SPID_ACR_FAUTHN_MAP[settings.SPID_AUTH_CONTEXT] authn_req.protocol_binding = SAML2_DEFAULT_BINDING assertion_consumer_service_url = client.config._sp_endpoints[ "assertion_consumer_service" ][0][0] authn_req.assertion_consumer_service_url = assertion_consumer_service_url authn_req_signed = client.sign( authn_req, sign_prepare=False, sign_alg=settings.SPID_SIG_ALG, digest_alg=settings.SPID_DIG_ALG, ) logger.debug(f"AuthRequest to {selected_idp}: {authn_req_signed}") relay_state = next_url or reverse("djangosaml2:saml2_echo_attributes") http_info = client.apply_binding( SAML2_DEFAULT_BINDING, authn_req_signed, location, sign=True, sigalg=settings.SPID_SIG_ALG, relay_state=relay_state, ) return dict( http_response=http_info, authn_request=authn_req_signed, relay_state=relay_state, session_id=authn_req.id, )
def spid_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 POST protocol binding. """ logger.debug('SPID 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(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}) # this works only if request came from wayf 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}) else: # otherwise is the first one try: selected_idp = list(idps.keys())[0] except TypeError as e: logger.error('Unable to know which IdP to use') return HttpResponse(text_type(e)) # choose a binding to try first # sign_requests = getattr(conf, '_sp_authn_requests_signed', False) binding = BINDING_HTTP_POST 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 != BINDING_HTTP_POST: raise UnsupportedBinding('IDP %s does not support %s or %s', selected_idp, BINDING_HTTP_POST, BINDING_HTTP_REDIRECT) client = Saml2Client(conf) logger.debug('Redirecting user to the IdP via %s binding.', binding) # use the html provided by pysaml2 if no template was specified or it didn't exist # SPID want the fqdn of the IDP, not the SSO endpoint location_fixed = selected_idp location = client.sso_location(selected_idp, binding) # ...hope to see the SSO endpoint soon in spid-testenv2 authn_req = saml2.samlp.AuthnRequest() authn_req.destination = location_fixed # spid-testenv2 preleva l'attribute consumer service dalla authnRequest (anche se questo sta già nei metadati...) authn_req.attribute_consuming_service_index = "0" # import pdb; pdb.set_trace() issuer = saml2.saml.Issuer() issuer.name_qualifier = client.config.entityid issuer.text = client.config.entityid issuer.format = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity" authn_req.issuer = issuer # message id authn_req.id = saml2.s_utils.sid() authn_req.version = saml2.VERSION # "2.0" authn_req.issue_instant = saml2.time_util.instant() name_id_policy = saml2.samlp.NameIDPolicy() # del(name_id_policy.allow_create) name_id_policy.format = settings.SPID_NAMEID_FORMAT authn_req.name_id_policy = name_id_policy authn_context = requested_authn_context(class_ref=settings.SPID_AUTH_CONTEXT) authn_req.requested_authn_context = authn_context authn_req.protocol_binding = settings.SPID_DEFAULT_BINDING assertion_consumer_service_url = client.config._sp_endpoints['assertion_consumer_service'][0][0] authn_req.assertion_consumer_service_url = assertion_consumer_service_url #'http://sp1.testunical.it:8000/saml2/acs/' authn_req_signed = client.sign(authn_req, sign_prepare=False, sign_alg=settings.SPID_ENC_ALG, digest_alg=settings.SPID_DIG_ALG) session_id = authn_req.id _req_str = authn_req_signed logger.debug('AuthRequest to {}: {}'.format(selected_idp, (_req_str))) http_info = client.apply_binding(binding, _req_str, location, sign=True, sigalg=settings.SPID_ENC_ALG) # 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 HttpResponse(http_info['data'])
def spid_logout(request, config_loader_path=None, **kwargs): """SAML Logout Request initiator This view initiates the SAML2 Logout request using the pysaml2 library to create the LogoutRequest. """ 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 contain the subject id for user %s', request.user) 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") slo_req = saml2.samlp.LogoutRequest() binding = settings.SPID_DEFAULT_BINDING location_fixed = subject_id.name_qualifier location = location_fixed slo_req.destination = location_fixed # spid-testenv2 preleva l'attribute consumer service dalla authnRequest (anche se questo sta già nei metadati...) slo_req.attribute_consuming_service_index = "0" # import pdb; pdb.set_trace() issuer = saml2.saml.Issuer() issuer.name_qualifier = client.config.entityid issuer.text = client.config.entityid issuer.format = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity" slo_req.issuer = issuer # message id slo_req.id = saml2.s_utils.sid() slo_req.version = saml2.VERSION # "2.0" slo_req.issue_instant = saml2.time_util.instant() # oggetto slo_req.name_id = subject_id session_info = client.users.get_info_from(slo_req.name_id, subject_id.name_qualifier, False) session_indexes = [session_info['session_index']] # aggiungere session index if session_indexes: sis = [] for si in session_indexes: if isinstance(si, saml2.samlp.SessionIndex): sis.append(si) else: sis.append(saml2.samlp.SessionIndex(text=si)) slo_req.session_index = sis slo_req.protocol_binding = binding assertion_consumer_service_url = client.config._sp_endpoints['assertion_consumer_service'][0][0] slo_req.assertion_consumer_service_url = assertion_consumer_service_url slo_req_signed = client.sign(slo_req, sign_prepare=False, sign_alg=settings.SPID_ENC_ALG, digest_alg=settings.SPID_DIG_ALG) session_id = slo_req.id _req_str = slo_req_signed logger.debug('LogoutRequest to {}: {}'.format(subject_id.name_qualifier, repr_saml(_req_str))) # get slo from metadata slo_location = None # for k,v in client.metadata.metadata.items(): # idp_nq = v.entity.get(subject_id.name_qualifier) # if idp_nq: # slo_location = idp_nq['idpsso_descriptor'][0]['single_logout_service'][0]['location'] slo_location = client.metadata.single_logout_service(subject_id.name_qualifier, binding, "idpsso")[0]['location'] if not slo_location: logger.error('Unable to know SLO endpoint in {}'.format(subject_id.name_qualifier)) return HttpResponse(text_type(e)) http_info = client.apply_binding(binding, _req_str, slo_location, sign=True, sigalg=settings.SPID_ENC_ALG) state.sync() return HttpResponse(http_info['data'])
def spid_sp_authn_request(conf, selected_idp, binding, name_id_format, authn_context, sig_alg, dig_alg, next_url=''): client = Saml2Client(conf) logger.debug(f'Redirecting user to the IdP via {binding} binding.') # use the html provided by pysaml2 if no template was specified or it didn't exist # SPID want the fqdn of the IDP, not the SSO endpoint location_fixed = selected_idp location = client.sso_location(selected_idp, binding) authn_req = saml2.samlp.AuthnRequest() authn_req.destination = location_fixed # spid-testenv2 preleva l'attribute consumer service dalla authnRequest (anche se questo sta già nei metadati...) authn_req.attribute_consuming_service_index = "0" # issuer issuer = saml2.saml.Issuer() issuer.name_qualifier = client.config.entityid issuer.text = client.config.entityid issuer.format = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity" authn_req.issuer = issuer # message id authn_req.id = saml2.s_utils.sid() authn_req.version = saml2.VERSION # "2.0" authn_req.issue_instant = saml2.time_util.instant() name_id_policy = saml2.samlp.NameIDPolicy() # del(name_id_policy.allow_create) name_id_policy.format = name_id_format # settings.SPID_NAMEID_FORMAT authn_req.name_id_policy = name_id_policy # settings.SPID_AUTH_CONTEXT authn_context = requested_authn_context(class_ref=authn_context) authn_req.requested_authn_context = authn_context # force_auth = true only if SpidL >= 2 # if 'SpidL1' in authn_context.authn_context_class_ref[0].text: # force_authn = 'false' # else: force_authn = 'true' authn_req.force_authn = force_authn # end force authn # settings.SPID_DEFAULT_BINDING authn_req.protocol_binding = binding assertion_consumer_service_url = client.config._sp_endpoints[ 'assertion_consumer_service'][0][0] authn_req.assertion_consumer_service_url = assertion_consumer_service_url authn_req_signed = client.sign( authn_req, sign_prepare=False, sign_alg=sig_alg, digest_alg=dig_alg, ) logger.debug(f'AuthRequest to {selected_idp}: {authn_req_signed}') relay_state = next_url or reverse('djangosaml2:saml2_echo_attributes') http_info = client.apply_binding(binding, authn_req_signed, location, sign=True, sigalg=sig_alg, relay_state=relay_state) return dict(http_response=http_info, authn_request=authn_req_signed, relay_state=relay_state, session_id=authn_req.id)
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 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: 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
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 """ attribute_mapping = attribute_mapping or get_custom_setting( 'SAML_ATTRIBUTE_MAPPING', {'uid': ('username', )}) create_unknown_user = create_unknown_user if create_unknown_user is not None else \ get_custom_setting('SAML_CREATE_UNKNOWN_USER', True) conf = get_config(config_loader_path, request) try: xmlstr = request.POST['SAMLResponse'] except KeyError: logger.warning('Missing "SAMLResponse" parameter in POST data.') raise SuspiciousOperation client = Saml2Client(conf, identity_cache=IdentityCache(request.session)) oq_cache = OutstandingQueriesCache(request.session) outstanding_queries = oq_cache.outstanding_queries() try: response = client.parse_authn_request_response(xmlstr, BINDING_HTTP_POST, outstanding_queries) except (StatusError, ToEarly): logger.exception("Error processing SAML Assertion.") return fail_acs_response(request) except ResponseLifetimeExceed: logger.info( "SAML Assertion is no longer valid. Possibly caused by network delay or replay attack.", exc_info=True) return fail_acs_response(request) except SignatureError: logger.info("Invalid or malformed SAML Assertion.", exc_info=True) return fail_acs_response(request) except StatusAuthnFailed: logger.info("Authentication denied for user by IdP.", exc_info=True) return fail_acs_response(request) except StatusRequestDenied: logger.warning("Authentication interrupted at IdP.", exc_info=True) return fail_acs_response(request) except MissingKey: logger.exception( "SAML Identity Provider is not configured correctly: certificate key is missing!" ) return fail_acs_response(request) except UnsolicitedResponse: logger.exception( "Received SAMLResponse when no request has been made.") return fail_acs_response(request) if response is None: logger.warning("Invalid SAML Assertion received (unknown error).") return fail_acs_response(request, status=400, exc_class=SuspiciousOperation) session_id = response.session_id() oq_cache.delete(session_id) # authenticate the remote user session_info = response.session_info() if callable(attribute_mapping): attribute_mapping = attribute_mapping() if callable(create_unknown_user): create_unknown_user = create_unknown_user() logger.debug('Trying to authenticate the user. Session info: %s', session_info) user = auth.authenticate(request=request, session_info=session_info, attribute_mapping=attribute_mapping, create_unknown_user=create_unknown_user) if user is None: logger.warning( "Could not authenticate user received in SAML Assertion. Session info: %s", session_info) raise PermissionDenied auth.login(request, user) _set_subject_id(request.session, session_info['name_id']) logger.debug("User %s authenticated via SSO.", user) logger.debug('Sending the post_authenticated signal') post_authenticated.send_robust(sender=user, session_info=session_info) # redirect the user to the view where he came from default_relay_state = get_custom_setting('ACS_DEFAULT_REDIRECT_URL', settings.LOGIN_REDIRECT_URL) relay_state = request.POST.get('RelayState', default_relay_state) if not relay_state: logger.warning('The RelayState parameter exists but is empty') relay_state = default_relay_state if not is_safe_url_compat(url=relay_state, allowed_hosts={request.get_host()}): relay_state = settings.LOGIN_REDIRECT_URL logger.debug('Redirecting to the RelayState: %s', relay_state) return HttpResponseRedirect(relay_state)
import base64 import requests import saml2 from saml2 import saml, BINDING_HTTP_POST from django.test.client import RequestFactory from djangosaml2.conf import get_config from djangosaml2.overrides import Saml2Client from djangosaml2.utils import available_idps # SP init ######### conf = get_config(None) client = Saml2Client(conf) # just needed to initialize the MetadataStore - it automatically fetches remote idp's metadata configured_idps = available_idps(conf) # Arguments needed to create an Attribute query ############################################### message_id = 'MSG_ID1' entityid = "http://idp1.testunical.it:9000/idp/aa/metadata" destination = "http://idp1.testunical.it:9000/aap" subject_id = "E8042FB4-4D5B-48C3-8E14-8EDD852790DD" attributes = { ('urn:oasis:names:tc:SAML:attribute:pairwise-id', "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"): "*****@*****.**", ("fiscalCode", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic"): "TIN-SDF7SD89F7SD98F", ("email", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic", "email"): None,