def authn_response(self, context, binding): """ Endpoint for the idp response :type context: satosa.context,Context :type binding: str :rtype: satosa.response.Response :param context: The current context :param binding: The saml binding type :return: response """ if not context.request["SAMLResponse"]: msg = "Missing Response for state" logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) logger.debug(logline) raise SATOSAAuthenticationError(context.state, "Missing Response") try: authn_response = self.sp.parse_authn_request_response( context.request["SAMLResponse"], binding, outstanding=self.outstanding_queries) except Exception as err: msg = "Failed to parse authn request for state" logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) logger.debug(logline, exc_info=True) raise SATOSAAuthenticationError( context.state, "Failed to parse authn request") from err if self.sp.config.getattr('allow_unsolicited', 'sp') is False: req_id = authn_response.in_response_to if req_id not in self.outstanding_queries: msg = "No request with id: {}".format(req_id), logline = lu.LOG_FMT.format(id=lu.get_session_id( context.state), message=msg) logger.debug(logline) raise SATOSAAuthenticationError(context.state, msg) del self.outstanding_queries[req_id] # check if the relay_state matches the cookie state if context.state[ self.name]["relay_state"] != context.request["RelayState"]: msg = "State did not match relay state for state" logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) logger.debug(logline) raise SATOSAAuthenticationError(context.state, "State did not match relay state") context.decorate(Context.KEY_METADATA_STORE, self.sp.metadata) if self.config.get(SAMLBackend.KEY_MEMORIZE_IDP): issuer = authn_response.response.issuer.text.strip() context.state[Context.KEY_MEMORIZED_IDP] = issuer context.state.pop(self.name, None) context.state.pop(Context.KEY_FORCE_AUTHN, None) return self.auth_callback_func( context, self._translate_response(authn_response, context.state))
def authn_request(self, context, entity_id): """ Do an authorization request on idp with given entity id. This is the start of the authorization. :type context: satosa.context.Context :type entity_id: str :rtype: satosa.response.Response :param context: The current context :param entity_id: Target IDP entity id :return: response to the user agent """ # If IDP blacklisting is enabled and the selected IDP is blacklisted, # stop here if self.idp_blacklist_file: with open(self.idp_blacklist_file) as blacklist_file: blacklist_array = json.load(blacklist_file)['blacklist'] if entity_id in blacklist_array: satosa_logging(logger, logging.DEBUG, "IdP with EntityID {} is blacklisted".format(entity_id), context.state, exc_info=False) raise SATOSAAuthenticationError(context.state, "Selected IdP is blacklisted for this backend") kwargs = {} authn_context = self.construct_requested_authn_context(entity_id) if authn_context: kwargs['requested_authn_context'] = authn_context try: binding, destination = self.sp.pick_binding( "single_sign_on_service", None, "idpsso", entity_id=entity_id) satosa_logging(logger, logging.DEBUG, "binding: %s, destination: %s" % (binding, destination), context.state) acs_endp, response_binding = self.sp.config.getattr("endpoints", "sp")["assertion_consumer_service"][0] req_id, req = self.sp.create_authn_request( destination, binding=response_binding, **kwargs) relay_state = util.rndstr() ht_args = self.sp.apply_binding(binding, "%s" % req, destination, relay_state=relay_state) satosa_logging(logger, logging.DEBUG, "ht_args: %s" % ht_args, context.state) except Exception as exc: satosa_logging(logger, logging.DEBUG, "Failed to construct the AuthnRequest for state", context.state, exc_info=True) raise SATOSAAuthenticationError(context.state, "Failed to construct the AuthnRequest") from exc if self.sp.config.getattr('allow_unsolicited', 'sp') is False: if req_id in self.outstanding_queries: errmsg = "Request with duplicate id {}".format(req_id) satosa_logging(logger, logging.DEBUG, errmsg, context.state) raise SATOSAAuthenticationError(context.state, errmsg) self.outstanding_queries[req_id] = req context.state[self.name] = {"relay_state": relay_state} return make_saml_response(binding, ht_args)
def authn_response(self, context, binding): """ Endpoint for the idp response :type context: satosa.context,Context :type binding: str :rtype: satosa.response.Response :param context: The current context :param binding: The saml binding type :return: response """ if not context.request["SAMLResponse"]: satosa_logging(logger, logging.DEBUG, "Missing Response for state", context.state) raise SATOSAAuthenticationError(context.state, "Missing Response") try: authn_response = self.sp.parse_authn_request_response( context.request["SAMLResponse"], binding, outstanding=self.outstanding_queries) except Exception as err: satosa_logging(logger, logging.DEBUG, "Failed to parse authn request for state", context.state, exc_info=True) raise SATOSAAuthenticationError( context.state, "Failed to parse authn request") from err if self.sp.config.getattr('allow_unsolicited', 'sp') is False: req_id = authn_response.in_response_to if req_id not in self.outstanding_queries: errmsg = "No request with id: {}".format(req_id), satosa_logging(logger, logging.DEBUG, errmsg, context.state) raise SATOSAAuthenticationError(context.state, errmsg) del self.outstanding_queries[req_id] # check if the relay_state matches the cookie state if context.state[ self.name]["relay_state"] != context.request["RelayState"]: satosa_logging(logger, logging.DEBUG, "State did not match relay state for state", context.state) raise SATOSAAuthenticationError(context.state, "State did not match relay state") context.decorate(Context.KEY_BACKEND_METADATA_STORE, self.sp.metadata) del context.state[self.name] return self.auth_callback_func( context, self._translate_response(authn_response, context.state))
def _translate_response(self, auth_response, state): if 'eduPersonAffiliation' not in auth_response.ava: raise SATOSAAuthenticationError( state, 'Missing eduPersonAffiliation in response from IdP.') internal_resp = super()._translate_response(auth_response, state) internal_resp.user_id = self._get_user_id(auth_response) if not internal_resp.user_id: raise SATOSAAuthenticationError( state, 'Failed to construct persistent user id from IdP response.') return internal_resp
def verify_state(self, resp, state_data, state): """ Will verify the state and throw and error if the state is invalid. :type resp: AuthorizationResponse :type state_data: dict[str, str] :type state: satosa.state.State :param resp: The authorization response from the OP, created by pyoidc. :param state_data: The state data for this backend. :param state: The current state for the proxy and this backend. Only used for raising errors. """ if not ("state" in resp and "state" in state_data and resp["state"] == state_data["state"]): tmp_state = "" if "state" in resp: tmp_state = resp["state"] satosa_logging(LOGGER, logging.DEBUG, "Missing or invalid state [%s] in response!" % tmp_state, state, exc_info=True) raise SATOSAAuthenticationError( state, "Missing or invalid state [%s] in response!" % tmp_state)
def _translate_response(self, auth_response, state): # translate() will handle potentially encrypted SAML Assertions # auth_response object will also be modified # import pdb; pdb.set_trace() internal_resp = super()._translate_response(auth_response, state) if not any( affiliation_attr in auth_response.ava for affiliation_attr in self.config['affiliation_attributes']): raise SATOSAProcessingHaltError( state=state, message="No affiliation attribute from IdP", redirect_uri=self.error_uri) params = parse_qs(state['InAcademia']['oidc_request']) if 'persistent' in params['scope'][0].split(" "): scope = 'persistent' else: scope = 'transient' internal_resp.user_id = self._get_user_id(auth_response, scope) if not internal_resp.user_id: raise SATOSAAuthenticationError( state, 'Failed to construct persistent user id from IdP response.') return internal_resp
def disco_response(self, context): """ Endpoint for the discovery server response :type context: satosa.context.Context :rtype: satosa.response.Response :param context: The current context :return: response """ info = context.request state = context.state requested_attributes = state.pop(Context.KEY_REQUESTED_ATTRIBUTES, None) try: entity_id = info["entityID"] except KeyError as err: msg = "No IDP chosen for state" logline = lu.LOG_FMT.format(id=lu.get_session_id(state), message=msg) logger.debug(logline, exc_info=True) raise SATOSAAuthenticationError(state, "No IDP chosen") from err return self.authn_request(context, entity_id, requested_attributes=requested_attributes)
def disco_response(self, context): """ Endpoint for the discovery server response :type context: satosa.context.Context :rtype: satosa.response.Response :param context: The current context :return: response """ info = context.request state = context.state try: entity_id = info[self.idp_disco_query_param] except KeyError as err: satosa_logging(LOGGER, logging.DEBUG, "No IDP chosen for state", state, exc_info=True) raise SATOSAAuthenticationError(state, "No IDP chosen") from err else: request_info = InternalRequest(None, None) return self.authn_request(context, entity_id, request_info)
def redirect_endpoint(self, context, *args): """ Handles the authentication response from the OP. :type context: satosa.context.Context :type args: Any :rtype: satosa.response.Response :param context: SATOSA context :param args: None :return: """ state = context.state backend_state = state.get(self.config.STATE_ID) if backend_state["state"] != context.request["state"]: satosa_logging( LOGGER, logging.DEBUG, "Missing or invalid state in authn response for state: %s" % backend_state, state) raise SATOSAAuthenticationError( state, "Missing or invalid state in authn response") client = self.restore_state(backend_state) result = client.callback(context.request, state, backend_state) context.state.remove(self.config.STATE_ID) return self.auth_callback_func( context, self._translate_response( result, client.authorization_endpoint, self.get_subject_type(client), ))
def _get_uuid(self, context, issuer, id): """ Ask the account linking service for a uuid. If the given issuer/id pair is not linked, then the function will return a ticket. This ticket should be used for linking the issuer/id pair to the user account :type context: satosa.context.Context :type issuer: str :type id: str :rtype: (int, str) :param context: The current context :param issuer: the issuer used for authentication :param id: the given id :return: response status code and message (200, uuid) or (400, ticket) """ data = { "idp": issuer, "id": id, "redirect_endpoint": "%s/account_linking/%s" % (self.proxy_base, self.endpoint) } jws = self._to_jws(data) try: request = "{}/get_id?jwt={}".format(self.al_rest_uri, jws) response = requests.get(request, verify=self.verify_ssl) except requests.ConnectionError as con_exc: msg = "Could not connect to account linking service" satosa_logging(LOGGER, logging.CRITICAL, msg, context.state, exc_info=True) raise SATOSAAuthenticationError(context.state, msg) from con_exc if response.status_code != 200 and response.status_code != 404: msg = "Got status code '%s' from account linking service" % ( response.status_code) satosa_logging(LOGGER, logging.CRITICAL, msg, context.state) raise SATOSAAuthenticationError(context.state, msg) return response.status_code, response.text
def check_blacklist(self): # If IDP blacklisting is enabled and the selected IDP is blacklisted, # stop here if self.idp_blacklist_file: with open(self.idp_blacklist_file) as blacklist_file: blacklist_array = json.load(blacklist_file)['blacklist'] if entity_id in blacklist_array: logger.debug("IdP with EntityID {} is blacklisted".format(entity_id)) raise SATOSAAuthenticationError(context.state, "Selected IdP is blacklisted for this backend")
def deny_consent(self, context): """ Endpoint for handling denied consent. :type context: satosa.context.Context :rtype: satosa.response.Response :param context: response context :return: response """ del context.state[STATE_KEY] transaction_log(context.state, self.config.get("consent_exit_order", 1010), "user_consent", "deny", "exit", "cancel", '', '', ErrorDescription.USER_CONSENT_DENIED[LOG_MSG], 'user') auth_error = SATOSAAuthenticationError(context.state, '') auth_error._message = ErrorDescription.USER_CONSENT_DENIED[ERROR_DESC] raise auth_error
def authn_response(self, context): """ Handles the authentication response from the OP. :type context: satosa.context.Context :rtype: satosa.response.Response :param context: The context in SATOSA :return: A SATOSA response. This method is only responsible to call the callback function which generates the Response object. """ state = context.state try: state_data = state.get(self.config["state_id"]) consumer = self.get_consumer() request = context.request aresp = consumer.parse_response(AuthorizationResponse, info=json.dumps(request)) self.verify_state(aresp, state_data, state) rargs = { "code": aresp["code"], "redirect_uri": self.redirect_url, "state": state_data["state"] } atresp = consumer.do_access_token_request(request_args=rargs, state=aresp["state"]) if ("verify_accesstoken_state" not in self.config or self.config["verify_accesstoken_state"]): self.verify_state(atresp, state_data, state) user_info = self.user_information(atresp["access_token"]) internal_response = InternalResponse( auth_info=self.auth_info(request)) internal_response.add_attributes( self.converter.to_internal(self.external_type, user_info)) internal_response.set_user_id(user_info[self.user_id_attr]) if "user_id_params" in self.config: user_id = "" for param in self.config["user_id_params"]: try: user_id += user_info[param] except Exception as error: raise SATOSAAuthenticationError from error internal_response.set_user_id(user_id) context.state.remove(self.config["state_id"]) return self.auth_callback_func(context, internal_response) except Exception as error: satosa_logging(LOGGER, logging.DEBUG, "Not a valid authentication", state, exc_info=True) if isinstance(error, SATOSAError): raise error if state is not None: raise SATOSAAuthenticationError( state, "Not a valid authentication") from error raise
def authn_response(self, context, binding): """ Endpoint for the idp response :type context: satosa.context,Context :type binding: str :rtype: satosa.response.Response :param context: The current context :param binding: The saml binding type :return: response """ _authn_response = context.request state = context.state if not _authn_response["SAMLResponse"]: satosa_logging(LOGGER, logging.DEBUG, "Missing Response for state", state) raise SATOSAAuthenticationError(state, "Missing Response") try: _response = self.sp.parse_authn_request_response( _authn_response["SAMLResponse"], binding) except Exception as err: satosa_logging(LOGGER, logging.DEBUG, "Failed to parse authn request for state", state, exc_info=True) raise SATOSAAuthenticationError( state, "Failed to parse authn request") from err # check if the relay_state matches the cookie state if state.get(self.state_id) != _authn_response['RelayState']: satosa_logging(LOGGER, logging.DEBUG, "State did not match relay state for state", state) raise SATOSAAuthenticationError(state, "State did not match relay state") context.state.remove(self.state_id) return self.auth_callback_func( context, self._translate_response(_response, context.state))
def deny_consent(self, context): """ Endpoint for handling denied consent. :type context: satosa.context.Context :rtype: satosa.response.Response :param context: response context :return: response """ del context.state[self.name] raise SATOSAAuthenticationError(context.state, 'Consent was denied by the user.')
def test_handle_backend_error(self, context, frontend): redirect_uri = "https://client.example.com" areq = AuthorizationRequest(client_id=CLIENT_ID, scope="openid", response_type="id_token", redirect_uri=redirect_uri) context.state[frontend.name] = {"oidc_request": areq.to_urlencoded()} # fake an error message = "test error" error = SATOSAAuthenticationError(context.state, message) resp = frontend.handle_backend_error(error) assert resp.message.startswith(redirect_uri) error_response = AuthorizationErrorResponse().deserialize(urlparse(resp.message).fragment) error_response["error"] = "access_denied" error_response["error_description"] == message
def disco_response(self, context): """ Endpoint for the discovery server response :type context: satosa.context.Context :rtype: satosa.response.Response :param context: The current context :return: response """ info = context.request state = context.state try: entity_id = info["entityID"] except KeyError as err: satosa_logging(logger, logging.DEBUG, "No IDP chosen for state", state, exc_info=True) raise SATOSAAuthenticationError(state, "No IDP chosen") from err return self.authn_request(context, entity_id)
def _translate_response(self, auth_response, state): # translate() will handle potentially encrypted SAML Assertions # auth_response object will also be modified # import pdb; pdb.set_trace() internal_resp = super()._translate_response(auth_response, state) satosa_logging( logger, logging.INFO, "Attributes received from IdP {} {}".format( auth_response.response.issuer.text, json.dumps([k for k in auth_response.ava.keys()])), state) resp_idp_entityid = internal_resp.to_dict().get('auth_info').get( 'issuer') if not any( affiliation_attr in auth_response.ava for affiliation_attr in self.config['affiliation_attributes']): transaction_log(state, self.config.get("response_exit_order", 610), "inacademia_backend", "response", "exit", "fail", '', resp_idp_entityid, ErrorDescription.NO_AFFILIATION_ATTR[LOG_MSG], 'idp') auth_error = SATOSAAuthenticationError(state, "") auth_error._message = ErrorDescription.NO_AFFILIATION_ATTR[ ERROR_DESC] raise auth_error params = parse_qs(state['InAcademia']['oidc_request']) if 'persistent' in params['scope'][0].split(" "): scope = 'persistent' else: scope = 'transient' internal_resp.user_id = self._get_user_id(auth_response, scope) if not internal_resp.user_id: transaction_log( state, self.config.get("response_exit_order", 620), "inacademia_backend", "response", "exit", "fail", '', resp_idp_entityid, ErrorDescription. FAILED_TO_CONSTRUCT_PERSISTENT_USERID[LOG_MSG], 'idp') auth_error = SATOSAAuthenticationError(state, "") auth_error._message = ErrorDescription.FAILED_TO_CONSTRUCT_PERSISTENT_USERID[ ERROR_DESC] raise auth_error return internal_resp
def _verify_state(self, resp, state_data, state): """ Will verify the state and throw and error if the state is invalid. :type resp: AuthorizationResponse :type state_data: dict[str, str] :type state: satosa.state.State :param resp: The authorization response from the AS, created by pyoidc. :param state_data: The state data for this backend. :param state: The current state for the proxy and this backend. Only used for raising errors. """ is_known_state = "state" in resp and "state" in state_data and resp[ "state"] == state_data["state"] if not is_known_state: received_state = resp.get("state", "") satosa_logging( logger, logging.DEBUG, "Missing or invalid state [%s] in response!" % received_state, state) raise SATOSAAuthenticationError( state, "Missing or invalid state [%s] in response!" % received_state)
def process_service_queue(self, context, data): """ Processes the whole queue of micro services :type context: satosa.context.Context :type data: satosa.internal_data.InternalResponse | satosa.internal_data.InternalRequest :rtype: satosa.internal_data.InternalResponse | satosa.internal_data.InternalRequest :param context: The current context :param data: Data to be modified :return: Modified data """ if self._child_service: data = self._child_service.process_service_queue(context, data) try: return self.process(context, data) except Exception as err: satosa_logging(LOGGER, logging.DEBUG, "Micro service error.", context.state, exc_info=True) raise SATOSAAuthenticationError(context.state, "Micro service error") from err
def _verify_state(self, resp, state_data, state): """ Will verify the state and throw and error if the state is invalid. :type resp: AuthorizationResponse :type state_data: dict[str, str] :type state: satosa.state.State :param resp: The authorization response from the AS, created by pyoidc. :param state_data: The state data for this backend. :param state: The current state for the proxy and this backend. Only used for raising errors. """ is_known_state = "state" in resp and "state" in state_data and resp[ "state"] == state_data["state"] if not is_known_state: received_state = resp.get("state", "") msg = "Missing or invalid state [{}] in response!".format( received_state) logline = lu.LOG_FMT.format(id=lu.get_session_id(state), message=msg) logger.debug(logline) raise SATOSAAuthenticationError( state, "Missing or invalid state [%s] in response!" % received_state)
def authn_request(self, context, entity_id, internal_req): """ Do an authorization request on idp with given entity id. This is the start of the authorization. :type context: satosa.context.Context :type entity_id: str :type internal_req: satosa.internal_data.InternalRequest :rtype: satosa.response.Response :param context: The curretn context :param entity_id: Target IDP entity id :param internal_req: The request :return: Response """ _cli = self.sp hash_type = UserIdHashType.persistent.name if "hash_type" in self.config: hash_type = self.config["hash_type"] req_args = {"name_id_policy": self.create_name_id_policy(hash_type)} state = context.state try: # Picks a binding to use for sending the Request to the IDP _binding, destination = _cli.pick_binding("single_sign_on_service", self.bindings, "idpsso", entity_id=entity_id) satosa_logging( LOGGER, logging.DEBUG, "binding: %s, destination: %s" % (_binding, destination), state) # Binding here is the response binding that is which binding the # IDP should use to return the response. acs = _cli.config.getattr("endpoints", "sp")["assertion_consumer_service"] # just pick one endp, return_binding = acs[0] req_id, req = _cli.create_authn_request(destination, binding=return_binding, **req_args) relay_state = rndstr() ht_args = _cli.apply_binding(_binding, "%s" % req, destination, relay_state=relay_state) satosa_logging(LOGGER, logging.DEBUG, "ht_args: %s" % ht_args, state) except Exception as exc: satosa_logging(LOGGER, logging.DEBUG, "Failed to construct the AuthnRequest for state", state, exc_info=True) raise SATOSAAuthenticationError( state, "Failed to construct the AuthnRequest") from exc state.add(self.state_id, relay_state) if _binding == BINDING_HTTP_REDIRECT: for param, value in ht_args["headers"]: if param == "Location": resp = SeeOther(str(value)) break else: satosa_logging(LOGGER, logging.DEBUG, "Parameter error for state", state) raise SATOSAAuthenticationError(state, "Parameter error") else: resp = Response(ht_args["data"], headers=ht_args["headers"]) return resp
def authn_request(self, context, entity_id): """ Do an authorization request on idp with given entity id. This is the start of the authorization. :type context: satosa.context.Context :type entity_id: str :rtype: satosa.response.Response :param context: The current context :param entity_id: Target IDP entity id :return: response to the user agent """ # If IDP blacklisting is enabled and the selected IDP is blacklisted, # stop here if self.idp_blacklist_file: with open(self.idp_blacklist_file) as blacklist_file: blacklist_array = json.load(blacklist_file)['blacklist'] if entity_id in blacklist_array: msg = "IdP with EntityID {} is blacklisted".format(entity_id) logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) logger.debug(logline, exc_info=False) raise SATOSAAuthenticationError(context.state, "Selected IdP is blacklisted for this backend") kwargs = {} target_accr = context.state.get(Context.KEY_TARGET_AUTHN_CONTEXT_CLASS_REF) authn_context = self.construct_requested_authn_context(entity_id, target_accr=target_accr) if authn_context: kwargs["requested_authn_context"] = authn_context if self.config.get(SAMLBackend.KEY_MIRROR_FORCE_AUTHN): kwargs["force_authn"] = get_force_authn( context, self.config, self.sp.config ) if self.config.get(SAMLBackend.KEY_SEND_REQUESTER_ID): requester = context.state.state_dict[STATE_KEY_BASE]['requester'] kwargs["scoping"] = Scoping(requester_id=[RequesterID(text=requester)]) try: acs_endp, response_binding = self.sp.config.getattr("endpoints", "sp")["assertion_consumer_service"][0] relay_state = util.rndstr() req_id, binding, http_info = self.sp.prepare_for_negotiated_authenticate( entityid=entity_id, response_binding=response_binding, relay_state=relay_state, **kwargs, ) except Exception as e: msg = "Failed to construct the AuthnRequest for state" logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) logger.debug(logline, exc_info=True) raise SATOSAAuthenticationError(context.state, "Failed to construct the AuthnRequest") from e if self.sp.config.getattr('allow_unsolicited', 'sp') is False: if req_id in self.outstanding_queries: msg = "Request with duplicate id {}".format(req_id) logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) logger.debug(logline) raise SATOSAAuthenticationError(context.state, msg) self.outstanding_queries[req_id] = req_id context.state[self.name] = {"relay_state": relay_state} return make_saml_response(binding, http_info)
def authn_response(self, context, binding): """ Endpoint for the idp response :type context: satosa.context,Context :type binding: str :rtype: satosa.response.Response :param context: The current context :param binding: The saml binding type :return: response """ if not context.request["SAMLResponse"]: logger.debug("Missing Response for state") raise SATOSAAuthenticationError(context.state, "Missing Response") try: authn_response = self.sp.parse_authn_request_response( context.request["SAMLResponse"], binding, outstanding=self.outstanding_queries, ) except StatusAuthnFailed as err: erdict = re.search(r"ErrorCode nr(?P<err_code>\d+)", str(err)) if erdict: return self.handle_spid_anomaly(erdict.groupdict()["err_code"], err) else: return self.handle_error( **{ "err": err, "message": "Autenticazione fallita", "troubleshoot": ("Anomalia riscontrata durante la fase di Autenticazione. " f"{_TROUBLESHOOT_MSG}"), }) except SignatureError as err: return self.handle_error( **{ "err": err, "message": "Autenticazione fallita", "troubleshoot": ( "La firma digitale della risposta ottenuta " f"non risulta essere corretta. {_TROUBLESHOOT_MSG}"), }) except Exception as err: return self.handle_error( **{ "err": err, "message": "Anomalia riscontrata nel processo di Autenticazione", "troubleshoot": _TROUBLESHOOT_MSG, }) if self.sp.config.getattr("allow_unsolicited", "sp") is False: req_id = authn_response.in_response_to if req_id not in self.outstanding_queries: errmsg = ("No request with id: {}".format(req_id), ) logger.debug(errmsg) return self.handle_error(**{ "message": errmsg, "troubleshoot": _TROUBLESHOOT_MSG }) del self.outstanding_queries[req_id] # Context validation if not context.state.get(self.name): _msg = f"context.state[self.name] KeyError: where self.name is {self.name}" logger.error(_msg) return self.handle_error(**{ "message": _msg, "troubleshoot": _TROUBLESHOOT_MSG }) # check if the relay_state matches the cookie state if context.state[ self.name]["relay_state"] != context.request["RelayState"]: _msg = "State did not match relay state for state" return self.handle_error(**{ "message": _msg, "troubleshoot": _TROUBLESHOOT_MSG }) # Spid and SAML2 additional tests _sp_config = self.config["sp_config"] accepted_time_diff = _sp_config["accepted_time_diff"] recipient = _sp_config["service"]["sp"]["endpoints"][ "assertion_consumer_service"][0][0] authn_context_classref = self.config["acr_mapping"][""] issuer = authn_response.response.issuer # this will get the entity name in state if len(context.state.keys()) < 2: _msg = "Inconsistent context.state" return self.handle_error(**{ "message": _msg, "troubleshoot": _TROUBLESHOOT_MSG }) list(context.state.keys())[1] # deprecated # if not context.state.get('Saml2IDP'): # _msg = "context.state['Saml2IDP'] KeyError" # logger.error(_msg) # raise SATOSAStateError(context.state, "State without Saml2IDP") in_response_to = context.state["req_args"]["id"] # some debug if authn_response.ava: logging.debug(f"Attributes to {authn_response.return_addrs} " f"in_response_to {authn_response.in_response_to}: " f'{",".join(authn_response.ava.keys())}') validator = Saml2ResponseValidator( authn_response=authn_response.xmlstr, recipient=recipient, in_response_to=in_response_to, accepted_time_diff=accepted_time_diff, authn_context_class_ref=authn_context_classref, return_addrs=authn_response.return_addrs, allowed_acrs=self.config["spid_allowed_acrs"], ) try: validator.run() except Exception as e: logger.error(e) return self.handle_error(e) context.decorate(Context.KEY_BACKEND_METADATA_STORE, self.sp.metadata) if self.config.get(SAMLBackend.KEY_MEMORIZE_IDP): issuer = authn_response.response.issuer.text.strip() context.state[Context.KEY_MEMORIZED_IDP] = issuer context.state.pop(self.name, None) context.state.pop(Context.KEY_FORCE_AUTHN, None) logger.info(f"SAMLResponse{authn_response.xmlstr}") return self.auth_callback_func( context, self._translate_response(authn_response, context.state))
def authn_request(self, context, entity_id): """ Do an authorization request on idp with given entity id. This is the start of the authorization. :type context: satosa.context.Context :type entity_id: str :rtype: satosa.response.Response :param context: The current context :param entity_id: Target IDP entity id :return: response to the user agent """ self.check_blacklist(context, entity_id) kwargs = {} # fetch additional kwargs kwargs.update(self.get_kwargs_sign_dig_algs()) authn_context = self.construct_requested_authn_context(entity_id) req_authn_context = authn_context or requested_authn_context( class_ref=self._authn_context) req_authn_context.comparison = self.config.get("spid_acr_comparison", "minimum") # force_auth = true only if SpidL >= 2 if "SpidL1" in authn_context.authn_context_class_ref[0].text: force_authn = "false" else: force_authn = "true" try: binding = saml2.BINDING_HTTP_POST destination = context.internal_data.get("target_entity_id", entity_id) # SPID CUSTOMIZATION # client = saml2.client.Saml2Client(conf) client = self.sp logger.debug(f"binding: {binding}, destination: {destination}") # acs_endp, response_binding = self.sp.config.getattr("endpoints", "sp")["assertion_consumer_service"][0] # req_id, req = self.sp.create_authn_request( # destination, binding=response_binding, **kwargs) logger.debug(f"Redirecting user to the IdP via {binding} binding.") # use the html provided by pysaml2 if no template was specified or it didn't exist # SPID want the fqdn of the IDP as entityID, not the SSO endpoint # 'http://idpspid.testunical.it:8088' # dovrebbe essere destination ma nel caso di spid-testenv2 è entityid... # binding, destination = self.sp.pick_binding("single_sign_on_service", None, "idpsso", entity_id=entity_id) location = client.sso_location(destination, binding) # location = client.sso_location(entity_id, binding) # not used anymore thanks to avviso 11 # location_fixed = destination # entity_id # ...hope to see the SSO endpoint soon in spid-testenv2 # returns 'http://idpspid.testunical.it:8088/sso' # fixed: https://github.com/italia/spid-testenv2/commit/6041b986ec87ab8515dd0d43fed3619ab4eebbe9 # verificare qui # acs_endp, response_binding = self.sp.config.getattr("endpoints", "sp")["assertion_consumer_service"][0] authn_req = saml2.samlp.AuthnRequest() authn_req.force_authn = force_authn authn_req.destination = location # spid-testenv2 preleva l'attribute consumer service dalla authnRequest # (anche se questo sta già nei metadati...) authn_req.attribute_consuming_service_index = "0" issuer = saml2.saml.Issuer() issuer.name_qualifier = client.config.entityid issuer.text = client.config.entityid issuer.format = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity" authn_req.issuer = issuer # message id authn_req.id = saml2.s_utils.sid() authn_req.version = saml2.VERSION # "2.0" authn_req.issue_instant = saml2.time_util.instant() name_id_policy = saml2.samlp.NameIDPolicy() # del(name_id_policy.allow_create) name_id_policy.format = NAMEID_FORMAT_TRANSIENT authn_req.name_id_policy = name_id_policy # TODO: use a parameter instead authn_req.requested_authn_context = req_authn_context authn_req.protocol_binding = binding assertion_consumer_service_url = client.config._sp_endpoints[ "assertion_consumer_service"][0][0] authn_req.assertion_consumer_service_url = ( assertion_consumer_service_url # 'http://sp-fqdn/saml2/acs/' ) authn_req_signed = client.sign( authn_req, sign_prepare=False, sign_alg=kwargs["sign_alg"], digest_alg=kwargs["digest_alg"], ) authn_req.id _req_str = authn_req_signed logger.debug(f"AuthRequest to {destination}: {_req_str}") relay_state = util.rndstr() ht_args = client.apply_binding( binding, _req_str, location, sign=True, sigalg=kwargs["sign_alg"], relay_state=relay_state, ) if self.sp.config.getattr("allow_unsolicited", "sp") is False: if authn_req.id in self.outstanding_queries: errmsg = "Request with duplicate id {}".format( authn_req.id) logger.debug(errmsg) raise SATOSAAuthenticationError(context.state, errmsg) self.outstanding_queries[authn_req.id] = authn_req_signed context.state[self.name] = {"relay_state": relay_state} # these will give the way to check compliances between the req and resp context.state["req_args"] = {"id": authn_req.id} logger.info(f"SAMLRequest: {ht_args}") return make_saml_response(binding, ht_args) except Exception as exc: logger.debug("Failed to construct the AuthnRequest for state") raise SATOSAAuthenticationError( context.state, "Failed to construct the AuthnRequest") from exc
def callback(self, response, state, backend_state): """ This is the method that should be called when an AuthN response has been received from the OP. :type response: dict[str, str] :type state: satosa.sate.State :type backend_state: dict[str, str] :rtype: satosa.response.Response :param response: The response parameters from the OP. :param state: A SATOSA state. :param backend_state: The state data for this backend module. :return: """ authresp = self.parse_response(AuthorizationResponse, response, sformat="dict", keyjar=self.keyjar) if isinstance(authresp, ErrorResponse): if authresp["error"] == "login_required": satosa_logging(LOGGER, logging.WARN, "Access denied for state: %s" % backend_state, state) raise SATOSAAuthenticationError(state, "Access denied") else: satosa_logging(LOGGER, logging.DEBUG, "Access denied for state: %s" % backend_state, state) raise SATOSAAuthenticationError(state, "Access denied") try: if authresp["id_token"] != backend_state["nonce"]: satosa_logging(LOGGER, logging.DEBUG, "Invalid nonce. for state: %s" % backend_state, state) raise SATOSAAuthenticationError(state, "Invalid nonce") self.id_token[authresp["state"]] = authresp["id_token"] except KeyError: pass if self.behaviour["response_type"] == "code": # get the access token try: args = { "code": authresp["code"], "redirect_uri": self.registration_response["redirect_uris"][0], "client_id": self.client_id, "client_secret": self.client_secret } atresp = self.do_access_token_request( scope="openid", state=authresp["state"], request_args=args, authn_method=self. registration_response["token_endpoint_auth_method"]) except Exception as err: satosa_logging(LOGGER, logging.ERROR, "%s" % err, state, exc_info=True) raise if isinstance(atresp, ErrorResponse): msg = "Invalid response %s." % atresp["error"] satosa_logging(LOGGER, logging.ERROR, msg, state) raise SATOSAAuthenticationError(state, msg) kwargs = {} try: kwargs = {"method": self.userinfo_request_method} except AttributeError: pass inforesp = self.do_user_info_request(state=authresp["state"], **kwargs) if isinstance(inforesp, ErrorResponse): msg = "Invalid response %s." % inforesp["error"] satosa_logging(LOGGER, logging.ERROR, msg, state) raise SATOSAAuthenticationError( state, "Invalid response %s." % inforesp["error"]) userinfo = inforesp.to_dict() satosa_logging(LOGGER, logging.DEBUG, "UserInfo: %s" % inforesp, state) return userinfo
def authn_request(self, context, entity_id, requested_attributes=None): """ Do an authorization request on idp with given entity id. This is the start of the authorization. :type context: satosa.context.Context :type entity_id: str :type requested_attributes: list :rtype: satosa.response.Response :param context: The current context :param entity_id: Target IDP entity id :return: response to the user agent """ # If IDP blacklisting is enabled and the selected IDP is blacklisted, # stop here if self.idp_blacklist_file: with open(self.idp_blacklist_file) as blacklist_file: blacklist_array = json.load(blacklist_file)['blacklist'] if entity_id in blacklist_array: msg = "IdP with EntityID {} is blacklisted".format( entity_id) logline = lu.LOG_FMT.format(id=lu.get_session_id( context.state), message=msg) logger.debug(logline, exc_info=False) raise SATOSAAuthenticationError( context.state, "Selected IdP is blacklisted for this backend") try: binding, destination = self.sp.pick_binding( "single_sign_on_service", None, "idpsso", entity_id=entity_id) msg = "binding: {}, destination: {}".format(binding, destination) logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) logger.debug(logline) kwargs = self._get_authn_request_args( context, entity_id, requested_attributes=requested_attributes) req_id, req = self.sp.create_authn_request(destination, **kwargs) relay_state = util.rndstr() ht_args = self.sp.apply_binding(binding, "%s" % req, destination, relay_state=relay_state) msg = "ht_args: {}".format(ht_args) logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) logger.debug(logline) except Exception as exc: msg = "Failed to construct the AuthnRequest for state" logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) logger.debug(logline, exc_info=True) raise SATOSAAuthenticationError( context.state, "Failed to construct the AuthnRequest") from exc if self.sp.config.getattr('allow_unsolicited', 'sp') is False: if req_id in self.outstanding_queries: msg = "Request with duplicate id {}".format(req_id) logline = lu.LOG_FMT.format(id=lu.get_session_id( context.state), message=msg) logger.debug(logline) raise SATOSAAuthenticationError(context.state, msg) self.outstanding_queries[req_id] = req context.state[self.name] = {"relay_state": relay_state} return make_saml_response(binding, ht_args)
def authn_response(self, context, binding): """ Endpoint for the idp response :type context: satosa.context,Context :type binding: str :rtype: satosa.response.Response :param context: The current context :param binding: The saml binding type :return: response """ if not context.request["SAMLResponse"]: logger.debug("Missing Response for state") raise SATOSAAuthenticationError(context.state, "Missing Response") try: authn_response = self.sp.parse_authn_request_response( context.request["SAMLResponse"], binding, outstanding=self.outstanding_queries) except Exception as err: logger.debug("Failed to parse authn request for state") raise SATOSAAuthenticationError(context.state, "Failed to parse authn request") from err if self.sp.config.getattr('allow_unsolicited', 'sp') is False: req_id = authn_response.in_response_to if req_id not in self.outstanding_queries: errmsg = "No request with id: {}".format(req_id), logger.debug(errmsg) raise SATOSAAuthenticationError(context.state, errmsg) del self.outstanding_queries[req_id] # Context validation if not context.state.get(self.name): _msg = "context.state[self.name] KeyError: where self.name is {}".format(self.name) logger.error(_msg) raise SATOSAStateError(context.state, _msg) # check if the relay_state matches the cookie state if context.state[self.name]["relay_state"] != context.request["RelayState"]: logger.debug("State did not match relay state for state") raise SATOSAAuthenticationError(context.state, "State did not match relay state") # Spid and SAML2 additional tests accepted_time_diff = self.config['sp_config']['accepted_time_diff'] recipient = self.config['sp_config']['service']['sp']['endpoints']['assertion_consumer_service'][0][0] authn_context_classref = self.config['acr_mapping'][''] issuer = authn_response.response.issuer # this will get the entity name in state if len(context.state.keys()) < 2: _msg = "Inconsistent context.state" logger.error(_msg) raise SATOSAStateError(context.state, _msg) destination_frontend = list(context.state.keys())[1] # deprecated # if not context.state.get('Saml2IDP'): # _msg = "context.state['Saml2IDP'] KeyError" # logger.error(_msg) # raise SATOSAStateError(context.state, "State without Saml2IDP") in_response_to = context.state['req_args']['id'] requester = context.state['SATOSA_BASE']['requester'] # some debug if authn_response.ava: logging.debug(f'Attributes to {authn_response.return_addrs} ' f'in_response_to {authn_response.in_response_to}: ' f'{",".join(authn_response.ava.keys())}') validator = Saml2ResponseValidator(authn_response=authn_response.xmlstr, recipient = recipient, in_response_to=in_response_to, requester = requester, accepted_time_diff = accepted_time_diff, authn_context_class_ref=authn_context_classref, return_addrs=authn_response.return_addrs) validator.run() context.decorate(Context.KEY_BACKEND_METADATA_STORE, self.sp.metadata) if self.config.get(SAMLBackend.KEY_MEMORIZE_IDP): issuer = authn_response.response.issuer.text.strip() context.state[Context.KEY_MEMORIZED_IDP] = issuer context.state.pop(self.name, None) context.state.pop(Context.KEY_FORCE_AUTHN, None) return self.auth_callback_func(context, self._translate_response(authn_response, context.state))
def authn_response(self, context, binding): """ Endpoint for the idp response :type context: satosa.context,Context :type binding: str :rtype: satosa.response.Response :param context: The current context :param binding: The saml binding type :return: response """ if not context.request["SAMLResponse"]: satosa_logging(logger, logging.DEBUG, "Missing Response for state", context.state) raise SATOSAAuthenticationError(context.state, "Missing Response") try: authn_response = self.sp.parse_authn_request_response( context.request["SAMLResponse"], binding, outstanding=self.outstanding_queries) except Exception as err: satosa_logging(logger, logging.DEBUG, "Failed to parse authn request for state", context.state, exc_info=True) raise SATOSAAuthenticationError( context.state, "Failed to parse authn request") from err if self.sp.config.getattr('allow_unsolicited', 'sp') is False: req_id = authn_response.in_response_to if req_id not in self.outstanding_queries: errmsg = "No request with id: {}".format(req_id), satosa_logging(logger, logging.DEBUG, errmsg, context.state) raise SATOSAAuthenticationError(context.state, errmsg) del self.outstanding_queries[req_id] # check if the relay_state matches the cookie state if context.state[ self.name]["relay_state"] != context.request["RelayState"]: satosa_logging(logger, logging.DEBUG, "State did not match relay state for state", context.state) raise SATOSAAuthenticationError(context.state, "State did not match relay state") # Spid and SAML2 additional tests issuer = context.state['Saml2IDP']['resp_args']['sp_entity_id'] accepted_time_diff = self.config['sp_config']['accepted_time_diff'] recipient = self.config['sp_config']['service']['sp']['endpoints'][ 'assertion_consumer_service'][0][0] authn_context_classref = self.config['acr_mapping'][''] in_response_to = context.state['Saml2IDP']['resp_args'][ 'in_response_to'] validator = Saml2ResponseValidator( authn_response=authn_response.xmlstr, recipient=recipient, in_response_to=in_response_to, accepted_time_diff=accepted_time_diff, authn_context_class_ref=authn_context_classref) validator.run() context.decorate(Context.KEY_BACKEND_METADATA_STORE, self.sp.metadata) if self.config.get(SAMLBackend.KEY_MEMORIZE_IDP): issuer = authn_response.response.issuer.text.strip() context.state[Context.KEY_MEMORIZED_IDP] = issuer context.state.pop(self.name, None) context.state.pop(Context.KEY_FORCE_AUTHN, None) return self.auth_callback_func( context, self._translate_response(authn_response, context.state))
def authn_request(self, context, entity_id): """ Do an authorization request on idp with given entity id. This is the start of the authorization. :type context: satosa.context.Context :type entity_id: str :rtype: satosa.response.Response :param context: The current context :param entity_id: Target IDP entity id :return: response to the user agent """ self.check_blacklist() kwargs = {} # fetch additional kwargs kwargs.update(self.get_kwargs_sign_dig_algs()) authn_context = self.construct_requested_authn_context(entity_id) requested_authn_context = authn_context or requested_authn_context( class_ref=self._authn_context) # force_auth = true only if SpidL >= 2 if 'SpidL1' in authn_context.authn_context_class_ref[0].text: force_authn = 'false' else: force_authn = 'true' try: binding = saml2.BINDING_HTTP_POST destination = context.request['entityID'] # SPID CUSTOMIZATION #client = saml2.client.Saml2Client(conf) client = self.sp satosa_logging( logger, logging.DEBUG, "binding: %s, destination: %s" % (binding, destination), context.state) # acs_endp, response_binding = self.sp.config.getattr("endpoints", "sp")["assertion_consumer_service"][0] # req_id, req = self.sp.create_authn_request( # destination, binding=response_binding, **kwargs) logger.debug('Redirecting user to the IdP via %s binding.', binding) # use the html provided by pysaml2 if no template was specified or it didn't exist # SPID want the fqdn of the IDP as entityID, not the SSO endpoint # 'http://idpspid.testunical.it:8088' # dovrebbe essere destination ma nel caso di spid-testenv2 è entityid... # binding, destination = self.sp.pick_binding("single_sign_on_service", None, "idpsso", entity_id=entity_id) # location = client.sso_location(destination, binding) location = client.sso_location(entity_id, binding) location_fixed = entity_id # ...hope to see the SSO endpoint soon in spid-testenv2 # returns 'http://idpspid.testunical.it:8088/sso' # fixed: https://github.com/italia/spid-testenv2/commit/6041b986ec87ab8515dd0d43fed3619ab4eebbe9 # verificare qui # acs_endp, response_binding = self.sp.config.getattr("endpoints", "sp")["assertion_consumer_service"][0] authn_req = saml2.samlp.AuthnRequest() authn_req.force_authn = force_authn authn_req.destination = location # spid-testenv2 preleva l'attribute consumer service dalla authnRequest (anche se questo sta già nei metadati...) authn_req.attribute_consuming_service_index = "0" # import pdb; pdb.set_trace() issuer = saml2.saml.Issuer() issuer.name_qualifier = client.config.entityid issuer.text = client.config.entityid issuer.format = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity" authn_req.issuer = issuer # message id authn_req.id = saml2.s_utils.sid() authn_req.version = saml2.VERSION # "2.0" authn_req.issue_instant = saml2.time_util.instant() name_id_policy = saml2.samlp.NameIDPolicy() # del(name_id_policy.allow_create) name_id_policy.format = NAMEID_FORMAT_TRANSIENT authn_req.name_id_policy = name_id_policy # TODO: use a parameter instead authn_req.requested_authn_context = requested_authn_context authn_req.protocol_binding = binding assertion_consumer_service_url = client.config._sp_endpoints[ 'assertion_consumer_service'][0][0] authn_req.assertion_consumer_service_url = assertion_consumer_service_url #'http://sp-fqdn/saml2/acs/' authn_req_signed = client.sign(authn_req, sign_prepare=False, sign_alg=kwargs['sign_alg'], digest_alg=kwargs['digest_alg']) session_id = authn_req.id _req_str = authn_req_signed logger.debug('AuthRequest to {}: {}'.format( destination, (_req_str))) relay_state = util.rndstr() ht_args = client.apply_binding(binding, _req_str, location, sign=True, sigalg=kwargs['sign_alg'], relay_state=relay_state) if self.sp.config.getattr('allow_unsolicited', 'sp') is False: if authn_req.id in self.outstanding_queries: errmsg = "Request with duplicate id {}".format(req_id) satosa_logging(logger, logging.DEBUG, errmsg, context.state) raise SATOSAAuthenticationError(context.state, errmsg) self.outstanding_queries[authn_req.id] = authn_req_signed context.state[self.name] = {"relay_state": relay_state} satosa_logging(logger, logging.DEBUG, "ht_args: %s" % ht_args, context.state) return make_saml_response(binding, ht_args) except Exception as exc: satosa_logging(logger, logging.DEBUG, "Failed to construct the AuthnRequest for state", context.state, exc_info=True) raise SATOSAAuthenticationError( context.state, "Failed to construct the AuthnRequest") from exc