def test_1(self): server = Server("idp_slo_redirect_conf") req_id, request = _logout_request("sp_slo_redirect_conf") print(request) bindings = [BINDING_HTTP_REDIRECT] response = server.create_logout_response(request, bindings) binding, destination = server.pick_binding("single_logout_service", bindings, "spsso", request) http_args = server.apply_binding(binding, "%s" % response, destination, "relay_state", response=True) assert len(http_args) == 4 assert http_args["headers"][0][0] == "Location" assert http_args["data"] == []
def test_1(self): server = Server("idp_slo_redirect_conf") req_id, request = _logout_request("sp_slo_redirect_conf") print(request) bindings = [BINDING_HTTP_REDIRECT] response = server.create_logout_response(request, bindings) binding, destination = server.pick_binding("single_logout_service", bindings, "spsso", request) http_args = server.apply_binding(binding, "%s" % response, destination, "relay_state", response=True) assert len(http_args) == 4 assert http_args["headers"][0][0] == "Location" assert http_args["data"] == []
def test_flow(): sp = Saml2Client(config_file="servera_conf") idp = Server(config_file="idp_all_conf") binding, destination = sp.pick_binding("manage_name_id_service", entity_id=idp.config.entityid) nameid = NameID(format=NAMEID_FORMAT_TRANSIENT, text="foobar") newid = NewID(text="Barfoo") mid, mreq = sp.create_manage_name_id_request(destination, name_id=nameid, new_id=newid) print(mreq) rargs = sp.apply_binding(binding, "%s" % mreq, destination, "") # --------- @IDP -------------- _req = idp.parse_manage_name_id_request(rargs["data"], binding) print((_req.message)) mnir = idp.create_manage_name_id_response(_req.message, None) if binding != BINDING_SOAP: binding, destination = idp.pick_binding("manage_name_id_service", entity_id=sp.config.entityid) else: destination = "" respargs = idp.apply_binding(binding, "%s" % mnir, destination, "") print(respargs) # ---------- @SP --------------- _response = sp.parse_manage_name_id_request_response( respargs["data"], binding) print((_response.response)) assert _response.response.id == mnir.id
def test_flow(): sp = Saml2Client(config_file="servera_conf") idp = Server(config_file="idp_all_conf") binding, destination = sp.pick_binding("manage_name_id_service", entity_id=idp.config.entityid) nameid = NameID(format=NAMEID_FORMAT_TRANSIENT, text="foobar") newid = NewID(text="Barfoo") mid, midq = sp.create_manage_name_id_request(destination, name_id=nameid, new_id=newid) print midq rargs = sp.apply_binding(binding, "%s" % midq, destination, "") # --------- @IDP -------------- _req = idp.parse_manage_name_id_request(rargs["data"], binding) print _req.message mnir = idp.create_manage_name_id_response(_req.message, [binding]) if binding != BINDING_SOAP: binding, destination = idp.pick_binding("manage_name_id_service", entity_id=sp.config.entityid) else: destination = "" respargs = idp.apply_binding(binding, "%s" % mnir, destination, "") print respargs # ---------- @SP --------------- _response = sp.parse_manage_name_id_request_response(respargs["data"], binding) print _response.response assert _response.response.id == mnir.id
def test_handle_logout_soap(): sp = Saml2Client(config_file="servera_conf") idp = Server(config_file="idp_all_conf") policy = NameIDPolicy(format=NAMEID_FORMAT_TRANSIENT, sp_name_qualifier=sp.config.entityid, allow_create="true") name_id = idp.ident.construct_nameid("subject", name_id_policy=policy) binding, destination = idp.pick_binding("single_logout_service", [BINDING_SOAP], entity_id=sp.config.entityid) rid, request = idp.create_logout_request(destination, idp.config.entityid, name_id=name_id) args = idp.apply_binding(BINDING_SOAP, "%s" % request, destination) # register the user session_info = { "name_id": name_id, "issuer": idp.config.entityid, "not_on_or_after": in_a_while(minutes=15), "ava": { "givenName": "Anders", "surName": "Andersson", "mail": "*****@*****.**" } } sp.users.add_information_about_person(session_info) reply_args = sp.handle_logout_request(args["data"], name_id, binding, sign=False) print(reply_args) assert reply_args["method"] == "POST" assert reply_args["headers"] == [('content-type', 'application/soap+xml')] #if __name__ == "__main__": # test_handle_logout_soap()
class SamlIDP(service.Service): def __init__(self, environ, start_response, conf, cache, incoming): """ Constructor for the class. :param environ: WSGI environ :param start_response: WSGI start response function :param conf: The SAML configuration :param cache: Cache with active sessions """ service.Service.__init__(self, environ, start_response) self.response_bindings = None self.idp = Server(config=conf, cache=cache) self.incoming = incoming def verify_request(self, query, binding): """ Parses and verifies the SAML Authentication Request :param query: The SAML authn request, transport encoded :param binding: Which binding the query came in over :returns: dictionary """ if not query: logger.info("Missing QUERY") resp = Unauthorized('Unknown user') return {"response": resp(self.environ, self.start_response)} req_info = self.idp.parse_authn_request(query, binding) logger.info("parsed OK") _authn_req = req_info.message logger.debug("%s" % _authn_req) # Check that I know where to send the reply to. try: binding_out, destination = self.idp.pick_binding( "assertion_consumer_service", bindings=self.response_bindings, entity_id=_authn_req.issuer.text, request=_authn_req) except Exception as err: logger.error("Couldn't find receiver endpoint: %s" % err) raise logger.debug("Binding: %s, destination: %s" % (binding_out, destination)) resp_args = {} try: resp_args = self.idp.response_args(_authn_req) _resp = None except UnknownPrincipal as excp: _resp = self.idp.create_error_response(_authn_req.id, destination, excp) except UnsupportedBinding as excp: _resp = self.idp.create_error_response(_authn_req.id, destination, excp) req_args = {} for key in ["subject", "name_id_policy", "conditions", "requested_authn_context", "scoping", "force_authn", "is_passive"]: try: val = getattr(_authn_req, key) except AttributeError: pass else: if val is not None: req_args[key] = val return {"resp_args": resp_args, "response": _resp, "authn_req": _authn_req, "req_args": req_args} def handle_authn_request(self, binding_in): """ Deal with an authentication request :param binding_in: Which binding was used when receiving the query :return: A response if an error occurred or session information in a dictionary """ _request = self.unpack(binding_in) _binding_in = service.INV_BINDING_MAP[binding_in] try: _dict = self.verify_request(_request["SAMLRequest"], _binding_in) except UnknownPrincipal as excp: logger.error("UnknownPrincipal: %s" % (excp,)) resp = ServiceError("UnknownPrincipal: %s" % (excp,)) return resp(self.environ, self.start_response) except UnsupportedBinding as excp: logger.error("UnsupportedBinding: %s" % (excp,)) resp = ServiceError("UnsupportedBinding: %s" % (excp,)) return resp(self.environ, self.start_response) _binding = _dict["resp_args"]["binding"] if _dict["response"]: # An error response. http_args = self.idp.apply_binding( _binding, "%s" % _dict["response"], _dict["resp_args"]["destination"], _request["RelayState"], response=True) logger.debug("HTTPargs: %s" % http_args) return self.response(_binding, http_args) else: return self.incoming(_dict, self.environ, self.start_response, _request["RelayState"]) def construct_authn_response(self, identity, name_id, authn, resp_args, relay_state, sign_response=True): """ :param identity: :param name_id: :param authn: :param resp_args: :param relay_state: :param sign_response: :return: """ _resp = self.idp.create_authn_response(identity, name_id=name_id, authn=authn, sign_response=sign_response, **resp_args) http_args = self.idp.apply_binding( resp_args["binding"], "%s" % _resp, resp_args["destination"], relay_state, response=True) logger.debug("HTTPargs: %s" % http_args) resp = None if http_args["data"]: resp = Response(http_args["data"], headers=http_args["headers"]) else: for header in http_args["headers"]: if header[0] == "Location": resp = Redirect(header[1]) if not resp: resp = ServiceError("Don't know how to return response") return resp(self.environ, self.start_response) def register_endpoints(self): """ Given the configuration, return a set of URL to function mappings. """ url_map = [] idp_endpoints = self.idp.config.getattr("endpoints", "idp") for endp, binding in idp_endpoints["single_sign_on_service"]: p = urlparse(endp) url_map.append(("^%s/(.*)$" % p.path[1:], ("IDP", "handle_authn_request", service.BINDING_MAP[binding]))) url_map.append(("^%s$" % p.path[1:], ("IDP", "handle_authn_request", service.BINDING_MAP[binding]))) return url_map
def test_complete_flow(): client = ecp_client.Client("user", "password", metadata_file=full_path("idp_all.xml")) sp = Saml2Client(config_file=dotname("servera_conf")) idp = Server(config_file=dotname("idp_all_conf")) 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
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)
class IdpServer(object): ticket = {} responses = {} challenges = {} _binding_mapping = { 'http-redirect': BINDING_HTTP_REDIRECT, 'http-post': BINDING_HTTP_POST } _endpoint_types = ['single_sign_on_service', 'single_logout_service'] _spid_levels = [ 'https://www.spid.gov.it/SpidL1', 'https://www.spid.gov.it/SpidL2', 'https://www.spid.gov.it/SpidL3' ] _spid_attributes = { 'primary': { 'spidCode': 'xs:string', 'name': 'xs:string', 'familyName': 'xs:string', 'placeOfBirth': 'xs:string', 'countryOfBirth': 'xs:string', 'dateOfBirth': 'xs:date', 'gender': 'xs:string', 'companyName': 'xs:string', 'registeredOffice': 'xs:string', 'fiscalNumber': 'xs:string', 'ivaCode': 'xs:string', 'idCard': 'xs:string', }, 'secondary': { 'mobilePhone': 'xs:string', 'email': 'xs:string', 'address': 'xs:string', 'expirationDate': 'xs:date', 'digitalAddress': 'xs:string' # PEC } } CHALLENGES_TIMEOUT = 30 # seconds def __init__(self, app, config, *args, **kwargs): """ :param app: Flask instance :param config: dictionary containing the configuration :param args: :param kwargs: """ # bind Flask app self.app = app self.user_manager = JsonUserManager() # setup self._config = config self.app.secret_key = 'sosecret' handler = RotatingFileHandler('spid.log', maxBytes=500000, backupCount=1) self.app.logger.addHandler(handler) self._prepare_server() @property def _mode(self): return 'https' if self._config.get('https', False) else 'http' def _idp_config(self): """ Process pysaml2 configuration """ key_file_path = self._config.get('key_file') cert_file_path = self._config.get('cert_file') metadata = self._config.get('metadata') metadata = metadata if metadata else [] existing_key = os.path.isfile(key_file_path) if key_file_path else None existing_cert = os.path.isfile( cert_file_path) if cert_file_path else None if not existing_key: raise BadConfiguration( 'Chiave privata dell\'IdP di test non trovata: {} non trovato'. format(key_file_path)) if not existing_cert: raise BadConfiguration( 'Certificato dell\'IdP di test non trovato: {} non trovato'. format(cert_file_path)) self.entity_id = self._config.get('hostname') if not self.entity_id: self.entity_id = self._config.get('host') self.entity_id = '{}://{}'.format(self._mode, self.entity_id) port = self._config.get('port') if port: self.entity_id = '{}:{}'.format(self.entity_id, port) idp_conf = { "entityid": self.entity_id, "description": "Spid Test IdP", "service": { "idp": { "name": "Spid Testenv", "endpoints": { "single_sign_on_service": [], "single_logout_service": [], }, "policy": { "default": { "name_form": NAME_FORMAT_BASIC, }, }, "name_id_format": [ NAMEID_FORMAT_TRANSIENT, ] }, }, "debug": 1, "key_file": self._config.get('key_file'), "cert_file": self._config.get('cert_file'), "metadata": metadata, "organization": { "display_name": "Spid testenv", "name": "Spid testenv", "url": "http://www.example.com", }, "contact_person": [ { "contact_type": "technical", "given_name": "support", "sur_name": "support", "email_address": "*****@*****.**" }, ], "logger": { "rotating": { "filename": "idp.log", "maxBytes": 500000, "backupCount": 1, }, "loglevel": "debug", } } # setup services url for _service_type in self._endpoint_types: endpoint = self._config['endpoints'][_service_type] idp_conf['service']['idp']['endpoints'][_service_type].append( ('{}{}'.format(self.entity_id, endpoint), BINDING_HTTP_REDIRECT)) idp_conf['service']['idp']['endpoints'][_service_type].append( ('{}{}'.format(self.entity_id, endpoint), BINDING_HTTP_POST)) return idp_conf def _setup_app_routes(self): """ Setup Flask routes """ # Setup SSO and SLO endpoints endpoints = self._config.get('endpoints') if endpoints: for ep_type in self._endpoint_types: _url = endpoints.get(ep_type) if _url: if not _url.startswith('/'): raise BadConfiguration( 'Errore nella configurazione delle url, i path devono essere relativi ed iniziare con "/" (slash) - url {}' .format(_url)) for _binding in self._binding_mapping.keys(): self.app.add_url_rule(_url, '{}_{}'.format( ep_type, _binding), getattr(self, ep_type), methods=[ 'GET', ]) self.app.add_url_rule('/login', 'login', self.login, methods=[ 'POST', 'GET', ]) # Endpoint for user add action self.app.add_url_rule('/add-user', 'add_user', self.add_user, methods=[ 'GET', 'POST', ]) self.app.add_url_rule('/continue-response', 'continue_response', self.continue_response, methods=[ 'POST', ]) self.app.add_url_rule('/metadata', 'metadata', self.metadata, methods=['POST', 'GET']) def _prepare_server(self): """ Setup server """ self.idp_config = Saml2Config() self.BASE = '{}://{}:{}'.format(self._mode, self._config.get('host'), self._config.get('port')) if 'entityid' not in self._config: # as fallback for entityid use host:port string self._config['entityid'] = self.BASE self.idp_config.load(cnf=self._idp_config()) self.server = Server(config=self.idp_config) self._setup_app_routes() # setup custom methods in order to # prepare the login form and verify the challenge (optional) # for every spid level (1-2-3) self.authn_broker = AuthnBroker() for index, _level in enumerate(self._spid_levels): self.authn_broker.add( authn_context_class_ref(_level), getattr(self, '_verify_spid_{}'.format(index + 1))) def _verify_spid_1(self, verify=False, **kwargs): self.app.logger.debug('spid level 1 - verifica ({})'.format(verify)) return self._verify_spid(1, verify, **kwargs) def _verify_spid_2(self, verify=False, **kwargs): self.app.logger.debug('spid level 2 - verifica ({})'.format(verify)) return self._verify_spid(2, verify, **kwargs) def _verify_spid_3(self, verify=False, **kwargs): self.app.logger.debug('spid level 3 - verifica ({})'.format(verify)) return self._verify_spid(3, verify, **kwargs) def _verify_spid(self, level=1, verify=False, **kwargs): """ :param level: integer, SPID level :param verify: boolean, if True verify spid extra challenge (otp etc.), if False prepare the challenge :param kwargs: dictionary, extra arguments """ if verify: # Verify the challenge if level == 2: # spid level 2 otp = kwargs.get('data').get('otp') key = kwargs.get('key') if key and key not in self.challenges or not otp: return False total_seconds = (datetime.now() - self.challenges[key][1]).total_seconds() # Check that opt value is equal and not expired if self.challenges[key][ 0] != otp or total_seconds > self.CHALLENGES_TIMEOUT: del self.challenges[key] return False return True else: # Prepare the challenge if level == 2: # spid level 2 # very simple otp implementation, while opt is a random 6 digits string # with a lifetime setup in the server instance key = kwargs.get('key') otp = ''.join(random.choice(string.digits) for _ in range(6)) self.challenges[key] = [otp, datetime.now()] extra_challenge = '<span>Otp ({})</span><input type="text" name="otp" />'.format( otp) else: extra_challenge = '' return extra_challenge def unpack_args(self, elems): """ Unpack arguments from request """ return dict([(k, v) for k, v in elems.items()]) def _raise_error(self, msg, extra=None): """ Raise some error using 'abort' function from Flask :param msg: string for error type :param extra: optional string for error details """ abort(Response(error_table.format(msg, extra), 200)) def _check_saml_message_restrictions(self, obj): # TODO: Implement here or somewhere (e.g. mixin on pysaml2 subclasses) # the logic to validate spid rules on saml entities raise NotImplementedError def _store_request(self, authnreq): """ Store authnrequest in a dictionary :param authnreq: authentication request string """ self.app.logger.debug('store_request: {}'.format(authnreq)) key = sha1(authnreq.xmlstr).hexdigest() # store the AuthnRequest self.ticket[key] = authnreq return key def single_sign_on_service(self): """ Process Http-Redirect or Http-POST request :param request: Flask request object """ self.app.logger.info("Http-Redirect") # Unpack parameters saml_msg = self.unpack_args(request.args) try: _key = session['request_key'] req_info = self.ticket[_key] except KeyError as e: try: binding = self._get_binding('single_sign_on_service', request) # Parse AuthnRequest req_info = self.server.parse_authn_request( saml_msg["SAMLRequest"], binding) authn_req = req_info.message except KeyError as err: self.app.logger.debug(str(err)) self._raise_error('Parametro SAMLRequest assente.') if not req_info: self._raise_error('Processo di parsing del messaggio fallito.') self.app.logger.debug('AuthnRequest: {}'.format(authn_req)) # Check if it is signed if "SigAlg" in saml_msg and "Signature" in saml_msg: # Signed request self.app.logger.debug('Messaggio SAML firmato.') issuer_name = authn_req.issuer.text _certs = self.server.metadata.certs(issuer_name, "any", "signing") verified_ok = False for cert in _certs: self.app.logger.debug('security backend: {}'.format( self.server.sec.sec_backend.__class__.__name__)) # Check signature if verify_redirect_signature(saml_msg, self.server.sec.sec_backend, cert): verified_ok = True break if not verified_ok: self._raise_error( 'Verifica della firma del messaggio fallita.') # Perform login key = self._store_request(req_info) relay_state = saml_msg.get('RelayState', '') session['request_key'] = key session['relay_state'] = relay_state return redirect(url_for('login')) def _get_binding(self, endpoint_type, request): try: endpoint = request.endpoint binding = endpoint.split('{}_'.format(endpoint_type))[1] return self._binding_mapping.get(binding) except IndexError: pass @property def _spid_main_fields(self): """ Returns a list of spid main attributes """ return self._spid_attributes['primary'].keys() @property def _spid_secondary_fields(self): """ Returns a list of spid secondary attributes """ return self._spid_attributes['secondary'].keys() def add_user(self): """ Add user endpoint """ spid_main_fields = self._spid_main_fields spid_secondary_fields = self._spid_secondary_fields _fields = '<br><b>{}</b><br>'.format('Primary attributes') for _field_name in spid_main_fields: _fields = '{}<span>{}</span> <input type="text" name={} /><br>'.format( _fields, _field_name, _field_name) _fields = '{}<br><b>{}</b><br>'.format(_fields, 'Secondary attributes') for _field_name in spid_secondary_fields: _fields = '{}<span>{}</span> <input type="text" name={} /><br>'.format( _fields, _field_name, _field_name) if request.method == 'GET': return FORM_ADD_USER.format('/add-user', _fields), 200 elif request.method == 'POST': username = request.form.get('username') password = request.form.get('password') sp = request.form.get('service_provider') if not username or not password or not sp: abort(400) extra = {} for spid_field in spid_main_fields: spid_value = request.form.get(spid_field) if spid_value: extra[spid_field] = spid_value for spid_field in spid_secondary_fields: spid_value = request.form.get(spid_field) if spid_value: extra[spid_field] = spid_value self.user_manager.add(username, password, sp, extra) return 'Added a new user', 200 def login(self): """ Login endpoint (verify user credentials) """ key = session['request_key'] if 'request_key' in session else None relay_state = session['relay_state'] if 'relay_state' in session else '' self.app.logger.debug('Request key: {}'.format(key)) if key and key in self.ticket: authn_request = self.ticket[key] sp_id = authn_request.message.issuer.text destination = authn_request.message.assertion_consumer_service_url spid_level = authn_request.message.requested_authn_context.authn_context_class_ref[ 0].text authn_info = self.authn_broker.pick( authn_request.message.requested_authn_context) callback, reference = authn_info[0] if request.method == 'GET': # inject extra data in form login based on spid level extra_challenge = callback(**{'key': key}) return FORM_LOGIN.format(url_for('login'), key, relay_state, extra_challenge), 200 # verify optional challenge based on spid level verified = callback(verify=True, **{ 'key': key, 'data': request.form }) if verified: # verify user credentials user_id, user = self.user_manager.get(request.form['username'], request.form['password'], sp_id) if user_id is not None: # setup response attribute_statement_on_response = self._config.get( 'attribute_statement_on_response') identity = user['attrs'] AUTHN = {"class_ref": spid_level, "authn_auth": spid_level} _data = dict(identity=identity, userid=user_id, in_response_to=authn_request.message.id, destination=destination, sp_entity_id=sp_id, authn=AUTHN, issuer=self.server.config.entityid, sign_alg=SIGN_ALG, digest_alg=DIGEST_ALG, sign_assertion=True) response = self.server.create_authn_response(**_data) http_args = self.server.apply_binding( BINDING_HTTP_POST, response, destination, response=True, sign=True, relay_state=relay_state) # Setup confirmation page data ast = Assertion(identity) policy = self.server.config.getattr("policy", "idp") ast.acs = self.server.config.getattr( "attribute_converters", "idp") res = ast.apply_policy(sp_id, policy, self.server.metadata) attrs = res.keys() attrs_list = '' for _attr in attrs: attrs_list = '{}<tr><td>{}</td></tr>'.format( attrs_list, _attr) self.responses[key] = http_args['data'] return CONFIRM_PAGE.format(attrs_list, '/continue-response', key), 200 abort(403) def continue_response(self): key = request.form['request_key'] if key and key in self.ticket and key in self.responses: return self.responses[key], 200 abort(403) def single_logout_service(self): """ SLO endpoint :param binding: 'redirect' is http-redirect, 'post' is http-post binding """ self.app.logger.debug("req: '%s'", request) saml_msg = self.unpack_args(request.args) _binding = self._get_binding('single_logout_service', request) req_info = self.server.parse_logout_request(saml_msg['SAMLRequest'], _binding) msg = req_info.message response = self.server.create_logout_response( msg, [BINDING_HTTP_POST, BINDING_HTTP_REDIRECT], sign_alg=SIGN_ALG, digest_alg=DIGEST_ALG, sign=True) binding, destination = self.server.pick_binding( "single_logout_service", [BINDING_HTTP_POST, BINDING_HTTP_REDIRECT], "spsso", req_info) http_args = self.server.apply_binding(binding, "%s" % response, destination, response=True, sign=True) return http_args['data'], 200 def metadata(self): metadata = create_metadata_string( __file__, self.server.config, ) return Response(metadata, mimetype='text/xml') @property def _wsgiconf(self): _cnf = { 'host': self._config.get('host', '0.0.0.0'), 'port': self._config.get('port', '8000'), 'debug': self._config.get('debug', True), } if self._config.get('https', False): key = self._config.get('https_key_file') cert = self._config.get('https_cert_file') if not key or not cert: raise KeyError( 'Errore modalitĂ https: Chiave e/o certificato assenti!') _cnf['ssl_context'] = ( cert, key, ) return _cnf def start(self): """ Start the server instance """ self.app.run(**self._wsgiconf)
def test_complete_flow(): client = ecp_client.Client("user", "password", metadata_file=full_path("idp_all.xml")) sp = Saml2Client(config_file=dotname("servera_conf")) idp = Server(config_file=dotname("idp_all_conf")) 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
def test_flow(): sp = Saml2Client(config_file="servera_conf") idp = Server(config_file="idp_all_conf") 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 = 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
class SamlIDP(service.Service): def __init__(self, environ, start_response, conf, cache, incomming, tid1_to_tid2, tid2_to_tid1, encmsg_to_iv, tid_handler, force_persistant_nameid, force_no_userid_subject_cacheing, idp=None): """ Constructor for the class. :param environ: WSGI environ :param start_response: WSGI start response function :param conf: The SAML configuration :param cache: Cache with active sessions """ service.Service.__init__(self, environ, start_response) self.response_bindings = None if idp is None: self.idp = Server(config=conf, cache=cache) else: self.idp = idp self.incomming = incomming self.tid1_to_tid2 = tid1_to_tid2 self.tid2_to_tid1 = tid2_to_tid1 self.encmsg_to_iv = encmsg_to_iv self.tid_handler = tid_handler self.force_persistant_nameid = force_persistant_nameid self.force_no_userid_subject_cacheing = force_no_userid_subject_cacheing def verify_request(self, query, binding): """ Parses and verifies the SAML Authentication Request :param query: The SAML authn request, transport encoded :param binding: Which binding the query came in over :returns: dictionary """ if not query: logger.info("Missing QUERY") resp = Unauthorized('Unknown user') return {"response": resp} req_info = self.idp.parse_authn_request(query, binding) encrypt_cert = encrypt_cert_from_item(req_info.message) logger.info("parsed OK") _authn_req = req_info.message logger.debug("%s" % _authn_req) # Check that I know where to send the reply to try: binding_out, destination = self.idp.pick_binding( "assertion_consumer_service", bindings=self.response_bindings, entity_id=_authn_req.issuer.text, request=_authn_req) except Exception as err: logger.error("Couldn't find receiver endpoint: %s" % err) raise logger.debug("Binding: %s, destination: %s" % (binding_out, destination)) resp_args = {} try: resp_args = self.idp.response_args(_authn_req) _resp = None except UnknownPrincipal as excp: _resp = self.idp.create_error_response(_authn_req.id, destination, excp) except UnsupportedBinding as excp: _resp = self.idp.create_error_response(_authn_req.id, destination, excp) req_args = {} for key in ["subject", "name_id_policy", "conditions", "requested_authn_context", "scoping", "force_authn", "is_passive"]: try: val = getattr(_authn_req, key) except AttributeError: pass else: if val is not None: req_args[key] = val return {"resp_args": resp_args, "response": _resp, "authn_req": _authn_req, "req_args": req_args, "encrypt_cert": encrypt_cert} def handle_authn_request(self, binding_in): """ Deal with an authentication request :param binding_in: Which binding was used when receiving the query :return: A response if an error occurred or session information in a dictionary """ _request = self.unpack(binding_in) _binding_in = service.INV_BINDING_MAP[binding_in] try: _dict = self.verify_request(_request["SAMLRequest"], _binding_in) except UnknownPrincipal as excp: logger.error("UnknownPrincipal: %s" % (excp,)) resp = ServiceError("UnknownPrincipal: %s" % (excp,)) return resp except UnsupportedBinding as excp: logger.error("UnsupportedBinding: %s" % (excp,)) resp = ServiceError("UnsupportedBinding: %s" % (excp,)) return resp _binding = _dict["resp_args"]["binding"] if _dict["response"]: # An error response http_args = self.idp.apply_binding( _binding, "%s" % _dict["response"], _dict["resp_args"]["destination"], _request["RelayState"], response=True) logger.debug("HTTPargs: %s" % http_args) return self.response(_binding, http_args) else: return self.incomming(_dict, self, self.environ, self.start_response, _request["RelayState"]) def get_tid1_resp(self, org_resp): tid1 = org_resp.assertion.subject.name_id.text return tid1 def get_sp_entityid(self, resp_args): sp_entityid = resp_args["destination"] return sp_entityid def get_tid2_enc(self, tid1, sp_entityid): iv = None if self.encmsg_to_iv is not None: iv = self.tid_handler.get_new_iv() tid2_enc = self.tid_handler.tid2_encrypt(tid1, sp_entityid, iv=iv) if self.encmsg_to_iv is not None: self.encmsg_to_iv[tid2_enc] = iv return tid2_enc def get_tid2_hash(self, tid1, sp_entityid): tid2_hash = self.tid_handler.tid2_hash(tid1, sp_entityid) return tid2_hash def handle_tid(self, tid1, tid2): if self.tid1_to_tid2 is not None: self.tid1_to_tid2[tid1] = tid2 if self.tid2_to_tid1 is not None: self.tid2_to_tid1[tid2] = tid1 def name_id_exists(self, userid, name_id_policy, sp_entity_id): try: snq = name_id_policy.sp_name_qualifier except AttributeError: snq = sp_entity_id if not snq: snq = sp_entity_id kwa = {"sp_name_qualifier": snq} try: kwa["format"] = name_id_policy.format except AttributeError: pass return self.idp.ident.find_nameid(userid, **kwa) def construct_authn_response(self, identity, userid, authn, resp_args, relay_state, name_id=None, sign_response=True, org_resp=None, org_xml_response=None): """ :param identity: :param name_id: :param authn: :param resp_args: :param relay_state: :param sign_response: :return: """ if self.force_persistant_nameid: if "name_id_policy" in resp_args: resp_args["name_id_policy"].format = NAMEID_FORMAT_PERSISTENT sp_entityid = self.get_sp_entityid(resp_args) tid1 = self.get_tid1_resp(org_resp) userid = self.tid_handler.uid_hash(tid1) if self.force_no_userid_subject_cacheing: self.idp.ident = IdentDB({}) name_id_exist = False if self.name_id_exists(userid, resp_args["name_id_policy"], resp_args["sp_entity_id"]): name_id_exist = True if not name_id_exist: if identity is not None: identity["uid"] = userid if self.tid2_to_tid1 is None: tid2 = self.get_tid2_enc(tid1, sp_entityid) else: tid2 = self.get_tid2_hash(tid1, sp_entityid) else: tid2 = None _resp = self.idp.create_authn_response(identity, userid=userid, name_id=name_id, authn=authn, sign_response=False, **resp_args) if not name_id_exist: #Fix for name_id so sid2 is used instead. _resp.assertion.subject.name_id.text = tid2 self.idp.ident.remove_local(userid) self.idp.ident.store(userid, _resp.assertion.subject.name_id) tid2 = _resp.assertion.subject.name_id.text if self.tid2_to_tid1 is not None: self.tid2_to_tid1[tid2] = tid1 if self.tid1_to_tid2 is not None: self.tid1_to_tid2[tid1] = tid2 advice = None for tmp_assertion in org_resp.response.assertion: if tmp_assertion.advice is not None: advice = tmp_assertion.advice break if advice is not None: _resp.assertion.advice = advice #_resp.assertion = [] if sign_response: _class_sign = class_name(_resp) _resp.signature = pre_signature_part(_resp.id, self.idp.sec.my_cert, 1) _resp = self.idp.sec.sign_statement(_resp, _class_sign, node_id=_resp.id) http_args = self.idp.apply_binding( resp_args["binding"], "%s" % _resp, resp_args["destination"], relay_state, response=True) logger.debug("HTTPargs: %s" % http_args) resp = None if http_args["data"]: resp = Response(http_args["data"], headers=http_args["headers"]) else: for header in http_args["headers"]: if header[0] == "Location": resp = Redirect(header[1]) if not resp: resp = ServiceError("Don't know how to return response") return resp def register_endpoints(self): """ Given the configuration, return a set of URL to function mappings. """ url_map = [] for endp, binding in self.idp.config.getattr("endpoints", "idp")[ "single_sign_on_service"]: p = urlparse(endp) url_map.append(("^%s/(.*)$" % p.path[1:], ("IDP", "handle_authn_request", service.BINDING_MAP[binding]))) url_map.append(("^%s$" % p.path[1:], ("IDP", "handle_authn_request", service.BINDING_MAP[binding]))) return url_map
def test_flow(): sp = Saml2Client(config_file="servera_conf") idp = Server(config_file="idp_all_conf") 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 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)
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 login_process(request): """ View which processes the actual SAML request and returns a self-submitting form with the SAML response. The login_required decorator ensures the user authenticates first on the IdP using 'normal' ways. """ # Construct server with config from settings dict conf = IdPConfig() conf.load(copy.deepcopy(settings.SAML_IDP_CONFIG)) IDP = Server(config=conf) # Parse incoming request try: req_info = IDP.parse_authn_request(request.session['SAMLRequest'], BINDING_HTTP_POST) except Exception as excp: return HttpResponseBadRequest(excp) # TODO this is taken from example, but no idea how this works or whats it does. Check SAML2 specification? # Signed request for HTTP-REDIRECT if "SigAlg" in request.session and "Signature" in request.session: _certs = IDP.metadata.certs(req_info.message.issuer.text, "any", "signing") verified_ok = False for cert in _certs: # TODO implement #if verify_redirect_signature(_info, IDP.sec.sec_backend, cert): # verified_ok = True # break pass if not verified_ok: return HttpResponseBadRequest( "Message signature verification failure") binding_out, destination = IDP.pick_binding( service="assertion_consumer_service", entity_id=req_info.message.issuer.text) # Gather response arguments try: resp_args = IDP.response_args(req_info.message) except (UnknownPrincipal, UnsupportedBinding) as excp: return HttpResponseServerError(excp) try: sp_config = settings.SAML_IDP_SPCONFIG[resp_args['sp_entity_id']] except Exception: raise ImproperlyConfigured( "No config for SP %s defined in SAML_IDP_SPCONFIG" % resp_args['sp_entity_id']) # Create user-specified processor or fallback to all-access base processor processor_string = sp_config.get('processor', None) if processor_string is None: processor = BaseProcessor else: processor_class = import_string(processor_string) processor = processor_class() # Check if user has access to the service of this SP if not processor.has_access(request.user): raise PermissionDenied("You do not have access to this resource") # Create Identity dict (SP-specific) sp_mapping = sp_config.get('attribute_mapping', {'username': '******'}) identity = processor.create_identity(request.user, sp_mapping) # TODO investigate how this works, because I don't get it. Specification? req_authn_context = req_info.message.requested_authn_context or PASSWORD AUTHN_BROKER = AuthnBroker() AUTHN_BROKER.add(authn_context_class_ref(req_authn_context), "") # Construct SamlResponse message try: authn_resp = IDP.create_authn_response( identity=identity, userid=request.user.username, name_id=NameID(format=resp_args['name_id_policy'].format, sp_name_qualifier=destination, text=request.user.username), authn=AUTHN_BROKER.get_authn_by_accr(req_authn_context), sign_response=IDP.config.getattr("sign_response", "idp") or False, sign_assertion=IDP.config.getattr("sign_assertion", "idp") or False, **resp_args) except Exception as excp: return HttpResponseServerError(excp) # Return as html with self-submitting form. http_args = IDP.apply_binding(binding=binding_out, msg_str="%s" % authn_resp, destination=destination, relay_state=request.session['RelayState'], response=True) logger.debug('http args are: %s' % http_args) if processor.enable_multifactor(request.user): # Store http_args in session for after multi factor is complete request.session['saml_data'] = http_args['data'] logger.debug("Redirecting to process_multi_factor") return HttpResponseRedirect(reverse('saml_multi_factor')) else: logger.debug("Performing SAML redirect") return HttpResponse(http_args['data'])
class SamlIDP(service.Service): def __init__(self, environ, start_response, conf, cache, incomming): """ Constructor for the class. :param environ: WSGI environ :param start_response: WSGI start response function :param conf: The SAML configuration :param cache: Cache with active sessions """ service.Service.__init__(self, environ, start_response) self.response_bindings = None self.idp = Server(config=conf, cache=cache) self.incomming = incomming def verify_request(self, query, binding): """ Parses and verifies the SAML Authentication Request :param query: The SAML authn request, transport encoded :param binding: Which binding the query came in over :returns: dictionary """ if not query: logger.info("Missing QUERY") resp = Unauthorized('Unknown user') return {"response": resp(self.environ, self.start_response)} req_info = self.idp.parse_authn_request(query, binding) logger.info("parsed OK") _authn_req = req_info.message logger.debug("%s" % _authn_req) # Check that I know where to send the reply to try: binding_out, destination = self.idp.pick_binding( "assertion_consumer_service", bindings=self.response_bindings, entity_id=_authn_req.issuer.text, request=_authn_req) except Exception as err: logger.error("Couldn't find receiver endpoint: %s" % err) raise logger.debug("Binding: %s, destination: %s" % (binding_out, destination)) resp_args = {} try: resp_args = self.idp.response_args(_authn_req) _resp = None except UnknownPrincipal as excp: _resp = self.idp.create_error_response(_authn_req.id, destination, excp) except UnsupportedBinding as excp: _resp = self.idp.create_error_response(_authn_req.id, destination, excp) req_args = {} for key in [ "subject", "name_id_policy", "conditions", "requested_authn_context", "scoping", "force_authn", "is_passive" ]: try: val = getattr(_authn_req, key) except AttributeError: pass else: req_args[key] = val return { "resp_args": resp_args, "response": _resp, "authn_req": _authn_req, "req_args": req_args } def handle_authn_request(self, binding_in): """ Deal with an authentication request :param binding_in: Which binding was used when receiving the query :return: A response if an error occurred or session information in a dictionary """ _request = self.unpack(binding_in) _binding_in = service.INV_BINDING_MAP[binding_in] try: _dict = self.verify_request(_request["SAMLRequest"], _binding_in) except UnknownPrincipal as excp: logger.error("UnknownPrincipal: %s" % (excp, )) resp = ServiceError("UnknownPrincipal: %s" % (excp, )) return resp(self.environ, self.start_response) except UnsupportedBinding as excp: logger.error("UnsupportedBinding: %s" % (excp, )) resp = ServiceError("UnsupportedBinding: %s" % (excp, )) return resp(self.environ, self.start_response) _binding = _dict["resp_args"]["binding"] if _dict["response"]: # An error response http_args = self.idp.apply_binding( _binding, "%s" % _dict["response"], _dict["resp_args"]["destination"], _request["RelayState"], response=True) logger.debug("HTTPargs: %s" % http_args) return self.response(_binding, http_args) else: return self.incomming(_dict, self, self.environ, self.start_response, _request["RelayState"]) def construct_authn_response(self, identity, name_id, authn, resp_args, relay_state, sign_response=True): """ :param identity: :param name_id: :param authn: :param resp_args: :param relay_state: :param sign_response: :return: """ _resp = self.idp.create_authn_response(identity, name_id=name_id, authn=authn, sign_response=sign_response, **resp_args) http_args = self.idp.apply_binding(resp_args["binding"], "%s" % _resp, resp_args["destination"], relay_state, response=True) logger.debug("HTTPargs: %s" % http_args) resp = None if http_args["data"]: resp = Response(http_args["data"], headers=http_args["headers"]) else: for header in http_args["headers"]: if header[0] == "Location": resp = Redirect(header[1]) if not resp: resp = ServiceError("Don't know how to return response") return resp(self.environ, self.start_response) def register_endpoints(self): """ Given the configuration, return a set of URL to function mappings. """ url_map = [] for endp, binding in self.idp.config.getattr( "endpoints", "idp")["single_sign_on_service"]: p = urlparse(endp) url_map.append( ("^%s/(.*)$" % p.path[1:], ("IDP", "handle_authn_request", service.BINDING_MAP[binding]))) url_map.append( ("^%s$" % p.path[1:], ("IDP", "handle_authn_request", service.BINDING_MAP[binding]))) return url_map