def __init__(self, config_module, config=None): """ :type config_module: str :type config: {dict} :param config_module: Path to a file containing the SP SAML configuration. :param config: SP SAML configuration. """ if config is None: config = config_factory('sp', config_module) Saml2Client.__init__(self, config)
def handle_discovery_response(self, request): """Handle SAML Discovery Service response. This method is basically a wrapper around `authenticate` with a little extra logic for getting the `entityID` out of the request and the next_url and binding that was previously submitted to `authenticate` from the user's session. Args: request (Request): Flask request object for this HTTP transaction. Returns: Flask Response object to return to user containing either HTTP_REDIRECT or HTTP_POST SAML message. Raises: AuthException: when unable to locate valid IdP. BadRequest: when invalid result returned from SAML client. """ session_id = request.args.get('session_id') next_url = "/" # Retrieve cache. Get `next_url` from cache. outstanding_queries_cache = \ AuthDictCache(session, '_saml_outstanding_queries') if session_id in outstanding_queries_cache.keys(): next_url = outstanding_queries_cache[session_id] del outstanding_queries_cache[session_id] outstanding_queries_cache.sync() # Get the selected IdP from the Discovery Service response. selected_idp = Saml2Client.parse_discovery_service_response( query=request.query_string) return self.authenticate(next_url=next_url, selected_idp=selected_idp)
def _handle_discovery_request(self): """Handle SAML Discovery Service request. This method is called internally by the `authenticate` method when multiple acceptable IdPs are detected. Returns: Tuple containing session Id and Flask Response object to return to user containing either HTTP_REDIRECT to configured Discovery Service end point. Raises: AuthException: when unable to find discovery response end point. """ session_id = sid() try: return_url = self._config.getattr( 'endpoints', 'sp')['discovery_response'][0][0] except KeyError: raise AuthException( "Multiple IdPs configured with no configured Discovery" + \ " response end point.") return_url += "?session_id=%s" % session_id disco_url = Saml2Client.create_discovery_service_request( self.discovery_service_end_point, self._config.entityid, **{'return': return_url}) LOGGER.debug("Redirect to Discovery Service %s", disco_url) return (session_id, make_response('', 302, {'Location': disco_url}))
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)
def saml_client_for(idp_name): """ Given the name of an IdP, return a configuation. The configuration is a hash for use by saml2.config.Config """ if idp_name not in current_app.config['SAML_IDP_SETTINGS']: raise Exception( f'Settings for IDP "{idp_name}" not found on SAML_IDP_SETTINGS.') acs_url = url_for('auth.saml_sso', idp_name=idp_name, _external=True) https_acs_url = url_for('auth.saml_sso', idp_name=idp_name, _external=True, _scheme='https') # SAML metadata changes very rarely. On a production system, # this data should be cached as approprate for your production system. rv = requests.get( current_app.config['SAML_IDP_SETTINGS'][idp_name]['metadata_url']) current_app.logger.debug('rv.rext: %s', rv.text) entityid = current_app.config['SAML_IDP_SETTINGS'][idp_name].get( 'entityid', acs_url) settings = { 'entityid': entityid, 'metadata': { 'inline': [rv.text], }, 'service': { 'sp': { 'endpoints': { 'assertion_consumer_service': [(acs_url, BINDING_HTTP_REDIRECT), (acs_url, BINDING_HTTP_POST), (https_acs_url, BINDING_HTTP_REDIRECT), (https_acs_url, BINDING_HTTP_POST)], }, # Don't verify that the incoming requests originate from us via # the built-in cache for authn request ids in pysaml2 'allow_unsolicited': True, # Don't sign authn requests, since signed requests only make # sense in a situation where you control both the SP and IdP 'authn_requests_signed': False, 'logout_requests_signed': True, 'want_assertions_signed': True, 'want_response_signed': False } } } current_app.logger.info('settings: %s', settings) saml2_config = Saml2Config() saml2_config.load(settings) saml2_config.allow_unknown_attributes = True saml2_client = Saml2Client(config=saml2_config) return saml2_client
def setup_class(self): self.server = Server("idp_conf") conf = config.SPConfig() conf.load_file("server_conf") self.client = Saml2Client(conf)
def test_complete_flow(): client = ecp_client.Client("user", "password", metadata_file=full_path("idp_all.xml")) sp = Saml2Client(config_file=dotname("servera_conf")) with closing(Server(config_file=dotname("idp_all_conf"))) as idp: IDP_ENTITY_ID = idp.config.entityid #SP_ENTITY_ID = sp.config.entityid # ------------ @Client ----------------------------- headers = client.add_paos_headers([]) assert len(headers) == 2 # ------------ @SP ----------------------------- response = DummyResponse(set_list2dict(headers)) assert sp.can_handle_ecp_response(response) sid, message = sp.create_ecp_authn_request(IDP_ENTITY_ID, relay_state="XYZ") # ------------ @Client ----------------------------- respdict = client.parse_soap_message(message) cargs = client.parse_sp_ecp_response(respdict) assert isinstance(respdict["body"], AuthnRequest) assert len(respdict["header"]) == 2 item0 = respdict["header"][0] assert isinstance(item0, Request) or isinstance(item0, RelayState) destination = respdict["body"].destination ht_args = client.apply_binding(BINDING_SOAP, respdict["body"], destination) # Time to send to the IDP # ----------- @IDP ------------------------------- req = idp.parse_authn_request(ht_args["data"], BINDING_SOAP) assert isinstance(req.message, AuthnRequest) # create Response and return in the SOAP response sp_entity_id = req.sender() name_id = idp.ident.transient_nameid("id12", sp.config.entityid) binding, destination = idp.pick_binding("assertion_consumer_service", [BINDING_PAOS], entity_id=sp_entity_id) resp = idp.create_ecp_authn_request_response(destination, { "eduPersonEntitlement": "Short stop", "surName": "Jeter", "givenName": "Derek", "mail": "*****@*****.**", "title": "The man" }, req.message.id, destination, sp_entity_id, name_id=name_id, authn=AUTHN) # ------------ @Client ----------------------------- # The client got the response from the IDP repackage and send it to the SP respdict = client.parse_soap_message(resp) idp_response = respdict["body"] assert isinstance(idp_response, Response) assert len(respdict["header"]) == 1 _ecp_response = None for item in respdict["header"]: if item.c_tag == "Response" and item.c_namespace == ecp_prof.NAMESPACE: _ecp_response = item #_acs_url = _ecp_response.assertion_consumer_service_url # done phase2 at the client ht_args = client.use_soap(idp_response, cargs["rc_url"], [cargs["relay_state"]]) print(ht_args) # ------------ @SP ----------------------------- respdict = sp.unpack_soap_message(ht_args["data"]) # verify the relay_state for header in respdict["header"]: inst = create_class_from_xml_string(RelayState, header) if isinstance(inst, RelayState): assert inst.text == "XYZ" # parse the response resp = sp.parse_authn_request_response(respdict["body"], None, {sid: "/"}) print(resp.response) assert resp.response.destination == "http://lingon.catalogix.se:8087/paos" assert resp.response.status.status_code.value == STATUS_SUCCESS
if _args.service_conf_module: service_conf = importlib.import_module(_args.service_conf_module) else: import service_conf HOST = service_conf.HOST PORT = service_conf.PORT # ------- HTTPS ------- # These should point to relevant files SERVER_CERT = service_conf.SERVER_CERT SERVER_KEY = service_conf.SERVER_KEY # This is of course the certificate chain for the CA that signed # your cert and all the way up to the top CERT_CHAIN = service_conf.CERT_CHAIN SP = Saml2Client(config_file="%s" % CNFBASE) POLICY = service_conf.POLICY add_urls() sign_alg = None digest_alg = None try: sign_alg = service_conf.SIGN_ALG except: pass try: digest_alg = service_conf.DIGEST_ALG except: pass ds.DefaultSignature(sign_alg, digest_alg)
def get_saml2_client(self): return Saml2Client(config=self.get_saml2_config())
def logout_service(request, 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), logger=logger) if 'SAMLResponse' in request.GET: # we started the logout logger.debug('Receiving a logout response from the IdP') response = client.logout_response(request.GET['SAMLResponse'], binding=BINDING_HTTP_REDIRECT) state.sync() if response and response[1] == '200 Ok': if next_page is None and hasattr(settings, 'LOGOUT_REDIRECT_URL'): next_page = settings.LOGOUT_REDIRECT_URL logger.debug('Performing django_logout with a next_page of %s' % next_page) return django_logout(request, next_page=next_page) else: logger.error('Unknown error during the logout') return HttpResponse('Error during logout') elif 'SAMLRequest' in request.GET: # 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: response, success = client.logout_request(request.GET, subject_id) state.sync() if success: auth.logout(request) assert response[0][0] == 'Location' url = response[0][1] return HttpResponseRedirect(url) elif response is not None: assert response[0][0] == 'Location' url = response[0][1] return HttpResponseRedirect(url) else: logger.error('Unknown error during the logout') return HttpResponse('Error during logout') else: logger.error('No SAMLResponse or SAMLRequest parameter found') raise Http404('No SAMLResponse or SAMLRequest parameter found')
from saml2.config import SPConfig from saml2.response import AuthnResponse from saml2 import BINDING_HTTP_REDIRECT, BINDING_HTTP_POST from saml2.client import Saml2Client # OutStanding Queries # outstanding = {'id-R3qGBIK1FKbybkEOo': '/', 'id-vV5JVaBZCuC2LHP9Y': '/', 'id-TH9lfrLJL4KtNuEZJ': '/', 'id-KeYf8iMkonCWaqGrd': '/', 'id-S8lzm7lkEYIwokDVZ': '/', 'id-1naCBqIuGqm31mFnC': '/', 'id-D5bhbXLDxt6nS2QtZ': '/', 'id-UCjbQ7AS1nGG5wSN5': '/', 'id-EdrCM5hBIDix23Bf5': '/', 'id-p3yvaSmx6TJPZ0qK7': '/', 'id-DgwqMaGwOJYRxnzQe': '/'} outstanding = None outstanding_certs = None conv_info = None conf = SPConfig() conf.load(copy.deepcopy(SAML_CONFIG)) client = Saml2Client(conf) # client arguments selected_idp = None came_from = '/' # conf['sp']['authn_requests_signed'] determines if saml2.BINDING_HTTP_POST or saml2.BINDING_HTTP_REDIRECT binding = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' # saml2.BINDING_HTTP_REDIRECT sign = False sigalg = None nsprefix = { 'ds': 'http://www.w3.org/2000/09/xmldsig#', 'md': 'urn:oasis:names:tc:SAML:2.0:metadata', 'samlp': 'urn:oasis:names:tc:SAML:2.0:protocol', 'xenc': 'http://www.w3.org/2001/04/xmlenc#', 'saml': 'urn:oasis:names:tc:SAML:2.0:assertion' }
def __init__(self, hs): Resource.__init__(self) self._saml_client = Saml2Client(hs.config.saml2_sp_config) self._sso_auth_handler = SSOAuthHandler(hs)
def authenticate(self, next_url='/', binding=BINDING_HTTP_REDIRECT): """Start SAML Authentication login process. Args: next_url (string): HTTP URL to return user to when authentication is complete. binding (binding): Saml2 binding method to use for request, default BINDING_HTTP_REDIRECT (don't change til HTTP_POST support is complete in pysaml2. Returns: Flask Response object to return to user containing either HTTP_REDIRECT or HTTP_POST SAML message. Raises: AuthException: when unable to locate valid IdP. BadRequest: when invalid result returned from SAML client. """ # find configured for IdP for requested binding method idp_entityid = '' idps = self._config.idps().keys() for idp in idps: if self._config.single_sign_on_services(idp, binding) != []: idp_entityid = idp break if idp_entityid == '': raise AuthException('Unable to locate valid IdP for this request') # fail if signing requested but no private key configured if self._config.authn_requests_signed == 'true': if not self._config.key_file \ or not os.path.exists(self._config.key_file): raise AuthException( 'Signature requested for this Saml authentication request,' ' but no private key file configured') LOGGER.debug('Connecting to Identity Provider %s' % idp_entityid) # retrieve cache outstanding_queries_cache = \ AuthDictCache(session, '_saml_outstanding_queries') LOGGER.debug('Outstanding queries cache %s' % ( outstanding_queries_cache)) # make pysaml2 call to authenticate client = Saml2Client(self._config, logger=LOGGER) (session_id, result) = client.authenticate( entityid=idp_entityid, relay_state=next_url, binding=binding) # The psaml2 source for this method indicates that BINDING_HTTP_POST # should not be used right now to authenticate. Regardless, we'll # check for it and act accordingly. if binding == BINDING_HTTP_REDIRECT: LOGGER.debug('Redirect to Identity Provider %s ( %s )' % ( idp_entityid, result)) response = make_response('', 302, dict([result])) elif binding == BINDING_HTTP_POST: LOGGER.warn('POST binding used to authenticate is not currently' ' supported by pysaml2 release version. Fix in place in repo.') LOGGER.debug('Post to Identity Provider %s ( %s )' % ( idp_entityid, result)) response = make_response('\n'.join(result), 200) else: raise BadRequest('Invalid result returned from SAML client') LOGGER.debug( 'Saving session_id ( %s ) in outstanding queries' % session_id) # cache the outstanding query outstanding_queries_cache.update({session_id: next_url}) outstanding_queries_cache.sync() LOGGER.debug('Outstanding queries cache %s' % ( session['_saml_outstanding_queries'])) return response
def handle_assertion(self, request): """Handle SAML Authentication login assertion (POST). Args: request (Request): Flask request object for this HTTP transaction. Returns: User Id (string), User attributes (dict), Redirect Flask response object to return user to now that authentication is complete. Raises: BadRequest: when error with SAML response from Identity Provider. AuthException: when unable to locate uid attribute in response. """ if not request.form.get('SAMLResponse'): raise BadRequest('SAMLResponse missing from POST') # retrieve cache outstanding_queries_cache = \ AuthDictCache(session, '_saml_outstanding_queries') identity_cache = IdentityCache(session, '_saml_identity') LOGGER.debug('Outstanding queries cache %s' % ( outstanding_queries_cache)) LOGGER.debug('Identity cache %s' % identity_cache) # use pysaml2 to process the SAML authentication response client = Saml2Client(self._config, identity_cache=identity_cache, logger=LOGGER) saml_response = client.response( dict(SAMLResponse=request.form['SAMLResponse']), outstanding_queries_cache) if saml_response is None: raise BadRequest('SAML response is invalid') # make sure outstanding query cache is cleared for this session_id session_id = saml_response.session_id() if session_id in outstanding_queries_cache.keys(): del outstanding_queries_cache[session_id] outstanding_queries_cache.sync() # retrieve session_info saml_session_info = saml_response.session_info() LOGGER.debug('SAML Session Info ( %s )' % saml_session_info) # retrieve user data via API try: if self.attribute_map.get('uid', 'name_id') == 'name_id': user_id = saml_session_info.get('name_id') else: user_id = saml_session_info['ava'] \ .get(self.attribute_map.get('uid'))[0] except: raise AuthException('Unable to find "%s" attribute in response' % ( self.attribute_map.get('uid', 'name_id'))) # Future: map attributes to user info user_attributes = dict() # set subject Id in cache to retrieved name_id session['_saml_subject_id'] = saml_session_info.get('name_id') LOGGER.debug('Outstanding queries cache %s' % ( session['_saml_outstanding_queries'])) LOGGER.debug('Identity cache %s' % session['_saml_identity']) LOGGER.debug('Subject Id %s' % session['_saml_subject_id']) relay_state = request.form.get('RelayState', '/') LOGGER.debug('Returning redirect to %s' % relay_state) return user_id, user_attributes, redirect(relay_state)
def _get_client_for_provider(self, base_url=None): spConfig = self._get_config_for_provider(base_url) saml_client = Saml2Client(config=spConfig) return saml_client
from saml2.sigver import RSA_SHA1 from saml2.server import Server from saml2 import BINDING_HTTP_REDIRECT from saml2.client import Saml2Client from saml2.config import SPConfig from urlparse import parse_qs from pathutils import dotname __author__ = 'rolandh' idp = Server(config_file=dotname("idp_all_conf")) conf = SPConfig() conf.load_file(dotname("servera_conf")) sp = Saml2Client(conf) def test(): srvs = sp.metadata.single_sign_on_service(idp.config.entityid, BINDING_HTTP_REDIRECT) destination = srvs[0]["location"] req_id, req = sp.create_authn_request(destination, id="id1") try: key = sp.sec.key except AttributeError: key = import_rsa_key_from_file(sp.sec.key_file) info = http_redirect_message(req, destination, relay_state="RS",
def get_saml_client(org): """ Return SAML configuration. The configuration is a hash for use by saml2.config.Config """ saml_type = org.get_setting("auth_saml_type") entity_id = org.get_setting("auth_saml_entity_id") sso_url = org.get_setting("auth_saml_sso_url") x509_cert = org.get_setting("auth_saml_x509_cert") metadata_url = org.get_setting("auth_saml_metadata_url") sp_settings = org.get_setting("auth_saml_sp_settings") if settings.SAML_SCHEME_OVERRIDE: acs_url = url_for( "saml_auth.idp_initiated", org_slug=org.slug, _external=True, _scheme=settings.SAML_SCHEME_OVERRIDE, ) else: acs_url = url_for("saml_auth.idp_initiated", org_slug=org.slug, _external=True) saml_settings = { "metadata": {"remote": [{"url": metadata_url}]}, "service": { "sp": { "endpoints": { "assertion_consumer_service": [ (acs_url, BINDING_HTTP_REDIRECT), (acs_url, BINDING_HTTP_POST), ] }, # Don't verify that the incoming requests originate from us via # the built-in cache for authn request ids in pysaml2 "allow_unsolicited": True, # Don't sign authn requests, since signed requests only make # sense in a situation where you control both the SP and IdP "authn_requests_signed": False, "logout_requests_signed": True, "want_assertions_signed": True, "want_response_signed": False, } }, } if settings.SAML_ENCRYPTION_ENABLED: encryption_dict = { "xmlsec_binary": get_xmlsec_binary(), "encryption_keypairs": [ { "key_file": settings.SAML_ENCRYPTION_PEM_PATH, "cert_file": settings.SAML_ENCRYPTION_CERT_PATH, } ], } saml_settings.update(encryption_dict) if saml_type is not None and saml_type == "static": metadata_inline = mustache_render( inline_metadata_template, entity_id=entity_id, x509_cert=x509_cert, sso_url=sso_url, ) saml_settings["metadata"] = {"inline": [metadata_inline]} if acs_url is not None and acs_url != "": saml_settings["entityid"] = acs_url if sp_settings: import json saml_settings["service"]["sp"].update(json.loads(sp_settings)) sp_config = Saml2Config() sp_config.load(saml_settings) sp_config.allow_unknown_attributes = True saml_client = Saml2Client(config=sp_config) return saml_client
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
from saml2 import BINDING_HTTP_REDIRECT from saml2.client import Saml2Client from saml2.server import Server from saml2.saml import AUTHN_PASSWORD __author__ = 'rolandh' IDENTITY = { "eduPersonAffiliation": ["staff", "member"], "surName": ["Jeter"], "givenName": ["Derek"], "mail": ["*****@*****.**"], "title": ["shortstop"] } sp = Saml2Client(config_file="servera_conf") idp = Server(config_file="idp_all_conf") dest = sp._sso_location(entityid="urn:mace:example.com:saml:roland:idp", binding=BINDING_HTTP_REDIRECT) authn_request = sp.create_authn_request(destination=dest) htargs = sp.apply_binding(BINDING_HTTP_REDIRECT, "%s" % authn_request, dest, "abcd") _dict = parse_qs(htargs["headers"][0][1].split('?')[1]) def main(): for i in range(1000): req = idp.parse_authn_request(_dict["SAMLRequest"][0],
def __init__(self, config_module): Saml2Client.__init__(self, config_factory('sp', config_module))
def __init__(self, config): """ :type config: {dict} :param config: SP SAML configuration. """ Saml2Client.__init__(self, config)
def setup_class(self): conf = config.SPConfig() conf.load_file("server_conf") vo_name = conf.vorg.keys()[0] self.sp = Saml2Client(conf, virtual_organization=vo_name) add_derek_info(self.sp)
dest='debug', action='store_true', help="Print debug information") _parser.add_argument('-D', dest='discosrv', help="Which disco server to use") _parser.add_argument('-A', dest='authncontext', help="An ini file containing all the Authentication " "Context definitions") _parser.add_argument('-s', dest='seed', help="Cookie seed") _parser.add_argument("config", help="SAML client config") _args = _parser.parse_args() SP = Saml2Client(config_file="%s" % _args.config) if _args.discosrv: ARGS["discosrv"] = _args.discosrv if _args.authncontext: cnf = ConfigParser.ConfigParser() cnf.read(_args.authncontext) ARGS["authncontext"] = [] for section in cnf.sections(): name = cnf.get(section, "name") url = cnf.get(section, "url") ARGS["authncontext"].append((name, url)) ARGS["authncontext"].sort() CACHE = Cache()
def post(self, request): serializer = self.serializer_class(data=request.data) serializer.is_valid(raise_exception=True) attribute_mapping = get_custom_setting( 'SAML_ATTRIBUTE_MAPPING', { 'uid': ('username', ), 'eduPersonScopedAffiliation': ('_process_saml2_affiliations', ), }, ) create_unknown_user = get_custom_setting('SAML_CREATE_UNKNOWN_USER', True) conf = get_config(request=request) client = Saml2Client(conf, identity_cache=IdentityCache(request.session)) oq_cache = OutstandingQueriesCache(request.session) outstanding_queries = oq_cache.outstanding_queries() xmlstr = serializer.validated_data['SAMLResponse'] # process the authentication response try: response = client.parse_authn_request_response( xmlstr, BINDING_HTTP_POST, outstanding_queries) except Exception as e: if isinstance(e, StatusRequestDenied): return login_failed( _('Authentication request has been denied by identity provider. ' 'Please check your credentials.')) logger.error('SAML response parsing failed %s' % e) return login_failed(_('SAML2 response has errors.')) if response is None: logger.error('SAML response is None') return login_failed( _('SAML response has errors. Please check the logs')) if response.assertion is None: logger.error('SAML response assertion is None') return login_failed( _('SAML response has errors. Please check the logs')) 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() try: user = auth.authenticate( request= request, # AxesBackend requires request for authentication session_info=session_info, attribute_mapping=attribute_mapping, create_unknown_user=create_unknown_user, ) except ValidationError as e: return login_failed(e.message) if user is None: return login_failed(_('SAML2 authentication failed.')) registration_method = settings.WALDUR_AUTH_SAML2.get('NAME', 'saml2') if user.registration_method != registration_method: user.registration_method = registration_method user.save(update_fields=['registration_method']) # required for validating SAML2 logout requests 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) token = self.refresh_token(user) logger.info( 'Authenticated with SAML token. Returning token for successful login of user %s', user, ) event_logger.saml2_auth.info( 'User {user_username} with full name {user_full_name} logged in successfully with SAML2.', event_type='auth_logged_in_with_saml2', event_context={'user': user}, ) return login_completed(token.key, 'saml2')
def test_artifact_flow(): #SP = 'urn:mace:example.com:saml:roland:sp' sp = Saml2Client(config_file="servera_conf") idp = Server(config_file="idp_all_conf") # original request binding, destination = sp.pick_binding("single_sign_on_service", entity_id=idp.config.entityid) relay_state = "RS0" req = sp.create_authn_request(destination, id="id1") artifact = sp.use_artifact(req, 1) binding, destination = sp.pick_binding("single_sign_on_service", [BINDING_HTTP_ARTIFACT], entity_id=idp.config.entityid) hinfo = sp.apply_binding(binding, "%s" % artifact, destination, relay_state) # ========== @IDP ============ artifact2 = get_msg(hinfo, binding) assert artifact == artifact2 # The IDP now wants to replace the artifact with the real request destination = idp.artifact2destination(artifact2, "spsso") msg = idp.create_artifact_resolve(artifact2, destination, sid()) hinfo = idp.use_soap(msg, destination, None, False) # ======== @SP ========== msg = get_msg(hinfo, BINDING_SOAP) ar = sp.parse_artifact_resolve(msg) assert ar.artifact.text == artifact # The SP picks the request out of the repository with the artifact as the key oreq = sp.artifact[ar.artifact.text] # Should be the same as req above # Returns the information over the existing SOAP connection so # no transport information needed msg = sp.create_artifact_response(ar, ar.artifact.text) hinfo = sp.use_soap(msg, destination) # ========== @IDP ============ msg = get_msg(hinfo, BINDING_SOAP) # The IDP untangles the request from the artifact resolve response spreq = idp.parse_artifact_resolve_response(msg) # should be the same as req above assert spreq.id == req.id # That was one way, the Request from the SP # ---------------------------------------------# # Now for the other, the response from the IDP name_id = idp.ident.transient_nameid(sp.config.entityid, "derek") resp_args = idp.response_args(spreq, [BINDING_HTTP_POST]) response = idp.create_authn_response({"eduPersonEntitlement": "Short stop", "surName": "Jeter", "givenName": "Derek", "mail": "*****@*****.**", "title": "The man"}, name_id=name_id, authn=AUTHN, **resp_args) print response # with the response in hand create an artifact artifact = idp.use_artifact(response, 1) binding, destination = sp.pick_binding("single_sign_on_service", [BINDING_HTTP_ARTIFACT], entity_id=idp.config.entityid) hinfo = sp.apply_binding(binding, "%s" % artifact, destination, relay_state, response=True) # ========== SP ========= artifact3 = get_msg(hinfo, binding) assert artifact == artifact3 destination = sp.artifact2destination(artifact3, "idpsso") # Got an artifact want to replace it with the real message msg = sp.create_artifact_resolve(artifact3, destination, sid()) print msg hinfo = sp.use_soap(msg, destination, None, False) # ======== IDP ========== msg = get_msg(hinfo, BINDING_SOAP) ar = idp.parse_artifact_resolve(msg) print ar assert ar.artifact.text == artifact3 # The IDP retrieves the response from the database using the artifact as the key #oreq = idp.artifact[ar.artifact.text] binding, destination = idp.pick_binding("artifact_resolution_service", entity_id=sp.config.entityid) resp = idp.create_artifact_response(ar, ar.artifact.text) hinfo = idp.use_soap(resp, destination) # ========== SP ============ msg = get_msg(hinfo, BINDING_SOAP) sp_resp = sp.parse_artifact_resolve_response(msg) assert sp_resp.id == response.id
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=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=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['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=400) session_id, request_xml = client.create_authn_request( location, binding=binding, **kwargs) data = { 'binding': 'post', 'url': location, 'request': str(base64.b64encode(request_xml.encode('UTF-8')), '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'): """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, e: logger.error('Unable to know which IdP to use') return HttpResponse(unicode(e))
def logout_service(): """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') state = StateCache(session) identity = IdentityCache(session) client = Saml2Client(current_app.saml2_config, state_cache=state, identity_cache=identity) logout_redirect_url = current_app.config.get('SAML2_LOGOUT_REDIRECT_URL') next_page = session.get('next', logout_redirect_url) next_page = request.args.get('next', next_page) next_page = request.form.get('RelayState', next_page) if 'SAMLResponse' in request.form: # we started the logout logger.debug('Receiving a logout response from the IdP') response = client.parse_logout_request_response( request.form['SAMLResponse'], BINDING_HTTP_REDIRECT ) state.sync() if response and response.status_ok(): session.clear() return redirect(next_page) else: logger.error('Unknown error during the logout') abort(400) # logout started by the IdP elif 'SAMLRequest' in request.form: logger.debug('Receiving a logout request from the IdP') subject_id = _get_name_id(session) if subject_id is None: logger.warning( 'The session does not contain the subject id for user {0} ' 'Performing local logout'.format( session['eduPersonPrincipalName'] ) ) session.clear() return redirect(next_page) else: http_info = client.handle_logout_request( request.form['SAMLRequest'], subject_id, BINDING_HTTP_REDIRECT, relay_state=request.form['RelayState'] ) state.sync() location = get_location(http_info) session.clear() return redirect(location) logger.error('No SAMLResponse or SAMLRequest parameter found') abort(400)
def test_flow(): sp = Saml2Client(config_file="servera_conf") with closing(Server(config_file="idp_all_conf")) as idp: relay_state = "FOO" # -- dummy request --- orig_req = AuthnRequest(issuer=sp._issuer(), name_id_policy=NameIDPolicy( allow_create="true", format=NAMEID_FORMAT_TRANSIENT)) # == Create an AuthnRequest response name_id = idp.ident.transient_nameid(sp.config.entityid, "id12") binding, destination = idp.pick_binding("assertion_consumer_service", entity_id=sp.config.entityid) resp = idp.create_authn_response( { "eduPersonEntitlement": "Short stop", "surName": "Jeter", "givenName": "Derek", "mail": "*****@*****.**", "title": "The man" }, "id-123456789", destination, sp.config.entityid, name_id=name_id, authn=AUTHN) hinfo = idp.apply_binding(binding, "%s" % resp, destination, relay_state) # ------- @SP ---------- xmlstr = get_msg(hinfo, binding) aresp = sp.parse_authn_request_response(xmlstr, binding, {resp.in_response_to: "/"}) binding, destination = sp.pick_binding("authn_query_service", entity_id=idp.config.entityid) authn_context = requested_authn_context(INTERNETPROTOCOLPASSWORD) subject = aresp.assertion.subject aq_id, aq = sp.create_authn_query(subject, destination, authn_context) print(aq) assert isinstance(aq, AuthnQuery) binding = BINDING_SOAP hinfo = sp.apply_binding(binding, "%s" % aq, destination, "state2") # -------- @IDP ---------- xmlstr = get_msg(hinfo, binding) pm = idp.parse_authn_query(xmlstr, binding) msg = pm.message assert msg.id == aq.id p_res = idp.create_authn_query_response(msg.subject, msg.session_index, msg.requested_authn_context) print(p_res) hinfo = idp.apply_binding(binding, "%s" % p_res, "", "state2", response=True) # ------- @SP ---------- xmlstr = get_msg(hinfo, binding) final = sp.parse_authn_query_response(xmlstr, binding) print(final) assert final.response.id == p_res.id
def post(self, request): """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 """ serializer = self.serializer_class(data=request.data) if not serializer.is_valid(): errors = dict(serializer.errors) try: non_field_errors = errors.pop('non_field_errors') errors['detail'] = non_field_errors[0] except (KeyError, IndexError): pass return Response(errors, status=status.HTTP_401_UNAUTHORIZED) attribute_mapping = get_custom_setting('SAML_ATTRIBUTE_MAPPING', {'uid': ('username', )}) create_unknown_user = get_custom_setting('SAML_CREATE_UNKNOWN_USER', True) conf = get_config(request=request) client = Saml2Client(conf, logger=logger) post = {'SAMLResponse': serializer.validated_data['saml2response']} # process the authentication response # noinspection PyBroadException try: response = client.response( post, outstanding=None, # Rely on allow_unsolicited setting decode=False, # The response is already base64 decoded ) except Exception as e: logger.error('SAML response parsing failed %s' % e) response = None if response is None: return Response({'saml2response': 'SAML2 response has errors.'}, status=status.HTTP_401_UNAUTHORIZED) # authenticate the remote user session_info = response.session_info() user = auth.authenticate( session_info=session_info, attribute_mapping=attribute_mapping, create_unknown_user=create_unknown_user, ) if user is None: logger.info( 'Authentication with SAML token has failed, user not found') return Response({'detail': 'SAML2 authentication failed'}, status=status.HTTP_401_UNAUTHORIZED) post_authenticated.send_robust(sender=user, session_info=session_info) token, _ = Token.objects.get_or_create(user=user) event_logger.info( "User %s with full name %s authenticated successfully with Omani PKI.", user.username, user.full_name, extra={ 'user': user, 'event_type': 'auth_logged_in_with_pki' }) logger.info( 'Authenticated with SAML token. Returning token for successful login of user %s', user) return Response({'token': token.key})
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 or get_custom_setting( 'SAML_CREATE_UNKNOWN_USER', True) logger.debug('Assertion Consumer Service started') conf = get_config(config_loader_path, request) if 'SAMLResponse' not in request.POST: return HttpResponseBadRequest( 'Couldn\'t find "SAMLResponse" in POST data.') xmlstr = request.POST['SAMLResponse'] client = Saml2Client(conf, identity_cache=IdentityCache(request.session)) oq_cache = OutstandingQueriesCache(request.session) outstanding_queries = oq_cache.outstanding_queries() # process the authentication response response = client.parse_authn_request_response(xmlstr, BINDING_HTTP_POST, outstanding_queries) if response is None: logger.error('SAML response is None') return HttpResponseBadRequest( "SAML response has errors. Please check the logs") 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') user = auth.authenticate(session_info=session_info, attribute_mapping=attribute_mapping, create_unknown_user=create_unknown_user) if user is None: logger.error('The user is None') return HttpResponseForbidden("Permission denied") auth.login(request, user) _set_subject_id(request.session, session_info['name_id']) 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 logger.debug('Redirecting to the RelayState: %s', relay_state) return HttpResponseRedirect(relay_state)
def get_saml_client(): """ Return SAML configuration. The configuration is a hash for use by saml2.config.Config """ if settings.SAML_CALLBACK_SERVER_NAME: acs_url = settings.SAML_CALLBACK_SERVER_NAME + url_for( "saml_auth.idp_initiated") else: acs_url = url_for("saml_auth.idp_initiated", _external=True) # NOTE: # Ideally, this should fetch the metadata and pass it to # PySAML2 via the "inline" metadata type. # However, this method doesn't seem to work on PySAML2 v2.4.0 # # SAML metadata changes very rarely. On a production system, # this data should be cached as approprate for your production system. if settings.SAML_METADATA_URL != "": rv = requests.get(settings.SAML_METADATA_URL) import tempfile tmp = tempfile.NamedTemporaryFile() f = open(tmp.name, 'w') f.write(rv.text) f.close() metadata_path = tmp.name else: metadata_path = settings.SAML_LOCAL_METADATA_PATH saml_settings = { 'metadata': { # 'inline': metadata, "local": [metadata_path] }, 'service': { 'sp': { 'endpoints': { 'assertion_consumer_service': [(acs_url, BINDING_HTTP_REDIRECT), (acs_url, BINDING_HTTP_POST)], }, # Don't verify that the incoming requests originate from us via # the built-in cache for authn request ids in pysaml2 'allow_unsolicited': True, # Don't sign authn requests, since signed requests only make # sense in a situation where you control both the SP and IdP 'authn_requests_signed': False, 'logout_requests_signed': True, 'want_assertions_signed': True, 'want_response_signed': False, }, }, } if settings.SAML_ENTITY_ID != "": saml_settings['entityid'] = settings.SAML_ENTITY_ID spConfig = Saml2Config() spConfig.load(saml_settings) spConfig.allow_unknown_attributes = True saml_client = Saml2Client(config=spConfig) if settings.SAML_METADATA_URL != "": tmp.close() return saml_client
def test_construct_1(): sp = Saml2Client(config_file=dotname("servera_conf")) url = sp.create_discovery_service_request( "http://example.com/saml/disco", "https://example.com/saml/sp.xml") assert url == "http://example.com/saml/disco?entityID=https%3A%2F%2Fexample.com%2Fsaml%2Fsp.xml"
def test_basic_flow(): sp = Saml2Client(config_file="servera_conf") idp = Server(config_file="idp_all_conf") # -------- @IDP ------------- relay_state = "FOO" # -- dummy request --- orig_req = AuthnRequest(issuer=sp._issuer(), name_id_policy=NameIDPolicy( allow_create="true", format=NAMEID_FORMAT_TRANSIENT)) # == Create an AuthnRequest response name_id = idp.ident.transient_nameid("id12", sp.config.entityid) binding, destination = idp.pick_binding("assertion_consumer_service", entity_id=sp.config.entityid) resp = idp.create_authn_response( { "eduPersonEntitlement": "Short stop", "surName": "Jeter", "givenName": "Derek", "mail": "*****@*****.**", "title": "The man" }, "id-123456789", destination, sp.config.entityid, name_id=name_id, authn=AUTHN) hinfo = idp.apply_binding(binding, "%s" % resp, destination, relay_state) # --------- @SP ------------- xmlstr = get_msg(hinfo, binding) aresp = sp.parse_authn_request_response(xmlstr, binding, {resp.in_response_to: "/"}) # == Look for assertion X asid = aresp.assertion.id binding, destination = sp.pick_binding("assertion_id_request_service", entity_id=idp.config.entityid) hinfo = sp.apply_binding(binding, asid, destination) # ---------- @IDP ------------ aid = get_msg(hinfo, binding, response=False) # == construct response resp = idp.create_assertion_id_request_response(aid) hinfo = idp.apply_binding(binding, "%s" % resp, None, "", response=True) # ----------- @SP ------------- xmlstr = get_msg(hinfo, binding, response=True) final = sp.parse_assertion_id_request_response(xmlstr, binding) print final.response assert isinstance(final.response, Assertion)