def run(self, context): """ Runs the satosa proxy with the given context. :type context: satosa.context.Context :rtype: satosa.response.Response :param context: The request context :return: response """ try: self._load_state(context) spec = self.module_router.endpoint_routing(context) resp = self._run_bound_endpoint(context, spec) self._save_state(resp, context) except SATOSAError: satosa_logging(LOGGER, logging.ERROR, "Uncaught SATOSA error", context.state, exc_info=True) raise except Exception as err: satosa_logging(LOGGER, logging.ERROR, "Uncaught exception", context.state, exc_info=True) raise SATOSAUnknownError("Unknown error") from err return resp
def _save_state(self, resp, context): """ Saves a state from context to cookie :type resp: satosa.response.Response :type context: satosa.context.Context :param resp: The response :param context: Session context """ if context.state.should_delete(): # Save empty state with a max age of 0 cookie = state_to_cookie(State(), self.config.COOKIE_STATE_NAME, "/", self.config.STATE_ENCRYPTION_KEY, 0) else: cookie = state_to_cookie(context.state, self.config.COOKIE_STATE_NAME, "/", self.config.STATE_ENCRYPTION_KEY) if isinstance(resp, Response): resp.add_cookie(cookie) else: try: resp.headers.append(tuple(cookie.output().split(": ", 1))) except: satosa_logging( LOGGER, logging.WARN, "can't add cookie to response '%s'" % resp.__class__, context.state) pass
def _auth_req_callback_func(self, context, internal_request): """ This function is called by a frontend module when an authorization request has been processed. :type context: satosa.context.Context :type internal_request: satosa.internal_data.InternalRequest :rtype: satosa.response.Response :param context: The request context :param internal_request: request processed by the frontend :return: response """ state = context.state state.add(SATOSABase.STATE_KEY, internal_request.requestor) satosa_logging( LOGGER, logging.INFO, "Requesting provider: {}".format(internal_request.requestor), state) context.request = None backend = self.module_router.backend_routing(context) self.consent_module.save_state(internal_request, state) UserIdHasher.save_state(internal_request, state) if self.request_micro_services: internal_request = self.request_micro_services.process_service_queue( context, internal_request) return backend.start_auth(context, internal_request)
def _run_bound_endpoint(self, context, spec): """ :type context: satosa.context.Context :type spec: ((satosa.context.Context, Any) -> satosa.response.Response, Any) | (satosa.context.Context) -> satosa.response.Response :param context: The request context :param spec: bound endpoint function :return: response """ try: if isinstance(spec, tuple): return spec[0](context, *spec[1:]) else: return spec(context) except SATOSAAuthenticationError as error: error.error_id = uuid4().urn msg = "ERROR_ID [{err_id}]\nSTATE:\n{state}".format( err_id=error.error_id, state=json.dumps(error.state.state_dict, indent=4)) satosa_logging(LOGGER, logging.ERROR, msg, error.state, exc_info=True) return self._handle_satosa_authentication_error(error)
def _save_state(self, resp, context): """ Saves a state from context to cookie :type resp: satosa.response.Response :type context: satosa.context.Context :param resp: The response :param context: Session context """ if context.state.should_delete(): # Save empty state with a max age of 0 cookie = state_to_cookie(State(), self.config.COOKIE_STATE_NAME, "/", self.config.STATE_ENCRYPTION_KEY, 0) else: cookie = state_to_cookie(context.state, self.config.COOKIE_STATE_NAME, "/", self.config.STATE_ENCRYPTION_KEY) if isinstance(resp, Response): resp.add_cookie(cookie) else: try: resp.headers.append(tuple(cookie.output().split(": ", 1))) except: satosa_logging(LOGGER, logging.WARN, "can't add cookie to response '%s'" % resp.__class__, context.state) pass
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 _handle_backend_error(self, exception, idp): """ See super class satosa.frontends.base.FrontendModule :type exception: satosa.exception.SATOSAAuthenticationError :type idp: saml.server.Server :rtype: satosa.response.Response :param exception: The SATOSAAuthenticationError :param idp: The saml frontend idp server :return: A response """ loaded_state = self.load_state(exception.state) relay_state = loaded_state["relay_state"] resp_args = loaded_state["resp_args"] error_resp = idp.create_error_response(resp_args["in_response_to"], resp_args["destination"], Exception(exception.message)) http_args = idp.apply_binding( resp_args["binding"], "%s" % error_resp, resp_args["destination"], relay_state, response=True) satosa_logging(LOGGER, logging.DEBUG, "HTTPargs: %s" % http_args, exception.state) return response(resp_args["binding"], http_args)
def _translate_response(self, response, state): """ Translates a saml authorization response to an internal response :type response: saml2.response.AuthnResponse :rtype: satosa.internal_data.InternalResponse :param response: The saml authorization response :return: A translated internal response """ _authn_info = response.authn_info()[0] timestamp = response.assertion.authn_statement[0].authn_instant issuer = response.response.issuer.text auth_class_ref = _authn_info[0] auth_info = AuthenticationInformation(auth_class_ref, timestamp, issuer) internal_resp = InternalResponse(auth_info=auth_info) internal_resp.set_user_id(response.get_subject().text) if "user_id_params" in self.config: user_id = "" for param in self.config["user_id_params"]: try: user_id += response.ava[param] except Exception as error: raise SATOSAAuthenticationError from error internal_resp.set_user_id(user_id) internal_resp.add_attributes(self.converter.to_internal(self.attribute_profile, response.ava)) satosa_logging(LOGGER, logging.DEBUG, "received attributes:\n%s" % json.dumps(response.ava, indent=4), state) return internal_resp
def manage_al(self, context, internal_response): """ Manage account linking and recovery :type context: satosa.context.Context :type internal_response: satosa.internal_data.InternalResponse :rtype: satosa.response.Response :param context: :param internal_response: :return: response """ if not self.enabled: return self.callback_func(context, internal_response) issuer = internal_response.auth_info.issuer id = internal_response.get_user_id() status_code, message = self._get_uuid(context, issuer, id) if status_code == 200: satosa_logging(LOGGER, logging.INFO, "issuer/id pair is linked in AL service", context.state) internal_response.set_user_id(message) try: context.state.remove(AccountLinkingModule.STATE_KEY) except KeyError: pass return self.callback_func(context, internal_response) return self._approve_new_id(context, internal_response, message)
def _populate_attributes(self, config, record, context, data): """ Use a record found in LDAP to populate attributes. """ search_return_attributes = config['search_return_attributes'] for attr in search_return_attributes.keys(): if attr in record["attributes"]: if record["attributes"][attr]: data.attributes[search_return_attributes[attr]] = record["attributes"][attr] satosa_logging( logger, logging.DEBUG, "Setting internal attribute {} with values {}".format( search_return_attributes[attr], record["attributes"][attr] ), context.state ) else: satosa_logging( logger, logging.DEBUG, "Not setting internal attribute {} because value {} is null or empty".format( search_return_attributes[attr], record["attributes"][attr] ), context.state )
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_filter_attributes(self, idp, idp_policy, sp_entity_id, state): """ Returns a list of approved attributes :type idp: saml.server.Server :type idp_policy: saml2.assertion.Policy :type sp_entity_id: str :type state: satosa.state.State :rtype: list[str] :param idp: The saml frontend idp server :param idp_policy: The idp policy :param sp_entity_id: The requesting sp entity id :param state: The current state :return: A list containing approved attributes """ name_format = idp_policy.get_name_form(sp_entity_id) attrconvs = idp.config.attribute_converters idp_policy.acs = attrconvs attribute_filter = [] for aconv in attrconvs: if aconv.name_format == name_format: attribute_filter = list( idp_policy.restrict(aconv._to, sp_entity_id, idp.metadata).keys()) attribute_filter = self.converter.to_internal_filter(self.attribute_profile, attribute_filter, True) satosa_logging(LOGGER, logging.DEBUG, "Filter: %s" % attribute_filter, state) return attribute_filter
def render_consent(self, consent_state, internal_response, language='en'): requester_name = consent_state.get('requester_display_name', None) if not requester_name: requester_name = self._find_requester_name( internal_response.requester, language) requester_logo = consent_state.get('requester_logo', None) gettext.translation('messages', localedir=pkg_resources.resource_filename( 'svs', 'data/i18n/locale'), languages=[language]).install() released_attributes = self._attributes_to_release(internal_response) template = self.template_lookup.get_template('consent.mako') page = template.render( requester_name=requester_name, requester_logo=self._normalize_logo(requester_logo), privacy_url=self.privacy_url, released_claims=released_attributes, form_action='/consent{}'.format(self.endpoint), language=language) page = Template(page).render( requester_name=requester_name, requester_logo=self._normalize_logo(requester_logo), privacy_url=self.privacy_url, released_claims=released_attributes, form_action='/consent{}'.format(self.endpoint), language=language) satosa_logging(logger, logging.INFO, "released attributes: {}".format(released_attributes), consent_state) return Response(page, content='text/html')
def check_set_dict_defaults(dic, spec): for path, value in spec.items(): keys = path.split('.') try: _val = dict_get_nested(dic, keys) except KeyError: if type(value) is list: value_default = value[0] else: value_default = value dict_set_nested(dic, keys, value_default) else: if type(value) is list: is_value_valid = _val in value elif type(value) is dict: # do not validate dict is_value_valid = bool(_val) else: is_value_valid = _val == value if not is_value_valid: satosa_logging( logger, logging.WARNING, "Incompatible configuration value '{}' for '{}'." " Value shoud be: {}".format(_val, path, value), {}) return dic
def create_authn_request(self, state, oidc_state, nonce, acr_value=None, **kwargs): """ Creates an oidc authentication request. :type state: satosa.state.State :type oidc_state: str :type nonce: str :type acr_value: list[str] :type kwargs: Any :rtype: satosa.response.Redirect :param state: Module state :param oidc_state: OIDC state :param nonce: A nonce :param acr_value: Authentication type :param kwargs: Whatever :return: A redirect to the OP """ request_args = self.setup_authn_request_args(acr_value, kwargs, oidc_state, nonce) cis = self.construct_AuthorizationRequest(request_args=request_args) satosa_logging(LOGGER, logging.DEBUG, "request: %s" % cis, state) url, body, ht_args, cis = self.uri_and_body(AuthorizationRequest, cis, method="GET", request_args=request_args) satosa_logging(LOGGER, logging.DEBUG, "body: %s" % body, state) satosa_logging(LOGGER, logging.INFO, "URL: %s" % url, state) satosa_logging(LOGGER, logging.DEBUG, "ht_args: %s" % ht_args, state) resp = Redirect(str(url)) if ht_args: resp.headers.extend([(a, b) for a, b in ht_args.items()]) satosa_logging(LOGGER, logging.DEBUG, "resp_headers: %s" % resp.headers, state) return resp
def _auth_req_callback_func(self, context, internal_request): """ This function is called by a frontend module when an authorization request has been processed. :type context: satosa.context.Context :type internal_request: satosa.internal_data.InternalRequest :rtype: satosa.response.Response :param context: The request context :param internal_request: request processed by the frontend :return: response """ state = context.state state.add(SATOSABase.STATE_KEY, internal_request.requestor) satosa_logging(LOGGER, logging.INFO, "Requesting provider: {}".format(internal_request.requestor), state) context.request = None backend = self.module_router.backend_routing(context) self.consent_module.save_state(internal_request, state) UserIdHasher.save_state(internal_request, state) if self.request_micro_services: internal_request = self.request_micro_services.process_service_queue(context, internal_request) return backend.start_auth(context, internal_request)
def run(self, context): """ Runs the satosa proxy with the given context. :type context: satosa.context.Context :rtype: satosa.response.Response :param context: The request context :return: response """ try: self._load_state(context) spec = self.module_router.endpoint_routing(context) resp = self._run_bound_endpoint(context, spec) self._save_state(resp, context) except SATOSANoBoundEndpointError: raise except SATOSAError: satosa_logging(LOGGER, logging.ERROR, "Uncaught SATOSA error", context.state, exc_info=True) raise except Exception as err: satosa_logging(LOGGER, logging.ERROR, "Uncaught exception", context.state, exc_info=True) raise SATOSAUnknownError("Unknown error") from err return resp
def _populate_attributes(self, config, record): """ Use a record found in LDAP to populate attributes. """ ldap_attributes = record.get("attributes", None) if not ldap_attributes: msg = "No attributes returned with LDAP record" satosa_logging(logger, logging.DEBUG, msg, None) return ldap_to_internal_map = ( config["ldap_to_internal_map"] if config["ldap_to_internal_map"] # Deprecated configuration. Will be removed in future. else config["search_return_attributes"]) attributes = defaultdict(list) for attr, values in ldap_attributes.items(): internal_attr = ldap_to_internal_map.get(attr, None) if not internal_attr and ";" in attr: internal_attr = ldap_to_internal_map.get( attr.split(";")[0], None) if internal_attr and values: attributes[internal_attr].extend(values) msg = "Recording internal attribute {} with values {}" msg = msg.format(internal_attr, attributes[internal_attr]) satosa_logging(logger, logging.DEBUG, msg, None) return attributes
def _apply_filter(self, context, attributes, provider, requester): filter_allow = get_dict_defaults(self.attribute_allow, provider, requester) satosa_logging( logger, logging.DEBUG, "{} filter_allow: {}".format(self.logprefix, filter_allow), context.state) filter_deny = get_dict_defaults(self.attribute_deny, provider, requester) satosa_logging( logger, logging.DEBUG, "{} filter_deny: {}".format(self.logprefix, filter_deny), context.state) allow = self._filter_attributes(attributes, filter_allow) satosa_logging(logger, logging.DEBUG, "{} allow: {}".format(self.logprefix, allow), context.state) deny = self._filter_attributes(attributes, filter_deny) satosa_logging(logger, logging.DEBUG, "{} deny: {}".format(self.logprefix, deny), context.state) # Remove Denies (values) from Allows (values) for a, v in deny.items(): for r in v: if a in allow and r in allow[a]: allow[a].remove(r) result = {a: v for a, v in allow.items() if len(v)} satosa_logging(logger, logging.DEBUG, "{} result: {}".format(self.logprefix, result), context.state) return result
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 state_to_cookie(state, name, path, encryption_key, max_age=STATE_COOKIE_MAX_AGE): """ Saves a state to a cookie :type state: satosa.state.State :type name: str :type path: str :type encryption_key: str :rtype: http.cookies.SimpleCookie :param state: The state to save :param name: Name identifier of the cookie :param path: Endpoint path the cookie will be associated to :param encryption_key: Key to encrypt the state information :return: A cookie """ satosa_logging(LOGGER, logging.DEBUG, "Saving state as cookie, secure: %s, max-age: %s, path: %s" % (STATE_COOKIE_SECURE, STATE_COOKIE_MAX_AGE, path), state) cookie = SimpleCookie() cookie[name] = state.urlstate(encryption_key) cookie[name]["secure"] = STATE_COOKIE_SECURE cookie[name]["path"] = path cookie[name]["max-age"] = max_age return cookie
def handle_authn_request(self, context): """ Parse and verify the authentication request and pass it on to the backend. :type context: satosa.context.Context :rtype: oic.utils.http_util.Response :param context: the current context :return: HTTP response to the client """ # verify auth req (correct redirect_uri, contains nonce and response_type='id_token') request = urlencode(context.request) satosa_logging(LOGGER, logging.DEBUG, "Authn req from client: {}".format(request), context.state) info = self.provider.auth_init(request, request_class=AuthorizationRequest) if isinstance(info, Response): satosa_logging(LOGGER, logging.ERROR, "Error in authn req: {}".format(info.message), context.state) return info client_id = info["areq"]["client_id"] context.state.add(self.state_id, {"oidc_request": request}) hash_type = oidc_subject_type_to_hash_type( self.provider.cdb[client_id].get("subject_type", self.subject_type_default)) internal_req = InternalRequest(hash_type, client_id, self.provider.cdb[client_id].get("client_name")) return self.auth_req_callback_func(context, internal_req)
def get_idp_entity_id(self, context): """ :type context: satosa.context.Context :rtype: str | None :param context: The current context :return: the entity_id of the idp or None """ idps = self.sp.metadata.identity_providers() only_one_idp_in_metadata = ("mdq" not in self.config["sp_config"]["metadata"] and len(idps) == 1) only_idp = only_one_idp_in_metadata and idps[0] target_entity_id = context.get_decoration(Context.KEY_TARGET_ENTITYID) force_authn = get_force_authn(context, self.config, self.sp.config) memorized_idp = get_memorized_idp(context, self.config, force_authn) entity_id = only_idp or target_entity_id or memorized_idp or None satosa_logging( logger, logging.INFO, { "message": "Selected IdP", "only_one": only_idp, "target_entity_id": target_entity_id, "force_authn": force_authn, "memorized_idp": memorized_idp, "entity_id": entity_id, }, context.state, ) return entity_id
def _approve_new_consent(self, context, internal_response, id_hash): context.state[STATE_KEY]["internal_resp"] = internal_response.to_dict() consent_args = { "attr": internal_response.attributes, "id": id_hash, "redirect_endpoint": "%s/consent%s" % (self.base_url, self.endpoint), "requester_name": context.state[STATE_KEY]["requester_name"] } if self.locked_attr: consent_args["locked_attrs"] = [self.locked_attr] if 'requester_logo' in context.state[STATE_KEY]: consent_args["requester_logo"] = context.state[STATE_KEY][ 'requester_logo'] try: ticket = self._consent_registration(consent_args) except (ConnectionError, UnexpectedResponseError) as e: satosa_logging( logger, logging.ERROR, "Consent request failed, no consent given: {}".format(str(e)), context.state) # Send an internal_response without any attributes internal_response.attributes = {} return self._end_consent(context, internal_response) consent_redirect = "%s/%s" % (self.redirect_url, ticket) return Redirect(consent_redirect)
def get_filter_attributes(self, idp, idp_policy, sp_entity_id, state): """ Returns a list of approved attributes :type idp: saml.server.Server :type idp_policy: saml2.assertion.Policy :type sp_entity_id: str :type state: satosa.state.State :rtype: list[str] :param idp: The saml frontend idp server :param idp_policy: The idp policy :param sp_entity_id: The requesting sp entity id :param state: The current state :return: A list containing approved attributes """ name_format = idp_policy.get_name_form(sp_entity_id) attrconvs = idp.config.attribute_converters idp_policy.acs = attrconvs attribute_filter = [] for aconv in attrconvs: if aconv.name_format == name_format: attribute_filter = list(idp_policy.restrict(aconv._to, sp_entity_id, idp.metadata).keys()) attribute_filter = self.converter.to_internal_filter(self.attribute_profile, attribute_filter, True) satosa_logging(LOGGER, logging.DEBUG, "Filter: %s" % attribute_filter, state) return attribute_filter
def handle_authn_request(self, context): """ Parse and verify the authentication request and pass it on to the backend. :type context: satosa.context.Context :rtype: oic.utils.http_util.Response :param context: the current context :return: HTTP response to the client """ # verify auth req (correct redirect_uri, contains nonce and response_type='id_token') request = urlencode(context.request) satosa_logging(LOGGER, logging.DEBUG, "Authn req from client: {}".format(request), context.state) info = self.provider.auth_init(request, request_class=AuthorizationRequest) if isinstance(info, Response): satosa_logging(LOGGER, logging.ERROR, "Error in authn req: {}".format(info.message), context.state) return info client_id = info["areq"]["client_id"] context.state.add(self.state_id, {"oidc_request": request}) hash_type = oidc_subject_type_to_hash_type( self.provider.cdb[client_id].get("subject_type", self.subject_type_default)) internal_req = InternalRequest( hash_type, client_id, self.provider.cdb[client_id].get("client_name")) return self.auth_req_callback_func(context, internal_req)
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 accept_consent(self, context): """ Endpoint for handling accepted consent. :type context: satosa.context.Context :rtype: satosa.response.Response :param context: response context :return: response """ consent_state = context.state[STATE_KEY] saved_resp = consent_state['internal_response'] internal_response = InternalResponse.from_dict(saved_resp) del context.state[STATE_KEY] log = {} log['router'] = context.state.state_dict['ROUTER'] log['sessionid'] = context.state.state_dict['SESSION_ID'] log['timestamp'] = saved_resp['auth_info'].get('timestamp') log['idp'] = saved_resp['auth_info'].get('issuer', None) log['rp'] = saved_resp.get('to', None) log['attr'] = saved_resp.get('attr', None) satosa_logging(logger, logging.INFO, "log: {}".format(log), context.state) print(json.dumps(log), file=self.loghandle, end="\n") self.loghandle.flush() transaction_log(context.state, self.config.get("consent_exit_order", 1000), "user_consent", "accept", "exit", "success", '', '', 'Consent given by the user') return super().process(context, internal_response)
def process(self, context, data): satosa_logging(logger, logging.DEBUG, "{} Processing attribute filter".format(self.logprefix), context.state) data.attributes = self._apply_filter(context, data.attributes, data.auth_info.issuer, data.requester) return super().process(context, data)
def ping_endpoint(self, context): """ """ logprefix = PingFrontend.logprefix satosa_logging(logger, logging.DEBUG, "{} ping returning 200 OK".format(logprefix), context.state) msg = " " return Response(msg)
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 _metadata(self, context): """ Endpoint for retrieving the backend metadata :type context: satosa.context.Context :rtype: satosa.backends.saml2.MetadataResponse :param context: The current context :return: response with metadata """ satosa_logging(LOGGER, logging.DEBUG, "Sending metadata response", context.state) return MetadataResponse(self.sp.config)
def handle_authn_response(self, context, internal_resp): """ See super class method satosa.frontends.base.FrontendModule#handle_authn_response :type context: satosa.context.Context :type internal_response: satosa.internal_data.InternalResponse :rtype oic.utils.http_util.Response """ auth_req = self._get_authn_request_from_state(context.state) # filter attributes to return in ID Token as claims attributes = self.converter.from_internal( "openid", internal_resp.get_attributes()) satosa_logging( LOGGER, logging.DEBUG, "Attributes delivered by backend to OIDC frontend: {}".format( json.dumps(attributes)), context.state) flattened_attributes = {k: v[0] for k, v in attributes.items()} requested_id_token_claims = auth_req.get("claims", {}).get("id_token") user_claims = self._get_user_info(flattened_attributes, requested_id_token_claims, auth_req["scope"]) satosa_logging( LOGGER, logging.DEBUG, "Attributes filtered by requested claims/scope: {}".format( json.dumps(user_claims)), context.state) # construct epoch timestamp of reported authentication time auth_time = datetime.datetime.strptime( internal_resp.auth_info.timestamp, "%Y-%m-%dT%H:%M:%SZ") epoch_timestamp = (auth_time - datetime.datetime(1970, 1, 1)).total_seconds() base_claims = { "client_id": auth_req["client_id"], "sub": internal_resp.get_user_id(), "nonce": auth_req["nonce"] } id_token = self.provider.id_token_as_signed_jwt( base_claims, user_info=user_claims, auth_time=epoch_timestamp, loa="", alg=self.sign_alg) oidc_client_state = auth_req.get("state") kwargs = {} if oidc_client_state: # inlcude any optional 'state' sent by the client in the authn req kwargs["state"] = oidc_client_state auth_resp = AuthorizationResponse(id_token=id_token, **kwargs) http_response = auth_resp.request( auth_req["redirect_uri"], self._should_fragment_encode(auth_req)) return SeeOther(http_response)
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 handle_backend_error(self, exception): """ See super class satosa.frontends.base.FrontendModule :type exception: satosa.exception.SATOSAError :rtype: oic.utils.http_util.Response """ auth_req = self._get_authn_request_from_state(exception.state) error_resp = AuthorizationErrorResponse(error="access_denied", error_description=exception.message) satosa_logging(LOGGER, logging.DEBUG, exception.message, exception.state) return SeeOther( error_resp.request(auth_req["redirect_uri"], self._should_fragment_encode(auth_req)))
def construct_authn_response(self, idp, state, identity, name_id, authn, resp_args, relay_state, sign_response=True): """ Constructs an auth response :type idp: saml.server.Server :type state: satosa.state.State :type identity: dict[str, str] :type name_id: saml2.saml.NameID :type authn: dict[str, str] :type resp_args: dict[str, str] :type relay_state: str :type sign_response: bool :param idp: The saml frontend idp server :param state: The current state :param identity: Information about an user (The ava attributes) :param name_id: The name id :param authn: auth info :param resp_args: response arguments :param relay_state: the relay state :param sign_response: Flag for signing the response or not :return: The constructed response """ _resp = idp.create_authn_response(identity, name_id=name_id, authn=authn, sign_response=sign_response, **resp_args) http_args = idp.apply_binding( resp_args["binding"], "%s" % _resp, resp_args["destination"], relay_state, response=True) satosa_logging(LOGGER, logging.DEBUG, "HTTPargs: %s" % http_args, state) 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: msg = "Don't know how to return response" satosa_logging(LOGGER, logging.ERROR, msg, state) resp = ServiceError(msg) return resp
def _metadata_endpoint(self, context): """ Endpoint for retrieving the backend metadata :type context: satosa.context.Context :rtype: satosa.response.Response :param context: The current context :return: response with metadata """ satosa_logging(logger, logging.DEBUG, "Sending metadata response", context.state) conf = self.sp.config metadata = entity_descriptor(conf) # creare gli attribute_consuming_service cnt = 0 for attribute_consuming_service in metadata.spsso_descriptor.attribute_consuming_service: attribute_consuming_service.index = str(cnt) cnt += 1 cnt = 0 for assertion_consumer_service in metadata.spsso_descriptor.assertion_consumer_service: assertion_consumer_service.is_default = 'true' if not cnt else '' assertion_consumer_service.index = str(cnt) cnt += 1 # nameformat patch... tutto questo non rispecchia gli standard OASIS for reqattr in metadata.spsso_descriptor.attribute_consuming_service[ 0].requested_attribute: reqattr.name_format = None reqattr.friendly_name = None # attribute consuming service service name patch service_name = metadata.spsso_descriptor.attribute_consuming_service[ 0].service_name[0] service_name.lang = 'it' service_name.text = metadata.entity_id # remove extension disco and uuinfo (spid-testenv2) #metadata.spsso_descriptor.extensions = [] # metadata signature secc = security_context(conf) # sign_dig_algs = self.get_kwargs_sign_dig_algs() eid, xmldoc = sign_entity_descriptor(metadata, None, secc, **sign_dig_algs) valid_instance(eid) return Response(text_type(xmldoc).encode('utf-8'), content="text/xml; charset=utf8")
def handle_backend_error(self, exception): """ See super class satosa.frontends.base.FrontendModule :type exception: satosa.exception.SATOSAError :rtype: oic.utils.http_util.Response """ auth_req = self._get_authn_request_from_state(exception.state) error_resp = AuthorizationErrorResponse( error="access_denied", error_description=exception.message) satosa_logging(LOGGER, logging.DEBUG, exception.message, exception.state) return SeeOther( error_resp.request(auth_req["redirect_uri"], self._should_fragment_encode(auth_req)))
def _register_client(self, context): http_authz = context.wsgi_environ.get("HTTP_AUTHORIZATION") try: post_body = get_post(context.wsgi_environ) http_resp = self.OP.register_client(http_authz, post_body) except OIDCFederationError as e: satosa_logging(LOGGER, logging.ERROR, "OIDCFederation frontend error: {}".format(str(e)), context.state) return Response(str(e)) if not isinstance(http_resp, Created): return http_resp return self._fixup_registration_response(http_resp)
def _metadata_endpoint(self, context): """ Endpoint for retrieving the backend metadata :type context: satosa.context.Context :rtype: satosa.response.Response :param context: The current context :return: response with metadata """ satosa_logging(logger, logging.DEBUG, "Sending metadata response", context.state) metadata_string = create_metadata_string(None, self.sp.config, 4, None, None, None, None, None).decode("utf-8") return Response(metadata_string, content="text/xml")
def process(self, context: Context, data: InternalResponse) -> InternalResponse: """ :param context: The current context :param data: Internal response from backend """ # Send stat to service try: ticket = self._get_ticket() self._register(data.to_requestor, data.auth_info.issuer, ticket) except requests.ConnectionError as e: satosa_logging(LOGGER, logging.ERROR, "Could not connect to the statistics service '{}'".format(self.stat_uri), context.state) except Exception as e: satosa_logging(LOGGER, logging.ERROR, "Could not connect to the statistics service '{}'".format(self.stat_uri), context.state, exc_info=True) return data
def backend_routing(self, context): """ Returns the targeted backend and an updated state :type context: satosa.context.Context :rtype satosa.backends.base.BackendModule :param context: The request context :return: backend """ state = context.state satosa_logging(LOGGER, logging.INFO, "Routing to backend: %s " % context.target_backend, state) backend = self.backends[context.target_backend]["instance"] state.add(ModuleRouter.STATE_KEY, context.target_frontend) return backend
def endpoint_routing(self, context): """ Finds and returns the endpoint function bound to the path :type context: satosa.context.Context :rtype: ((satosa.context.Context, Any) -> Any, Any) :param context: The request context :return: registered endpoint and bound parameters """ satosa_logging(LOGGER, logging.DEBUG, "Routing path: %s" % context.path, context.state) self._validate_context(context) path_split = context.path.split('/') backend = path_split[0] if backend in self.backends: context.target_backend = backend # Search for frontend endpoint for frontend in self.frontends.keys(): for regex, spec in self.frontends[frontend]["endpoints"]: match = re.search(regex, context.path) if match is not None: context.target_frontend = frontend msg = "Frontend request. Module name:'{name}', endpoint: {endpoint}".format( name=frontend, endpoint=context.path) satosa_logging(LOGGER, logging.INFO, msg, context.state) return spec if backend in self.backends: # Search for backend endpoint for regex, spec in self.backends[backend]["endpoints"]: match = re.search(regex, context.path) if match is not None: msg = "Backend request. Module name:'{name}', endpoint: {endpoint}".format( name=backend, endpoint=context.path) satosa_logging(LOGGER, logging.INFO, msg, context.state) return spec satosa_logging(LOGGER, logging.DEBUG, "%s not bound to any function" % context.path, context.state) else: satosa_logging(LOGGER, logging.DEBUG, "Unknown backend %s" % backend, context.state) raise SATOSANoBoundEndpointError("'{}' not bound to any function".format(context.path))
def frontend_routing(self, context): """ Returns the targeted frontend and original state :type context: satosa.context.Context :rtype satosa.frontends.base.FrontendModule :param context: The response context :return: frontend """ state = context.state target_frontend = state.get(ModuleRouter.STATE_KEY) satosa_logging(LOGGER, logging.INFO, "Routing to frontend: %s " % target_frontend, state) context.target_frontend = target_frontend frontend = self.frontends[context.target_frontend]["instance"] return frontend
def _validate_context(self, context): """ Validates values in the context needed by the ModuleRouter. Raises BadContextError if any error. :type context: satosa.context.Context :rtype: None :param context: The request context :return: None """ if not context: satosa_logging(LOGGER, logging.DEBUG, "Context was None!", context.state) raise SATOSABadContextError("Context is None") if context.path is None: satosa_logging(LOGGER, logging.DEBUG, "Context did not contain a path!", context.state) raise SATOSABadContextError("Context did not contain any path")
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 handle_authn_response(self, context, internal_resp): """ See super class method satosa.frontends.base.FrontendModule#handle_authn_response :type context: satosa.context.Context :type internal_response: satosa.internal_data.InternalResponse :rtype oic.utils.http_util.Response """ auth_req = self._get_authn_request_from_state(context.state) # filter attributes to return in ID Token as claims attributes = self.converter.from_internal("openid", internal_resp.get_attributes()) satosa_logging(LOGGER, logging.DEBUG, "Attributes delivered by backend to OIDC frontend: {}".format( json.dumps(attributes)), context.state) flattened_attributes = {k: v[0] for k, v in attributes.items()} requested_id_token_claims = auth_req.get("claims", {}).get("id_token") user_claims = self._get_user_info(flattened_attributes, requested_id_token_claims, auth_req["scope"]) satosa_logging(LOGGER, logging.DEBUG, "Attributes filtered by requested claims/scope: {}".format( json.dumps(user_claims)), context.state) # construct epoch timestamp of reported authentication time auth_time = datetime.datetime.strptime(internal_resp.auth_info.timestamp, "%Y-%m-%dT%H:%M:%SZ") epoch_timestamp = (auth_time - datetime.datetime(1970, 1, 1)).total_seconds() base_claims = {"client_id": auth_req["client_id"], "sub": internal_resp.get_user_id(), "nonce": auth_req["nonce"]} id_token = self.provider.id_token_as_signed_jwt(base_claims, user_info=user_claims, auth_time=epoch_timestamp, loa="", alg=self.sign_alg) oidc_client_state = auth_req.get("state") kwargs = {} if oidc_client_state: # inlcude any optional 'state' sent by the client in the authn req kwargs["state"] = oidc_client_state auth_resp = AuthorizationResponse(id_token=id_token, **kwargs) http_response = auth_resp.request(auth_req["redirect_uri"], self._should_fragment_encode(auth_req)) return SeeOther(http_response)
def _approve_new_id(self, context, internal_response, ticket): """ Redirect the user to approve the new id :type context: satosa.context.Context :type internal_response: satosa.internal_data.InternalResponse :type ticket: str :rtype: satosa.response.Redirect :param context: The current context :param internal_response: The internal response :param ticket: The ticket given by the al service :return: A redirect to approve the new id linking """ satosa_logging(LOGGER, logging.INFO, "A new ID must be linked by the AL service", context.state) context.state.add(AccountLinkingModule.STATE_KEY, internal_response.to_dict()) return Redirect("%s/%s" % (self.al_redirect, ticket))
def _translate_response(self, response, state): """ Translates a saml authorization response to an internal response :type response: saml2.response.AuthnResponse :rtype: satosa.internal.InternalData :param response: The saml authorization response :return: A translated internal response """ # The response may have been encrypted by the IdP so if we have an # encryption key, try it. if self.encryption_keys: response.parse_assertion(self.encryption_keys) authn_info = response.authn_info()[0] auth_class_ref = authn_info[0] timestamp = response.assertion.authn_statement[0].authn_instant issuer = response.response.issuer.text auth_info = AuthenticationInformation( auth_class_ref, timestamp, issuer, ) # The SAML response may not include a NameID. subject = response.get_subject() name_id = subject.text if subject else None name_id_format = subject.format if subject else None attributes = self.converter.to_internal( self.attribute_profile, response.ava, ) internal_resp = InternalData( auth_info=auth_info, attributes=attributes, subject_type=name_id_format, subject_id=name_id, ) satosa_logging(logger, logging.DEBUG, "backend received attributes:\n%s" % json.dumps(response.ava, indent=4), state) 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 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)