def get_saml2_config(module_path): module = imp.load_source('saml2_settings', module_path) conf = SPConfig() conf.load(module.SAML_CONFIG) return conf
def test_config_loader_with_real_conf(request): config = SPConfig() config.load( conf.create_conf(sp_host='sp.example.com', idp_hosts=['idp.example.com'], metadata_file='remote_metadata_one_idp.xml')) return config
def config_settings_loader(request=None): """Utility function to load the pysaml2 configuration. This is also the default config loader. """ conf = SPConfig() conf.load(copy.deepcopy(settings.SAML_CONFIG)) return conf
def config_settings_loader(request: Optional[HttpRequest] = None) -> SPConfig: """ Utility function to load the pysaml2 configuration. The configuration can be modified based on the request being passed. This is the default config loader, which just loads the config from the settings. """ conf = SPConfig() conf.load(copy.deepcopy(settings.SAML_CONFIG)) return conf
def test_ecp(): cnf = SPConfig() cnf.load(ECP_SP) assert cnf.endpoint("assertion_consumer_service") == ["http://lingon.catalogix.se:8087/"] eid = cnf.ecp_endpoint("130.239.16.3") assert eid == "http://example.com/idp" eid = cnf.ecp_endpoint("130.238.20.20") assert eid is None
def test_ecp(): cnf = SPConfig() cnf.load(ECP_SP) assert cnf.endpoint("assertion_consumer_service") == \ ["http://lingon.catalogix.se:8087/"] eid = cnf.ecp_endpoint("130.239.16.3") assert eid == "http://example.com/idp" eid = cnf.ecp_endpoint("130.238.20.20") assert eid is None
def create_logout_request(subject_id, destination, issuer_entity_id, req_entity_id, sign=True): config = SPConfig() config.load(sp_config) sp_client = Saml2Client(config=config) # construct a request logout_request = samlp.LogoutRequest( id='a123456', version=VERSION, destination=destination, issuer=saml.Issuer(text=req_entity_id, format=saml.NAMEID_FORMAT_ENTITY), name_id=saml.NameID(text=subject_id)) return logout_request
def get_saml_login_request(binding=BINDING_HTTP_REDIRECT): conf = SPConfig() conf.load(copy.deepcopy(sp_conf_dict)) client = Saml2Client(conf) if binding == BINDING_HTTP_REDIRECT: session_id, result = client.prepare_for_authenticate( entityid="test_generic_idp", relay_state="", binding=binding, ) return parse.parse_qs(parse.urlparse( result['headers'][0][1]).query)['SAMLRequest'][0] elif binding == BINDING_HTTP_POST: session_id, request_xml = client.create_authn_request( "http://localhost:9000/idp/sso/post", binding=binding) return base64.b64encode(bytes(request_xml, 'UTF-8'))
def _saml2_config(self): if self._v_config is None: sp_config = self._saml2_config_template() sp_config['metadata']['local'] = [self.saml2_idp_configfile] sp_config['entityid'] = self.saml2_sp_entityid sp_config['service']['sp']['name'] = self.saml2_sp_entityid sp_config['service']['sp']['url'] = self.saml2_sp_url sp_config['service']['sp']['endpoints']['assertion_consumer_service'] = [self.saml2_sp_url,] sp_config['service']['sp']['endpoints']['single_logout_service'] = ['%s/logout' % self.saml2_sp_url, BINDING_HTTP_REDIRECT] sp_config['service']['sp']['url'] = self.saml2_sp_url sp_config['xmlsec_binary'] = self.saml2_xmlsec config = SPConfig() conf=sp_config.copy() config.load(conf) self._v_config = config return self._v_config
def create_logout_request(subject_id, destination, issuer_entity_id, req_entity_id, sign=True): config = SPConfig() config.load(sp_config) sp_client = Saml2Client(config=config) # construct a request logout_request = samlp.LogoutRequest(id='a123456', version=VERSION, destination=destination, issuer=saml.Issuer( text=req_entity_id, format=saml.NAMEID_FORMAT_ENTITY), name_id=saml.NameID(text=subject_id)) return logout_request
def _saml2_config(self): if self._v_config is None: sp_config = self._saml2_config_template() sp_config["metadata"]["local"] = [self.saml2_idp_configfile] sp_config["entityid"] = self.saml2_sp_entityid sp_config["service"]["sp"]["name"] = self.saml2_sp_entityid sp_config["service"]["sp"]["url"] = self.saml2_sp_url sp_config["service"]["sp"]["endpoints"]["assertion_consumer_service"] = [self.saml2_sp_url] sp_config["service"]["sp"]["endpoints"]["single_logout_service"] = [ "%s/logout" % self.saml2_sp_url, BINDING_HTTP_REDIRECT, ] sp_config["service"]["sp"]["url"] = self.saml2_sp_url sp_config["xmlsec_binary"] = self.saml2_xmlsec config = SPConfig() conf = sp_config.copy() config.load(conf) self._v_config = config return self._v_config
def _factory(binding: str = BINDING_HTTP_REDIRECT) -> str: conf = SPConfig() conf.load(sp_conf_dict) client = Saml2Client(conf) if binding == BINDING_HTTP_REDIRECT: session_id, result = client.prepare_for_authenticate( entityid="test_generic_idp", relay_state="", binding=binding, ) return parse.parse_qs( parse.urlparse( result['headers'][0][1]).query)['SAMLRequest'][0] elif binding == BINDING_HTTP_POST: session_id, request_xml = client.create_authn_request( "http://localhost:9000/idp/sso/post", binding=binding) return base64.b64encode(bytes(request_xml, 'UTF-8')) else: raise Exception(f"Invalid binding: {binding}")
def gen_xml(self, filename, entity_id, acs, sls): # pylint: disable=no-self-use '''将SAMLAPP配置写入指定路径xml文件 ''' conf = SPConfig() endpointconfig = { "entityid": entity_id, 'entity_category': [COC], "description": "extra SP setup", "service": { "sp": { "want_response_signed": False, "authn_requests_signed": True, "logout_requests_signed": True, "endpoints": { "assertion_consumer_service": [(acs, BINDING_HTTP_POST)], "single_logout_service": [ (sls, BINDING_HTTP_REDIRECT), (sls.replace('redirect', 'post'), BINDING_HTTP_POST), ], } }, }, "key_file": BASEDIR + "/djangosaml2idp/certificates/mykey.pem", # 随便放一个私钥,并不知道SP私钥 "cert_file": BASEDIR + '/djangosaml2idp/saml2_config/sp_cert/%s.pem' % filename, "xmlsec_binary": xmlsec_path, "metadata": { "local": [BASEDIR + '/djangosaml2idp/saml2_config/idp_metadata.xml'] }, "name_form": NAME_FORMAT_URI, } conf.load(copy.deepcopy(endpointconfig)) meta_data = entity_descriptor(conf) content = text_type(meta_data).encode('utf-8') with open(BASEDIR + '/djangosaml2idp/saml2_config/%s.xml' % filename, 'wb+') as f: f.write(content)
def _saml2_config(self): if self._v_config is None: sp_config = self._saml2_config_template() sp_config['metadata']['local'] = [self.saml2_idp_configfile] sp_config['entityid'] = self.saml2_sp_entityid sp_config['service']['sp']['name'] = self.saml2_sp_entityid sp_config['service']['sp']['url'] = self.saml2_sp_url sp_config['service']['sp']['endpoints'][ 'assertion_consumer_service'] = [ self.saml2_sp_url, ] sp_config['service']['sp']['endpoints'][ 'single_logout_service'] = [ '%s/logout' % self.saml2_sp_url, BINDING_HTTP_REDIRECT ] sp_config['service']['sp']['url'] = self.saml2_sp_url sp_config['xmlsec_binary'] = self.saml2_xmlsec config = SPConfig() conf = sp_config.copy() config.load(conf) self._v_config = config return self._v_config
def test_config_loader_with_real_conf(request): config = SPConfig() config.load(conf.create_conf(sp_host='sp.example.com', idp_hosts=['idp.example.com'], metadata_file='remote_metadata_one_idp.xml')) return config
def saml_acs(request, idp_name, ms): '''SAML ACS''' xmlstr = request.POST.get("SAMLResponse") # Create setting before call pysaml2 method for current IDP # Refer to: https://pythonhosted.org/pysaml2/howto/config.html setting = { "allow_unknown_attributes": True, # full path to the xmlsec1 binary programm 'xmlsec_binary': xmlsec_path, # your entity id, usually your subdomain plus the url to the metadata view 'entityid': 'PCG:PepperPD:Entity:ID', # directory with attribute mapping 'attribute_map_dir': path.join(SSO_DIR, 'attribute-maps'), # this block states what services we provide 'service': { # we are just a lonely SP 'sp': { "allow_unsolicited": True, 'name': 'Federated Django sample SP', 'name_id_format': saml.NAMEID_FORMAT_PERSISTENT, 'endpoints': { # url and binding to the assetion consumer service view # do not change the binding or service name 'assertion_consumer_service': [ ('https://59.45.37.54/genericsso/', saml2.BINDING_HTTP_POST), ], # url and binding to the single logout service view # do not change the binding or service name 'single_logout_service': [ ('https://59.45.37.54/saml2/ls/', saml2.BINDING_HTTP_REDIRECT), ('https://59.45.37.54/saml2/ls/post', saml2.BINDING_HTTP_POST), ] }, # attributes that this project need to identify a user 'required_attributes': ['uid'], # attributes that may be useful to have but not required 'optional_attributes': ['eduPersonAffiliation'], # in this section the list of IdPs we talk to are defined 'idp': { # we do not need a WAYF service since there is # only an IdP defined here. This IdP should be # present in our metadata # the keys of this dictionary are entity ids # 'https://idp.example.com/simplesaml/saml2/idp/metadata.php': { # 'single_sign_on_service': { # saml2.BINDING_HTTP_REDIRECT: 'https://idp.example.com/simplesaml/saml2/idp/SSOService.php', # }, # 'single_logout_service': { # saml2.BINDING_HTTP_REDIRECT: 'https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php', # }, # }, }, }, }, # where the remote metadata is stored 'metadata': { 'local': [ path.join(BASEDIR, idp_name, 'FederationMetadata.xml') ], }, # set to 1 to output debugging information 'debug': 1, # === CERTIFICATE === # cert_file must be a PEM formatted certificate chain file. # example: # 'key_file': path.join(BASEDIR, 'sso/' + idp_name + 'mycert.key'), # private part # 'cert_file': path.join(BASEDIR, 'sso/' + idp_name + 'mycert.pem'), # public part # 'key_file': path.join(BASEDIR, 'sso/' + idp_name + 'mycert.key'), # private part # 'cert_file': path.join(BASEDIR, 'sso/' + idp_name + 'customappsso.base64.cer'), # public part # === OWN METADATA SETTINGS === # 'contact_person': [ # {'given_name': 'Lorenzo', # 'sur_name': 'Gil', # 'company': 'Yaco Sistemas', # 'email_address': '*****@*****.**', # 'contact_type': 'technical'}, # {'given_name': 'Angel', # 'sur_name': 'Fernandez', # 'company': 'Yaco Sistemas', # 'email_address': '*****@*****.**', # 'contact_type': 'administrative'}, # ], # === YOU CAN SET MULTILANGUAGE INFORMATION HERE === # 'organization': { # 'name': [('Yaco Sistemas', 'es'), ('Yaco Systems', 'en')], # 'display_name': [('Yaco', 'es'), ('Yaco', 'en')], # 'url': [('http://www.yaco.es', 'es'), ('http://www.yaco.com', 'en')], # }, 'valid_for': 24, # how long is our metadata valid } #** load IDP config and parse the saml response conf = SPConfig() conf.load(copy.deepcopy(setting)) client = Saml2Client(conf, identity_cache=IdentityCache(request.session)) oq_cache = OutstandingQueriesCache(request.session) outstanding_queries = oq_cache.outstanding_queries() response = client.parse_authn_request_response(xmlstr, BINDING_HTTP_POST, outstanding_queries) session_info = response.session_info() # print session_info['issuer'] # Parse ava (received attributes) as dict data = {} for k, v in session_info['ava'].items(): data[k] = v[0] return post_acs(request, ms, data)
def test_config_loader(request): config = SPConfig() config.load({'entityid': 'testentity'}) return config
def asgard_sp_config(request=None): host = "localhost" if request != None: host = request.get_host().replace(":","-") x= { # your entity id, usually your subdomain plus the url to the metadata view 'entityid': 'https://keybucket.app.nordu.net/saml2/sp/metadata', # directory with attribute mapping "attribute_map_dir" : "%s/saml2/attributemaps" % settings.BASE_DIR, # this block states what services we provide 'service': { # we are just a lonely SP 'sp' : { 'name': 'KeyBucket', 'endpoints': { # url and binding to the assertion consumer service view # do not change the binding osettingsr service name 'assertion_consumer_service': [ ('https://keybucket.app.nordu.net/saml2/sp/acs/', BINDING_HTTP_POST), ], # url and binding to the single logout service view # do not change the binding or service name 'single_logout_service': [ ('https://keybucket.app.nordu.net/saml2/sp/ls/', BINDING_HTTP_REDIRECT), ], }, # attributes that this project need to identify a user 'required_attributes': ['eduPersonPrincipalName','displayName'], } }, # where the remote metadata is stored #'metadata': { 'remote': [{'url':'http://md.swamid.se/md/swamid-idp.xml', # 'cert':'%s/saml2/credentials/md-signer.crt' % settings.BASE_DIR}] }, 'metadata': {'local': [settings.SAML_METADATA_FILE]}, # set to 1 to output debugging information 'debug': 1, # certificate "key_file" : "%s/%s.key" % (settings.SSL_KEY_DIR,host), "cert_file" : "%s/%s.crt" % (settings.SSL_CRT_DIR,host), # own metadata settings 'contact_person': [ {'given_name': 'Leif', 'sur_name': 'Johansson', 'company': 'NORDUnet', 'email_address': '*****@*****.**', 'contact_type': 'technical'}, {'given_name': 'Johan', 'sur_name': 'Berggren', 'company': 'NORDUnet', 'email_address': '*****@*****.**', 'contact_type': 'technical'}, ], # you can set multilanguage information here 'organization': { 'name': [('NORDUNet', 'en')], 'display_name': [('NORDUnet A/S', 'en')], 'url': [('http://www.nordu.net', 'en')], } } c = SPConfig() c.load(copy.deepcopy(x)) return c
class BaseTestRP(TestCase): def setUp(self): # put idp metadata in sp metadata store # self.IDP = IdPConfig() # self.IDP.load(copy.deepcopy(SAML_IDP_CONFIG)) # idp_metadata = entity_descriptor(self.IDP) cleanup_metadata() idp_md_url = reverse('uniauth_saml2_idp:saml2_idp_metadata') client = Client() idp_metadata = client.get(idp_md_url) # idp metadata into sp md store with open(mds[0], 'wb') as fd: fd.write(idp_metadata.content) # create a pysaml SP self.sp_conf = SPConfig() self.sp_conf.load(copy.deepcopy(SAML_SP_CONFIG)) self.sp_client = Saml2Client(self.sp_conf) logger.info('{} SP: {}'.format(self.__class__.__name__, self.client)) def _get_superuser_user(self): data = dict(username='******', email='*****@*****.**', is_superuser=1, is_staff=1) user = get_user_model().objects.get_or_create(**data)[0] user.set_password('admin') user.save() return user def _superuser_login(self): user = self._get_superuser_user() self.client.force_login(user) def _add_sp_md(self): self._superuser_login() # put md store through admin UI create_url = reverse('admin:uniauth_saml2_idp_metadatastore_add') data = dict(name='sptest', type='local', url=idp_md_path, kwargs='{}', is_active=1) response = self.client.post(create_url, data, follow=True) assert 'was added successfully' in response.content.decode() # put sp metadata into IDP md store sp_metadata = entity_descriptor(self.sp_conf) with open(IDP_SP_METADATA_PATH + '/sp.xml', 'wb') as fd: fd.write(sp_metadata.to_string()) def _add_sp(self): self._superuser_login() create_url = reverse('admin:uniauth_saml2_idp_serviceprovider_add') data = dict(entity_id=SAML_SP_CONFIG['entityid'], display_name='That SP display name', signing_algorithm= "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", digest_algorithm="http://www.w3.org/2001/04/xmlenc#sha256", disable_encrypted_assertions=1, is_active=1) response = self.client.post(create_url, data, follow=True) assert 'was added successfully' in response.content.decode() def _get_sp_authn_request(self): session_id, result = self.sp_client.prepare_for_authenticate( entityid=idp_eid, relay_state='/', binding=BINDING_HTTP_POST) url, data = extract_saml_authn_data(result) return url, data, session_id def _run_ldapd(self): self.ldapd = subprocess.Popen(["python3", "tests/ldapd.py"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def test_config_loader_callable(request): config = SPConfig() config.load({'entityid': 'testentity_callable'}) return config
def config_settings_loader(request: Optional[HttpRequest] = None) -> SPConfig: conf = SPConfig() if request is None or not request.path.lstrip('/').startswith( settings.SPID_URLS_PREFIX): # Not a SPID request: load SAML_CONFIG unchanged conf.load(copy.deepcopy(settings.SAML_CONFIG)) return conf # Build a SAML_CONFIG for SPID metadata_url = request.build_absolute_uri( reverse('djangosaml2_spid:spid_metadata')) saml_config = { 'entityid': metadata_url, 'attribute_map_dir': os.path.join(djangosaml2_spid_config.path, 'attribute_maps/'), 'service': { 'sp': { 'name': metadata_url, 'name_qualifier': request.build_absolute_uri('/'), 'name_id_format': [settings.SPID_NAMEID_FORMAT], 'endpoints': { 'assertion_consumer_service': [ (request.build_absolute_uri( reverse('djangosaml2_spid:saml2_acs')), saml2.BINDING_HTTP_POST), ], 'single_logout_service': [ (request.build_absolute_uri( reverse('djangosaml2_spid:saml2_ls_post')), saml2.BINDING_HTTP_POST), ], }, # Mandates that the IdP MUST authenticate the presenter directly # rather than rely on a previous security context. 'force_authn': False, # SPID 'name_id_format_allow_create': False, # attributes that this project need to identify a user 'required_attributes': ['spidCode', 'name', 'familyName', 'fiscalNumber', 'email'], 'requested_attribute_name_format': saml2.saml.NAME_FORMAT_BASIC, 'name_format': saml2.saml.NAME_FORMAT_BASIC, # attributes that may be useful to have but not required 'optional_attributes': [ 'gender', 'companyName', 'registeredOffice', 'ivaCode', 'idCard', 'digitalAddress', 'placeOfBirth', 'countyOfBirth', 'dateOfBirth', 'address', 'mobilePhone', 'expirationDate' ], 'signing_algorithm': settings.SPID_SIG_ALG, 'digest_algorithm': settings.SPID_DIG_ALG, 'authn_requests_signed': True, 'logout_requests_signed': True, # Indicates that Authentication Responses to this SP must # be signed. If set to True, the SP will not consume # any SAML Responses that are not signed. 'want_assertions_signed': True, # When set to true, the SP will consume unsolicited SAML # Responses, i.e. SAML Responses for which it has not sent # a respective SAML Authentication Request. 'allow_unsolicited': False, # Permits to have attributes not configured in attribute-mappings # otherwise...without OID will be rejected 'allow_unknown_attributes': True, }, }, 'metadata': { 'local': [settings.SPID_IDENTITY_PROVIDERS_METADATA_DIR], 'remote': [] }, # Signing 'key_file': settings.SPID_PRIVATE_KEY, 'cert_file': settings.SPID_PUBLIC_CERT, # Encryption 'encryption_keypairs': [{ 'key_file': settings.SPID_PRIVATE_KEY, 'cert_file': settings.SPID_PUBLIC_CERT, }], 'organization': copy.deepcopy(settings.SAML_CONFIG['organization']) } if settings.SAML_CONFIG.get('debug'): saml_config['debug'] = True if 'xmlsec_binary' in settings.SAML_CONFIG: saml_config['xmlsec_binary'] = copy.deepcopy( settings.SAML_CONFIG['xmlsec_binary']) else: saml_config['xmlsec_binary'] = get_xmlsec_binary( ['/opt/local/bin', '/usr/bin/xmlsec1']) if settings.SPID_SAML_CHECK_REMOTE_METADATA_ACTIVE: saml_config['metadata']['remote'].append( {'url': settings.SPID_SAML_CHECK_METADATA_URL}) if settings.SPID_TESTENV2_REMOTE_METADATA_ACTIVE: saml_config['metadata']['remote'].append( {'url': settings.SPID_TESTENV2_METADATA_URL}) logger.debug(f'SAML_CONFIG: {saml_config}') conf.load(saml_config) return conf
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 test_config_loader_with_real_conf(request): config = SPConfig() config.load( conf.create_conf(sp_host='sp.example.com', idp_hosts=['idp.example.com'])) return config
class TestEnabledRP(BaseTestRP): def setUp(self): super().setUp() self._add_sp_md() settings.SAML_DISALLOW_UNDEFINED_SP = True self._add_sp() self.sp = ServiceProvider.objects.first() self.login_data = dict(username='******', password='******') # add LDAP in settings settings.INSTALLED_APPS.append('multildap') settings.LDAP_CONNECTIONS = LDAP_CONNECTIONS settings.AUTHENTICATION_BACKENDS.append( 'uniauth_saml2_idp.auth.multildap.LdapUnicalMultiAcademiaAuthBackend' ) # disable agreement screen self.sp.agreement_screen = 0 # configure sp processors self.sp.attribute_processor = 'uniauth_saml2_idp.processors.ldap.LdapUnicalMultiAcademiaProcessor' self.sp.attribute_mapping = json.dumps({ # refeds + edugain Entities "cn": "cn", "eduPersonEntitlement": "eduPersonEntitlement", "eduPersonPrincipalName": "eduPersonPrincipalName", "schacHomeOrganization": "schacHomeOrganization", "eduPersonHomeOrganization": "eduPersonHomeOrganization", "eduPersonAffiliation": "eduPersonAffiliation", "eduPersonScopedAffiliation": "eduPersonScopedAffiliation", "eduPersonTargetedID": "eduPersonTargetedID", "mail": ["mail", "email"], "email": ["mail", "email"], "schacPersonalUniqueCode": "schacPersonalUniqueCode", "schacPersonalUniqueID": "schacPersonalUniqueID", "sn": "sn", "givenName": ["givenName", "another_possible_occourrence"], "displayName": "displayName", # custom attributes "codice_fiscale": "codice_fiscale", "matricola_studente": "matricola_studente", "matricola_dipendente": "matricola_dipendente" }) self.sp.save() # run ldapd self._run_ldapd() def test_valid_form(self): url, data, session_id = self._get_sp_authn_request() response = self.client.post(url, data, follow=True) login_response = self.client.post(login_url, data=self.login_data, follow=True) # is there a SAML response? saml_resp = re.findall(samlresponse_form_regexp, login_response.content.decode()) assert saml_resp # login again to update existing user on db login_response = self.client.post(login_url, data=self.login_data, follow=True) # test a disabled user #user = get_user_model().objects.last() #user.is_active = 0 #user.save() #login_response = self.client.post(login_url, #data=self.login_data, follow=True) def test_invalid_form(self): url, data, session_id = self._get_sp_authn_request() response = self.client.post(url, data, follow=True) login_data = {'username': '******', 'password': '******'} login_response = self.client.post(login_url, data=login_data, follow=True) assert 'is invalid' in login_response.content.decode() login_data = {'username': '******', 'password': '******'} login_response = self.client.post(login_url, data=login_data, follow=True) assert 'is invalid' in login_response.content.decode() def test_sp_attr_policy(self): # create a pysaml SP self.sp_conf = SPConfig() _sp_conf = copy.deepcopy(SAML_SP_CONFIG) _sp_conf['service']['sp']['required_attributes'] = [ 'email', 'givenName', 'eduPersonPrincipalName', 'sn', 'displayName' ] self.sp_conf.load(_sp_conf) # put sp metadata into IDP md store sp_metadata = entity_descriptor(self.sp_conf) with open(IDP_SP_METADATA_PATH + '/sp.xml', 'wb') as fd: fd.write(sp_metadata.to_string()) sp_client = Saml2Client(self.sp_conf) session_id, result = sp_client.prepare_for_authenticate( entityid=idp_eid, relay_state='/', binding=BINDING_HTTP_POST) url, data = extract_saml_authn_data(result) response = self.client.post(url, data, follow=True) # login again to update existing user on db login_response = self.client.post(login_url, data=self.login_data, follow=True) # is there a SAML response? saml_resp = re.findall(samlresponse_form_regexp, login_response.content.decode()) assert saml_resp saml_assrt = base64.b64decode(saml_resp[0]).decode() assert 'sn' in saml_assrt def test_sp_attr_policy2(self): # create a pysaml SP self.sp_conf = SPConfig() _sp_conf = copy.deepcopy(SAML_SP_CONFIG) _sp_conf['service']['sp']['required_attributes'] = [ 'email', 'givenName', 'eduPersonPrincipalName', 'sn', 'telexNumber' ] self.sp_conf.load(_sp_conf) # put sp metadata into IDP md store sp_metadata = entity_descriptor(self.sp_conf) with open(IDP_SP_METADATA_PATH + '/sp.xml', 'wb') as fd: fd.write(sp_metadata.to_string()) sp_client = Saml2Client(self.sp_conf) session_id, result = sp_client.prepare_for_authenticate( entityid=idp_eid, relay_state='/', binding=BINDING_HTTP_POST) url, data = extract_saml_authn_data(result) response = self.client.post(url, data, follow=True) # login again to update existing user on db login_response = self.client.post(login_url, data=self.login_data, follow=True) # is there a SAML response? saml_resp = re.findall(samlresponse_form_regexp, login_response.content.decode()) # assert saml_resp # saml_assrt = base64.b64decode(saml_resp[0]).decode() # assert 'telexNumber' not in saml_assrt def tearDown(self): """Kill ldapd test server """ self.ldapd.kill()
class Saml(object): """ SAML Wrapper around pysaml2. Implements SAML2 Service Provider functionality for Flask. """ def __init__(self, config, attribute_map=None): """Initialize SAML Service Provider. Args: config (dict): Service Provider config info in dict form attribute_map (dict): Mapping of attribute keys to user data """ self._config = SPConfig() self._config.load(config) if config['metadata'].get('config'): # Hacked in a way to get the IdP metadata from a python dict # rather than having to resort to loading XML from file or http. idp_config = IdPConfig() idp_config.load(config['metadata']['config'][0]) idp_entityid = config['metadata']['config'][0]['entityid'] idp_metadata_str = str(entity_descriptor(idp_config, 24)) LOGGER.debug('IdP XML Metadata for %s: %s' % ( idp_entityid, idp_metadata_str)) self._config.metadata.import_metadata( idp_metadata_str, idp_entityid) self.attribute_map = {} if attribute_map is not None: self.attribute_map = attribute_map 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 logout(self, next_url='/'): """Start SAML Authentication logout process. Args: next_url (string): HTTP URL to return user to when logout is complete. Returns: Flask Response object to return to user containing either HTTP_REDIRECT or HTTP_POST SAML message. Raises: AuthException: when unable to resolve Identity Provider single logout end-point. """ # retrieve cache state_cache = AuthDictCache(session, '_saml_state') identity_cache = IdentityCache(session, '_saml_identity') subject_id = session.get('_saml_subject_id') # don't logout if not logged in if subject_id is None: raise AuthException('Unable to retrieve subject id for logout') # fail if signing requested but no private key configured if self._config.logout_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 logout request,' ' but no private key file configured') LOGGER.debug('State cache %s' % state_cache) LOGGER.debug('Identity cache %s' % identity_cache) LOGGER.debug('Subject Id %s' % subject_id) # use pysaml2 to initiate the SAML logout request client = Saml2Client(self._config, state_cache=state_cache, identity_cache=identity_cache, logger=LOGGER) saml_response = client.global_logout(subject_id, return_to=next_url) # sync the state to cache state_cache.sync() LOGGER.debug('State cache %s' % session['_saml_state']) LOGGER.debug('Identity cache %s' % session['_saml_identity']) if saml_response[1] == "": # used SOAP BINDING successfully return redirect(next_url) LOGGER.debug('Returning Response from SAML for continuation of the' ' logout process') return make_response('\n'.join(saml_response[3]), saml_response[1], saml_response[2]) # body, status, headers def _handle_logout_request(self, client, request, subject_id, binding): """Handle SAML Authentication logout request (GET). Args: client (Saml2Client): instance of SAML client class. request (Request): Flask request object for this HTTP transaction. subject_id (string): Id of the subject we are processing the logout for. binding (string): the SAML binding method being used for this request. Returns: Flask Response object to return to user containing HTTP_REDIRECT SAML message. Raises: BadRequest: when SAML request data is missing. AuthException: when SAML request indicates logout failed. """ LOGGER.debug('Received a logout request from Identity Provider') # pysaml2 logout_request currently only returns for # BINDING_HTTP_REDIRECT. We will have it fail for anything # other than the header 'Location' try: headers, _success = client.logout_request( request.values, subject_id, binding=binding) except TypeError: raise BadRequest('SAML request is invalid') try: assert headers is not None assert headers[0][0] == 'Location' return redirect(headers[0][1]) except: raise AuthException('An error occurred during logout') def _handle_logout_response(self, client, request, binding, next_url): """Handle SAML Authentication logout response (GET or POST). Args: client (Saml2Client): instance of SAML client class. request (Request): Flask request object for this HTTP transaction. binding (string): the SAML binding method being used for this request. next_url (string): URL to get redirected to if all is successful. Returns: Flask Response object to return to user containing HTTP_REDIRECT SAML message. Raises: BadRequest: when SAML response data is missing. AuthException: when SAML response indicates logout failed. """ LOGGER.debug('Received a logout response from Identity Provider') try: saml_response = client.logout_response( request.values['SAMLResponse'], binding=binding) except TypeError: raise BadRequest('SAML response is invalid') LOGGER.debug(saml_response) if saml_response: if saml_response[1] == '': # used SOAP BINDING successfully response = redirect(next_url) else: # body, status, headers response = make_response('\n'.join(saml_response[3]), saml_response[1], saml_response[2]) # pysaml2 returns an empty 200 in some cases, # we'll redirect instead if response.status_code == 200 and not response.data: response = redirect(next_url) else: raise AuthException('An error occurred during logout') return response def handle_logout(self, request, next_url='/'): """Handle SAML Authentication logout request/response. Args: request (Request): Flask request object for this HTTP transaction. next_url (string): URL to get redirected to if all is successful. Returns: (boolean) Success, Flask Response object to return to user containing HTTP_REDIRECT SAML message. Raises: BadRequest: when SAML request/response data is missing. """ # retrieve cache state_cache = AuthDictCache(session, '_saml_state') identity_cache = IdentityCache(session, '_saml_identity') subject_id = session.get('_saml_subject_id') LOGGER.debug('State cache %s' % state_cache) LOGGER.debug('Identity cache %s' % identity_cache) LOGGER.debug('Subject Id %s' % subject_id) # use pysaml2 to complete the SAML logout request client = Saml2Client(self._config, state_cache=state_cache, identity_cache=identity_cache, logger=LOGGER) # let's try to figure out what binding is being used and what type of # logout call we are handling if request.args: binding = BINDING_HTTP_REDIRECT elif request.form: binding = BINDING_HTTP_POST else: # The SOAP binding is only valid on logout requests which currently # pysaml2 doesn't support. raise BadRequest('Unable to find supported binding') if 'SAMLRequest' in request.values: response = self._handle_logout_request( client, request, subject_id, binding) elif 'SAMLResponse' in request.values: response = self._handle_logout_response( client, request, binding, next_url) else: raise BadRequest('Unable to find SAMLRequest or SAMLResponse') # cache the state and remove subject if logout was successful success = identity_cache.get_identity(subject_id) == ({}, []) if success: session.pop('_saml_subject_id') state_cache.sync() LOGGER.debug('State cache %s' % session['_saml_state']) LOGGER.debug('Identity cache %s' % session['_saml_identity']) LOGGER.debug( 'Returning redirect to complete/continue the logout process') return success, response def get_metadata(self): """Returns SAML Service Provider Metadata""" edesc = entity_descriptor(self._config, 24) if self._config.key_file: edesc = sign_entity_descriptor(edesc, 24, None, security_context(self._config)) response = make_response(str(edesc)) response.headers['Content-type'] = 'text/xml; charset=utf-8' return response
def test_config_loader_with_real_conf(request): config = SPConfig() config.load(conf.create_conf(sp_host='sp.example.com', idp_hosts=['idp.example.com'])) return config
def config_settings_loader(request: Optional[HttpRequest] = None) -> SPConfig: conf = SPConfig() if request is None: # Not a SPID request: load SAML_CONFIG unchanged conf.load(copy.deepcopy(settings.SAML_CONFIG)) return conf # Build a SAML_CONFIG for SPID base_url = settings.SPID_BASE_URL or request.build_absolute_uri("/") metadata_url = urljoin(base_url, settings.SPID_METADATA_URL_PATH) if settings.SPID_METADATA_URL_PATH in request.get_full_path(): _REQUIRED_ATTRIBUTES = settings.SPID_REQUIRED_ATTRIBUTES _OPTIONAL_ATTRIBUTES = settings.SPID_OPTIONAL_ATTRIBUTES else: _REQUIRED_ATTRIBUTES = settings.CIE_REQUIRED_ATTRIBUTES _OPTIONAL_ATTRIBUTES = [] saml_config = { "entityid": getattr(settings, 'SAML2_ENTITY_ID', metadata_url), "attribute_map_dir": settings.SPID_ATTR_MAP_DIR, "service": { "sp": { "name": metadata_url, "name_qualifier": base_url, "name_id_format": [settings.SPID_NAMEID_FORMAT], "endpoints": { "assertion_consumer_service": [ ( urljoin(base_url, reverse("djangosaml2_spid:saml2_acs")), saml2.BINDING_HTTP_POST, ), ], "single_logout_service": [ ( urljoin(base_url, reverse("djangosaml2_spid:saml2_ls_post")), saml2.BINDING_HTTP_POST, ), ], }, # Mandates that the IdP MUST authenticate the presenter directly # rather than rely on a previous security context. "force_authn": False, # SPID "name_id_format_allow_create": False, # attributes that this project need to identify a user "required_attributes": _REQUIRED_ATTRIBUTES, "optional_attributes": _OPTIONAL_ATTRIBUTES, "requested_attribute_name_format": saml2.saml.NAME_FORMAT_BASIC, "name_format": saml2.saml.NAME_FORMAT_BASIC, "signing_algorithm": settings.SPID_SIG_ALG, "digest_algorithm": settings.SPID_DIG_ALG, "authn_requests_signed": True, "logout_requests_signed": True, # Indicates that Authentication Responses to this SP must # be signed. If set to True, the SP will not consume # any SAML Responses that are not signed. "want_assertions_signed": True, # When set to true, the SP will consume unsolicited SAML # Responses, i.e. SAML Responses for which it has not sent # a respective SAML Authentication Request. Set to True to # let ACS endpoint work. "allow_unsolicited": settings.SAML_CONFIG.get("allow_unsolicited", False), # Permits to have attributes not configured in attribute-mappings # otherwise...without OID will be rejected "allow_unknown_attributes": True, }, }, "disable_ssl_certificate_validation": settings.SAML_CONFIG.get("disable_ssl_certificate_validation"), "metadata": { "local": [settings.SPID_IDENTITY_PROVIDERS_METADATA_DIR], "remote": [], }, # Signing "key_file": settings.SPID_PRIVATE_KEY, "cert_file": settings.SPID_PUBLIC_CERT, # Encryption "encryption_keypairs": [{ "key_file": settings.SPID_PRIVATE_KEY, "cert_file": settings.SPID_PUBLIC_CERT, }], "organization": copy.deepcopy(settings.SAML_CONFIG["organization"]), } if settings.SAML_CONFIG.get("debug"): saml_config["debug"] = True if "xmlsec_binary" in settings.SAML_CONFIG: saml_config["xmlsec_binary"] = copy.deepcopy( settings.SAML_CONFIG["xmlsec_binary"]) else: saml_config["xmlsec_binary"] = get_xmlsec_binary( ["/opt/local/bin", "/usr/bin/xmlsec1"]) if settings.SPID_SAML_CHECK_IDP_ACTIVE: saml_config["metadata"]["remote"].append( {"url": settings.SPID_SAML_CHECK_METADATA_URL}) if settings.SPID_DEMO_IDP_ACTIVE: saml_config["metadata"]["remote"].append( {"url": settings.SPID_DEMO_METADATA_URL}) if settings.SPID_VALIDATOR_IDP_ACTIVE: saml_config["metadata"]["remote"].append( {"url": settings.SPID_VALIDATOR_METADATA_URL}) logger.debug(f"SAML_CONFIG: {saml_config}") conf.load(saml_config) return conf