From d8ba6452ec8e62f62264199305f2d7d1c6190276 Mon Sep 17 00:00:00 2001 From: rhoerbe Date: Wed, 16 Oct 2019 21:55:52 +0300 Subject: [PATCH 1/7] add WKIS micro services --- src/satosa/micro_services/local_store.py | 26 +++ src/satosa/micro_services/redirect_url.py | 91 +++++++++++ src/satosa/micro_services/simpleconsent.py | 179 +++++++++++++++++++++ 3 files changed, 296 insertions(+) create mode 100644 src/satosa/micro_services/local_store.py create mode 100644 src/satosa/micro_services/redirect_url.py create mode 100644 src/satosa/micro_services/simpleconsent.py diff --git a/src/satosa/micro_services/local_store.py b/src/satosa/micro_services/local_store.py new file mode 100644 index 000000000..412bc73ec --- /dev/null +++ b/src/satosa/micro_services/local_store.py @@ -0,0 +1,26 @@ +""" implement key/valuer store """ +import pickle +import redis +from satosa.state import _AESCipher + +class LocalStore(): + """ Store context objects in Redis. + Create a new key when a new value is set. + Delete key/value after reading it + """ + def __init__(self, encryption_key: str, redishost: str): + self.redis = redis.Redis(host=redishost, port=6379) + self.aes_cipher = _AESCipher(encryption_key) + + def set(self, context: object) -> int: + context_serlzd = pickle.dumps(context, pickle.HIGHEST_PROTOCOL) + context_enc = self.aes_cipher.encrypt(context_serlzd) + key = self.redis.incr('REDIRURL_sequence', 1) + self.redis.set(key, context_serlzd, 1800) # generous 30 min timeout to complete SSO transaction + return key + + def get(self, key: int) -> object: + context_serlzd = self.redis.get(key) + self.redis.expire(key, 600) # delay deletion in case request is repeated due to network issues + return pickle.loads(context_serlzd) + diff --git a/src/satosa/micro_services/redirect_url.py b/src/satosa/micro_services/redirect_url.py new file mode 100644 index 000000000..d16b7da27 --- /dev/null +++ b/src/satosa/micro_services/redirect_url.py @@ -0,0 +1,91 @@ +""" +ADFS/SAML-Support for role selection and profile completion after a SAML-Response +was issued using a redirect-to-idp flow. +* Store AuthnRequest for later replay +* Handle redirect-to-idp and replay AuthnRequest after redirect-to-idp flow + +Persist state: Storing the the full context of the AuthnRequest in SATOSA_STATE is not feasible due to cookie size limitations. +Instead, it is stored in a local redis store, and the key is stored in SATOSA_STATE. + +The Redis interface is using a basic implementation creating a connection pool and TCP sockets for each call, which is OK for the modest deployment. +(Instantiating a global connection pool across gunicorn worker threads would impose some additional complexity.) +The AuthnRequest is stored unencrypted with the assumption that a stolen request cannot do harm, +because the final Response will only be delivered to the metadata-specified ACS endpoint. + + +""" + +import logging +import sys +from typing import Tuple +import satosa +from .base import RequestMicroService, ResponseMicroService +from satosa.micro_services.local_store import LocalStore + +MIN_PYTHON = (3, 6) +if sys.version_info < MIN_PYTHON: + sys.exit("Python %s.%s or later is required.\n" % MIN_PYTHON) + +STATE_KEY = "REDIRURLCONTEXT" + + +class RedirectUrlRequest(RequestMicroService): + """ Store AuthnRequest in SATOSA STATE in case it is required later for the RedirectUrl flow """ + def __init__(self, config: dict, *args, **kwargs): + super().__init__(*args, **kwargs) + self.local_store = LocalStore(config['db_encryption_key'], redishost=config.get('redis_host', 'localhost')) + logging.info('RedirectUrlRequest microservice active') + + def process(self, context: satosa.context.Context, internal_request: satosa.internal.InternalData) \ + -> Tuple[satosa.context.Context, satosa.internal.InternalData]: + key = self.local_store.set(context) + context.state[STATE_KEY] = str(key) + logging.debug(f"RedirectUrlRequest: store context (stub)") + return super().process(context, internal_request) + + +class RedirectUrlResponse(ResponseMicroService): + """ + Handle following events: + * Processing a SAML Response: + if the redirectUrl attribute is set in the response/attribute statement: + Redirect to responder + * Processing a RedirectUrlResponse: + Retrieve previously saved AuthnRequest + Replay AuthnRequest + """ + def __init__(self, config: dict, *args, **kwargs): + super().__init__(*args, **kwargs) + self.endpoint = 'redirecturl_response' + self.self_entityid = config['self_entityid'] + self.redir_attr = config['redirect_attr_name'] + self.redir_entityid = config['redir_entityid'] + self.local_store = LocalStore(config['db_encryption_key'], redishost=config.get('redis_host', 'localhost')) + logging.info('RedirectUrlResponse microservice active') + + def _handle_redirecturl_response( + self, + context: satosa.context.Context, + wsgi_app: callable(satosa.context.Context)) -> satosa.response.Response: + logging.debug(f"RedirectUrl microservice: RedirectUrl processing complete") + key = int(context.state[STATE_KEY]) + authnrequ_context = self.local_store.get(key) + resp = wsgi_app.run(authnrequ_context) + return resp + + def process(self, context: satosa.context.Context, + internal_response: satosa.internal.InternalData) -> satosa.response.Response: + if self.redir_attr in internal_response.attributes: + logging.debug(f"RedirectUrl microservice: Attribute {self.redir_attr} found, starting redirect") + redirecturl = internal_response.attributes[self.redir_attr][0] + '?wtrealm=' + self.self_entityid + return satosa.response.Redirect(redirecturl) + else: + logging.debug(f"RedirectUrl microservice: Attribute {self.redir_attr} not found") + return super().process(context, internal_response) + + def register_endpoints(self): + return [("^{}$".format(self.endpoint), self._handle_redirecturl_response), ] + + +if sys.version_info < (3, 6): + raise Exception("Must be using Python 3.6 or later") diff --git a/src/satosa/micro_services/simpleconsent.py b/src/satosa/micro_services/simpleconsent.py new file mode 100644 index 000000000..9df73688f --- /dev/null +++ b/src/satosa/micro_services/simpleconsent.py @@ -0,0 +1,179 @@ +""" +Integrate the "simple consent" application into SATOSA + +Logic: + 1. verify consent (API call) + 2. continue with response if true + 3. request consent (redirect to consent app) + 4. (consent service app will redirect to _handle_consent_response) + 5. verify consent (API call) + 6. delete attributes if no consent + 7. continue with response + +""" +import base64 +import hashlib +import hmac +import json +import logging +import pickle +import sys +import urllib.parse + +import requests +from requests.exceptions import ConnectionError + +import satosa +from satosa.internal import InternalData +from satosa.logging_util import satosa_logging +from satosa.micro_services.base import ResponseMicroService +from satosa.response import Redirect + +logger = logging.getLogger(__name__) + +RESPONSE_STATE = "Saml2IDP" +CONSENT_ID = "SimpleConsent" +CONSENT_INT_DATA = 'simpleconsent.internaldata' + + +class UnexpectedResponseError(Exception): + pass + + +class SimpleConsent(ResponseMicroService): + def __init__(self, config: dict, *args, **kwargs): + super().__init__(*args, **kwargs) + self.consent_attrname_display = config['consent_attrname_display'] + self.consent_attr_not_displayed = config['consent_attr_not_displayed'] + self.consent_cookie_name = config['consent_cookie_name'] + self.consent_service_api_auth = config['consent_service_api_auth'] + self.endpoint = "simpleconsent_response" + self.id_hash_alg = config['id_hash_alg'] + self.name = "simpleconsent" + self.proxy_hmac_key = config['PROXY_HMAC_KEY'].encode('ascii') + self.request_consent_url = config['request_consent_url'] + self.self_entityid = config['self_entityid'] + self.sp_entityid_names: dict = config['sp_entityid_names'] + self.verify_consent_url = config['verify_consent_url'] + logging.info('SimpleConsent microservice active') + + def _end_consent_flow(self, context: satosa.context.Context, + internal_response: satosa.internal.InternalData) -> satosa.response.Response: + del context.state[CONSENT_ID] + return super().process(context, internal_response) + + def _handle_consent_response( + self, + context: satosa.context.Context, + wsgi_app: callable(satosa.context.Context)) -> satosa.response.Response: + + logging.debug(f"SimpleConsent microservice: resuming response processing after requesting consent") + internal_resp_ser = base64.b64decode(context.state[CONSENT_INT_DATA].encode('ascii')) + internal_response = pickle.loads(internal_resp_ser) + consent_id = context.state[CONSENT_ID] + + try: + consent_given = self._verify_consent(internal_response.requester, consent_id) + except ConnectionError: + satosa_logging(logger, logging.ERROR, + "Consent service is not reachable, no consent given.", context.state) + internal_response.attributes = {} + + if consent_given: + satosa_logging(logger, logging.INFO, "Consent was given", context.state) + else: + satosa_logging(logger, logging.INFO, "Consent was NOT given, removing attributes", context.state) + internal_response.attributes = {} + + return self._end_consent_flow(context, internal_response) + + def _get_consent_id(self, user_id: str, attr_set: dict) -> str: + # include attributes in id_hash to ensure that consent is invalid if the attribute set changes + attr_key_list = sorted(attr_set.keys()) + consent_id_json = json.dumps([user_id, attr_key_list]) + if self.id_hash_alg == 'md5': + consent_id_hash = hashlib.md5(consent_id_json.encode('utf-8')) + elif self.id_hash_alg == 'sha224': + consent_id_hash = hashlib.sha224(consent_id_json.encode('utf-8')) + else: + raise Exception("Simpleconsent.config.id_hash_alg must be in ('md5', 'sha224')") + return consent_id_hash.hexdigest() + + def process(self, context: satosa.context.Context, + internal_resp: satosa.internal.InternalData) -> satosa.response.Response: + + response_state = context.state[RESPONSE_STATE] + consent_id = self._get_consent_id(internal_resp.subject_id, internal_resp.attributes) + context.state[CONSENT_ID] = consent_id + logging.debug(f"SimpleConsent microservice: verify consent, id={consent_id}") + try: + # Check if consent is already given + consent_given = self._verify_consent(internal_resp.requester, consent_id) + except requests.exceptions.ConnectionError: + satosa_logging(logger, logging.ERROR, + f"Consent service is not reachable at {self.verify_consent_url}, no consent given.", + context.state) + # Send an internal_resp without any attributes + internal_resp.attributes = {} + return self._end_consent_flow(context, internal_resp) + + if consent_given: + satosa_logging(logger, logging.DEBUG, "SimpleConsent microservice: previous consent found", context.state) + return self._end_consent_flow(context, internal_resp) # return attribute set unmodified + else: + logging.debug(f"SimpleConsent microservice: starting redirect to request consent") + # save internal response + internal_resp_ser = pickle.dumps(internal_resp) + context.state[CONSENT_INT_DATA] = base64.b64encode(internal_resp_ser).decode('ascii') + # create request object & redirect + consent_requ_json = self._make_consent_request(response_state, consent_id, internal_resp.attributes) + hmac_str = hmac.new(self.proxy_hmac_key, consent_requ_json.encode('utf-8'), hashlib.sha256).hexdigest() + consent_requ_b64 = base64.urlsafe_b64encode(consent_requ_json.encode('ascii')).decode('ascii') + redirecturl = f"{self.request_consent_url}/{urllib.parse.quote_plus(consent_requ_b64)}/{hmac_str}/" + return satosa.response.Redirect(redirecturl) + + return super().process(context, internal_resp) + + def _make_consent_request(self, response_state: dict, consent_id: str, attr: list) -> dict: + display_attr: set = set.difference(set(attr), set(self.consent_attr_not_displayed)) + for attr_name, attr_name_translated in self.consent_attrname_display.items(): + if attr_name in display_attr: + display_attr.discard(attr_name) + display_attr.add(attr_name_translated) + displayname = attr['displayname'][0] if attr['displayname'] else '' + entityid = response_state['resp_args']['sp_entity_id'] + sp_name = self.sp_entityid_names.get(entityid, entityid) + uid = attr['mail'][0] if attr['mail'] else '' + + consent_requ_dict = { + "entityid": entityid, + "consentid": consent_id, + "displayname": displayname, + "mail": uid, + "sp": sp_name, + "attr_list": sorted(list(display_attr)), + } + consent_requ_json = json.dumps(consent_requ_dict) + return consent_requ_json + + def register_endpoints(self) -> list: + return [("^{}$".format(self.endpoint), self._handle_consent_response), ] + + def _verify_consent(self, requester, consent_id: str) -> bool: + requester_b64 = base64.urlsafe_b64encode(requester.encode('ascii')).decode('ascii') + url = f"{self.verify_consent_url}/{requester_b64}/{consent_id}/" + try: + api_cred = (self.consent_service_api_auth['userid'], + self.consent_service_api_auth['password']) + response = requests.request(method='GET', url=url, auth=(api_cred)) + if response.status_code == 200: + return json.loads(response.text) + else: + raise ConnectionError(f"GET {url} returned status code {response.status_code}") + except requests.exceptions.ConnectionError as e: + logger.debug(f"GET {url} {str(e)}") + raise + + +if sys.version_info < (3, 6): + raise Exception("SimpleConsent microservice requires Python 3.6 or later") From 5d7ef4f58b3427d5fcf9b9ba96d028e7f9461fac Mon Sep 17 00:00:00 2001 From: rhoerbe Date: Thu, 17 Oct 2019 21:06:39 +0300 Subject: [PATCH 2/7] enable microservice-based flow to replay AuthnRequest (requires wsgi_app in registered callback) --- src/satosa/backends/base.py | 2 +- src/satosa/backends/github.py | 4 +-- src/satosa/backends/linkedin.py | 4 +-- src/satosa/backends/oauth.py | 4 +-- src/satosa/backends/openid_connect.py | 4 +-- src/satosa/backends/orcid.py | 2 +- src/satosa/backends/saml2.py | 25 ++++++++++--------- src/satosa/base.py | 2 +- src/satosa/frontends/base.py | 2 +- src/satosa/frontends/openid_connect.py | 16 ++++++------ src/satosa/frontends/ping.py | 4 +-- src/satosa/frontends/saml2.py | 24 +++++++++--------- src/satosa/micro_services/account_linking.py | 4 +-- .../micro_services/attribute_authorization.py | 2 +- src/satosa/micro_services/consent.py | 4 +-- src/satosa/micro_services/redirect_url.py | 3 ++- 16 files changed, 54 insertions(+), 52 deletions(-) diff --git a/src/satosa/backends/base.py b/src/satosa/backends/base.py index 8d0432da8..cdb4bf8c7 100644 --- a/src/satosa/backends/base.py +++ b/src/satosa/backends/base.py @@ -32,7 +32,7 @@ def __init__(self, auth_callback_func, internal_attributes, base_url, name): self.base_url = base_url self.name = name - def start_auth(self, context, internal_request): + def start_auth(self, context, internal_request, **kwargs): """ This is the start up function of the backend authorization. diff --git a/src/satosa/backends/github.py b/src/satosa/backends/github.py index 1da9dadbe..48b0a1dc0 100644 --- a/src/satosa/backends/github.py +++ b/src/satosa/backends/github.py @@ -45,7 +45,7 @@ def __init__(self, outgoing, internal_attributes, config, base_url, name): outgoing, internal_attributes, config, base_url, name, 'github', 'id') - def start_auth(self, context, internal_request, get_state=stateID): + def start_auth(self, context, internal_request, get_state=stateID, **kwargs): """ :param get_state: Generates a state to be used in authentication call @@ -75,7 +75,7 @@ def auth_info(self, requrest): UNSPECIFIED, None, self.config['server_info']['authorization_endpoint']) - def _authn_response(self, context): + def _authn_response(self, context, **kwargs): state_data = context.state[self.name] aresp = self.consumer.parse_response( AuthorizationResponse, info=json.dumps(context.request)) diff --git a/src/satosa/backends/linkedin.py b/src/satosa/backends/linkedin.py index cbaa2ea39..ea8709205 100644 --- a/src/satosa/backends/linkedin.py +++ b/src/satosa/backends/linkedin.py @@ -46,7 +46,7 @@ def __init__(self, outgoing, internal_attributes, config, base_url, name): outgoing, internal_attributes, config, base_url, name, 'linkedin', 'id') - def start_auth(self, context, internal_request, get_state=stateID): + def start_auth(self, context, internal_request, get_state=stateID, **kwargs): """ :param get_state: Generates a state to be used in authentication call @@ -76,7 +76,7 @@ def auth_info(self, requrest): UNSPECIFIED, None, self.config['server_info']['authorization_endpoint']) - def _authn_response(self, context): + def _authn_response(self, context, **kwargs): state_data = context.state[self.name] aresp = self.consumer.parse_response( AuthorizationResponse, info=json.dumps(context.request)) diff --git a/src/satosa/backends/oauth.py b/src/satosa/backends/oauth.py index 9136ce6d4..f641ab4a8 100644 --- a/src/satosa/backends/oauth.py +++ b/src/satosa/backends/oauth.py @@ -64,7 +64,7 @@ def __init__(self, outgoing, internal_attributes, config, base_url, name, extern response_type=self.config["response_type"]) self.consumer.client_secret = self.config["client_secret"] - def start_auth(self, context, internal_request, get_state=stateID): + def start_auth(self, context, internal_request, get_state=stateID, **kwargs): """ See super class method satosa.backends.base#start_auth :param get_state: Generates a state to be used in the authentication call. @@ -118,7 +118,7 @@ def _verify_state(self, resp, state_data, state): "Missing or invalid state [%s] in response!" % received_state) - def _authn_response(self, context): + def _authn_response(self, context, **kwargs): """ Handles the authentication response from the AS. diff --git a/src/satosa/backends/openid_connect.py b/src/satosa/backends/openid_connect.py index 60a17d7a3..b36a29420 100644 --- a/src/satosa/backends/openid_connect.py +++ b/src/satosa/backends/openid_connect.py @@ -64,7 +64,7 @@ def __init__(self, auth_callback_func, internal_attributes, config, base_url, na if "response_type" not in config["client"]["auth_req_params"]: config["auth_req_params"]["response_type"] = "code" - def start_auth(self, context, request_info): + def start_auth(self, context, request_info, **kwargs): """ See super class method satosa.backends.base#start_auth :type context: satosa.context.Context @@ -167,7 +167,7 @@ def _get_userinfo(self, state, context): self._check_error_response(userinfo_resp, context) return userinfo_resp.to_dict() - def response_endpoint(self, context, *args): + def response_endpoint(self, context, *args, **kwargs): """ Handles the authentication response from the OP. :type context: satosa.context.Context diff --git a/src/satosa/backends/orcid.py b/src/satosa/backends/orcid.py index aaa18b7e5..3582bce5d 100644 --- a/src/satosa/backends/orcid.py +++ b/src/satosa/backends/orcid.py @@ -60,7 +60,7 @@ def auth_info(self, requrest): UNSPECIFIED, None, self.config['server_info']['authorization_endpoint']) - def _authn_response(self, context): + def _authn_response(self, context, **kwargs): state_data = context.state[self.name] aresp = self.consumer.parse_response( AuthorizationResponse, info=json.dumps(context.request)) diff --git a/src/satosa/backends/saml2.py b/src/satosa/backends/saml2.py index d942817ac..63ca53f29 100644 --- a/src/satosa/backends/saml2.py +++ b/src/satosa/backends/saml2.py @@ -129,7 +129,7 @@ def __init__(self, outgoing, internal_attributes, config, base_url, name): with open(p) as key_file: self.encryption_keys.append(key_file.read()) - def get_idp_entity_id(self, context): + def get_idp_entity_id(self, context, **kwargs): """ :type context: satosa.context.Context :rtype: str | None @@ -164,7 +164,7 @@ def get_idp_entity_id(self, context): ) return entity_id - def start_auth(self, context, internal_req): + def start_auth(self, context, internal_req, **kwargs): """ See super class method satosa.backends.base.BackendModule#start_auth @@ -184,7 +184,7 @@ def start_auth(self, context, internal_req): return self.authn_request(context, entity_id) - def disco_query(self, context): + def disco_query(self, context, **kwargs): """ Makes a request to the discovery server @@ -236,7 +236,7 @@ def construct_requested_authn_context(self, entity_id): return authn_context - def authn_request(self, context, entity_id): + def authn_request(self, context, entity_id, **kwargs): """ Do an authorization request on idp with given entity id. This is the start of the authorization. @@ -294,7 +294,7 @@ def authn_request(self, context, entity_id): context.state[self.name] = {"relay_state": relay_state} return make_saml_response(binding, ht_args) - def authn_response(self, context, binding): + def authn_response(self, context, binding, **kwargs): """ Endpoint for the idp response :type context: satosa.context,Context @@ -326,11 +326,12 @@ def authn_response(self, context, binding): 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") + # if the response relay_state exists it must match that from the request + if self.name in context.state and "relay_state" in context.state[self.name]: + if context.state[self.name]["relay_state"] != context.request["RelayState"]: + logger.debug("State did not match relay state for state", extra={'state': context.state}) + raise SATOSAAuthenticationError(context.state, "State did not match relay state") + del context.state[self.name] context.decorate(Context.KEY_BACKEND_METADATA_STORE, self.sp.metadata) if self.config.get(SAMLBackend.KEY_MEMORIZE_IDP): @@ -340,7 +341,7 @@ def authn_response(self, context, binding): context.state.pop(Context.KEY_FORCE_AUTHN, None) return self.auth_callback_func(context, self._translate_response(authn_response, context.state)) - def disco_response(self, context): + def disco_response(self, context, **kwargs): """ Endpoint for the discovery server response @@ -406,7 +407,7 @@ def _translate_response(self, response, state): json.dumps(response.ava, indent=4), state) return internal_resp - def _metadata_endpoint(self, context): + def _metadata_endpoint(self, context, **kwargs): """ Endpoint for retrieving the backend metadata :type context: satosa.context.Context diff --git a/src/satosa/base.py b/src/satosa/base.py index ae041ab0e..856d8f17a 100644 --- a/src/satosa/base.py +++ b/src/satosa/base.py @@ -226,7 +226,7 @@ def _run_bound_endpoint(self, context, spec): :return: response """ try: - return spec(context) + return spec(context, wsgi_app=self) except SATOSAAuthenticationError as error: error.error_id = uuid.uuid4().urn state = json.dumps(error.state.state_dict, indent=4) diff --git a/src/satosa/frontends/base.py b/src/satosa/frontends/base.py index 52840a85c..a21ce4274 100644 --- a/src/satosa/frontends/base.py +++ b/src/satosa/frontends/base.py @@ -26,7 +26,7 @@ def __init__(self, auth_req_callback_func, internal_attributes, base_url, name): self.base_url = base_url self.name = name - def handle_authn_response(self, context, internal_resp): + def handle_authn_response(self, context, internal_resp, **kwargs): """ If an authorization has been successful in a backend, this function is called and is supposed to send an authorization response to the client. diff --git a/src/satosa/frontends/openid_connect.py b/src/satosa/frontends/openid_connect.py index bd5972511..0516a09c8 100644 --- a/src/satosa/frontends/openid_connect.py +++ b/src/satosa/frontends/openid_connect.py @@ -117,7 +117,7 @@ def _init_authorization_state(self): return AuthorizationState(HashBasedSubjectIdentifierFactory(sub_hash_salt), authz_code_db, access_token_db, refresh_token_db, sub_db, **token_lifetimes) - def handle_authn_response(self, context, internal_resp, extra_id_token_claims=None): + def handle_authn_response(self, context, internal_resp, extra_id_token_claims=None, **kwargs): """ See super class method satosa.frontends.base.FrontendModule#handle_authn_response :type context: satosa.context.Context @@ -232,7 +232,7 @@ def _get_authn_request_from_state(self, state): """ return AuthorizationRequest().deserialize(state[self.name]["oidc_request"]) - def client_registration(self, context): + def client_registration(self, context, **kwargs): """ Handle the OIDC dynamic client registration. :type context: satosa.context.Context @@ -247,7 +247,7 @@ def client_registration(self, context): except InvalidClientRegistrationRequest as e: return BadRequest(e.to_json(), content="application/json") - def provider_config(self, context): + def provider_config(self, context, **kwargs): """ Construct the provider configuration information (served at /.well-known/openid-configuration). :type context: satosa.context.Context @@ -270,7 +270,7 @@ def _get_approved_attributes(self, provider_supported_claims, authn_req): requested_claims.extend(authn_req["claims"][k].keys()) return set(provider_supported_claims).intersection(set(requested_claims)) - def _handle_authn_request(self, context): + def _handle_authn_request(self, context, **kwargs): """ Parse and verify the authentication request into an internal request. :type context: satosa.context.Context @@ -315,7 +315,7 @@ def _handle_authn_request(self, context): authn_req)) return internal_req - def handle_authn_request(self, context): + def handle_authn_request(self, context, **kwargs): """ Handle an authentication request and pass it on to the backend. :type context: satosa.context.Context @@ -329,7 +329,7 @@ def handle_authn_request(self, context): return internal_req return self.auth_req_callback_func(context, internal_req) - def jwks(self, context): + def jwks(self, context, **kwargs): """ Construct the JWKS document (served at /jwks). :type context: satosa.context.Context @@ -340,7 +340,7 @@ def jwks(self, context): """ return Response(json.dumps(self.provider.jwks), content="application/json") - def token_endpoint(self, context): + def token_endpoint(self, context, **kwargs): """ Handle token requests (served at /token). :type context: satosa.context.Context @@ -364,7 +364,7 @@ def token_endpoint(self, context): error_resp = TokenErrorResponse(error=e.oauth_error, error_description=str(e)) return BadRequest(error_resp.to_json(), content="application/json") - def userinfo_endpoint(self, context): + def userinfo_endpoint(self, context, **kwargs): headers = {"Authorization": context.request_authorization} try: diff --git a/src/satosa/frontends/ping.py b/src/satosa/frontends/ping.py index d0381210a..f9a2ed791 100644 --- a/src/satosa/frontends/ping.py +++ b/src/satosa/frontends/ping.py @@ -19,7 +19,7 @@ def __init__(self, auth_req_callback_func, internal_attributes, config, base_url self.config = config - def handle_authn_response(self, context, internal_resp, extra_id_token_claims=None): + def handle_authn_response(self, context, internal_resp, extra_id_token_claims=None, **kwargs): """ See super class method satosa.frontends.base.FrontendModule#handle_authn_response :type context: satosa.context.Context @@ -47,7 +47,7 @@ def register_endpoints(self, backend_names): return url_map - def ping_endpoint(self, context): + def ping_endpoint(self, context, **kwargs): """ """ logprefix = PingFrontend.logprefix diff --git a/src/satosa/frontends/saml2.py b/src/satosa/frontends/saml2.py index b65003054..3746c75ab 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -78,7 +78,7 @@ def __init__(self, auth_req_callback_func, internal_attributes, config, base_url self.KEY_CUSTOM_ATTR_RELEASE) self.idp = None - def handle_authn_response(self, context, internal_response): + def handle_authn_response(self, context, internal_response, **kwargs): """ See super class method satosa.frontends.base.FrontendModule#handle_authn_response :type context: satosa.context.Context @@ -87,7 +87,7 @@ def handle_authn_response(self, context, internal_response): """ return self._handle_authn_response(context, internal_response, self.idp) - def handle_authn_request(self, context, binding_in): + def handle_authn_request(self, context, binding_in, **kwargs): """ This method is bound to the starting endpoint of the authentication. @@ -174,7 +174,7 @@ def _validate_config(self, config): except KeyError as e: raise ValueError("Missing configuration key: %s" % key) from e - def _handle_authn_request(self, context, binding_in, idp): + def _handle_authn_request(self, context, binding_in, idp, **kwargs): """ See doc for handle_authn_request method. @@ -286,7 +286,7 @@ def _filter_attributes(self, idp, internal_response, context,): return attributes - def _handle_authn_response(self, context, internal_response, idp): + def _handle_authn_response(self, context, internal_response, idp, **kwargs): """ See super class satosa.frontends.base.FrontendModule @@ -425,7 +425,7 @@ def _handle_backend_error(self, exception, idp): satosa_logging(logger, logging.DEBUG, "HTTPargs: %s" % http_args, exception.state) return make_saml_response(resp_args["binding"], http_args) - def _metadata_endpoint(self, context): + def _metadata_endpoint(self, context, **kwargs): """ Endpoint for retrieving the backend metadata :type context: satosa.context.Context @@ -589,7 +589,7 @@ def _load_endpoints_to_config(self, provider, target_entity_id, config=None): idp_conf["service"]["idp"]["endpoints"][service] = idp_endpoints return idp_conf - def _load_idp_dynamic_endpoints(self, context): + def _load_idp_dynamic_endpoints(self, context, **kwargs): """ Loads an idp server that accepts the target backend name in the endpoint url ex: //sso/redirect @@ -621,7 +621,7 @@ def _load_idp_dynamic_entity_id(self, state): idp_config = IdPConfig().load(idp_config_file, metadata_construction=False) return Server(config=idp_config) - def handle_authn_request(self, context, binding_in): + def handle_authn_request(self, context, binding_in, **kwargs): """ Loads approved endpoints dynamically See super class satosa.frontends.saml2.SAMLFrontend#handle_authn_request @@ -637,7 +637,7 @@ def handle_authn_request(self, context, binding_in): idp = self._load_idp_dynamic_endpoints(context) return self._handle_authn_request(context, binding_in, idp) - def _create_state_data(self, context, resp_args, relay_state): + def _create_state_data(self, context, resp_args, relay_state, **kwargs): """ Adds the frontend idp entity id to state See super class satosa.frontends.saml2.SAMLFrontend#save_state @@ -661,7 +661,7 @@ def handle_backend_error(self, exception): idp = self._load_idp_dynamic_entity_id(exception.state) return self._handle_backend_error(exception, idp) - def handle_authn_response(self, context, internal_response): + def handle_authn_response(self, context, internal_response, **kwargs): """ See super class satosa.frontends.base.FrontendModule#handle_authn_response :param context: @@ -708,7 +708,7 @@ class SAMLVirtualCoFrontend(SAMLFrontend): KEY_ORGANIZATION = 'organization' KEY_ORGANIZATION_KEYS = ['display_name', 'name', 'url'] - def handle_authn_request(self, context, binding_in): + def handle_authn_request(self, context, binding_in, **kwargs): """ See super class satosa.frontends.saml2.SAMLFrontend#handle_authn_request @@ -723,7 +723,7 @@ def handle_authn_request(self, context, binding_in): idp = self._create_co_virtual_idp(context) return self._handle_authn_request(context, binding_in, idp) - def handle_authn_response(self, context, internal_response): + def handle_authn_response(self, context, internal_response, **kwargs): """ See super class satosa.frontends.base. FrontendModule#handle_authn_response @@ -734,7 +734,7 @@ def handle_authn_response(self, context, internal_response): return self._handle_authn_response(context, internal_response) - def _handle_authn_response(self, context, internal_response): + def _handle_authn_response(self, context, internal_response, **kwargs): """ """ # Using the context of the current request and saved state from the diff --git a/src/satosa/micro_services/account_linking.py b/src/satosa/micro_services/account_linking.py index 0e849f067..3d36a5974 100644 --- a/src/satosa/micro_services/account_linking.py +++ b/src/satosa/micro_services/account_linking.py @@ -35,7 +35,7 @@ def __init__(self, config, *args, **kwargs): self.id_to_attr = config.get("id_to_attr", None) logger.info("Account linking is active") - def _handle_al_response(self, context): + def _handle_al_response(self, context, **kwargs): """ Endpoint for handling account linking service response. When getting here user might have approved or rejected linking their account @@ -108,7 +108,7 @@ def process(self, context, internal_response): context.state[self.name] = internal_response.to_dict() return Redirect("%s/%s" % (self.redirect_url, jws)) - def _get_uuid(self, context, issuer, id): + def _get_uuid(self, context, issuer, id, **kwargs): """ Ask the account linking service for a uuid. If the given issuer/id pair is not linked, then the function will return a ticket. diff --git a/src/satosa/micro_services/attribute_authorization.py b/src/satosa/micro_services/attribute_authorization.py index 1bcaf8cda..3d07ffaa7 100644 --- a/src/satosa/micro_services/attribute_authorization.py +++ b/src/satosa/micro_services/attribute_authorization.py @@ -48,7 +48,7 @@ def __init__(self, config, *args, **kwargs): self.attribute_allow = config.get("attribute_allow", {}) self.attribute_deny = config.get("attribute_deny", {}) - def _check_authz(self, context, attributes, requester, provider): + def _check_authz(self, context, attributes, requester, provider, **kwargs): for attribute_name, attribute_filters in get_dict_defaults(self.attribute_allow, requester, provider).items(): if attribute_name in attributes: if not any([any(filter(re.compile(af).search, attributes[attribute_name])) for af in attribute_filters]): diff --git a/src/satosa/micro_services/consent.py b/src/satosa/micro_services/consent.py index 7841c5993..53e3d3842 100644 --- a/src/satosa/micro_services/consent.py +++ b/src/satosa/micro_services/consent.py @@ -44,7 +44,7 @@ def __init__(self, config, internal_attributes, *args, **kwargs): self.endpoint = "/handle_consent" logger.info("Consent flow is active") - def _handle_consent_response(self, context): + def _handle_consent_response(self, context, **kwargs): """ Endpoint for handling consent service response :type context: satosa.context.Context @@ -78,7 +78,7 @@ def _handle_consent_response(self, context): internal_response.attributes = self._filter_attributes(internal_response.attributes, consent_attributes) return self._end_consent(context, internal_response) - def _approve_new_consent(self, context, internal_response, id_hash): + def _approve_new_consent(self, context, internal_response, id_hash, **kwargs): context.state[STATE_KEY]["internal_resp"] = internal_response.to_dict() consent_args = { diff --git a/src/satosa/micro_services/redirect_url.py b/src/satosa/micro_services/redirect_url.py index d16b7da27..b0d5ec797 100644 --- a/src/satosa/micro_services/redirect_url.py +++ b/src/satosa/micro_services/redirect_url.py @@ -19,7 +19,8 @@ import sys from typing import Tuple import satosa -from .base import RequestMicroService, ResponseMicroService +from satosa.internal import InternalData +from satosa.micro_services.base import RequestMicroService, ResponseMicroService from satosa.micro_services.local_store import LocalStore MIN_PYTHON = (3, 6) From 58e537ccd8f155a6bffb8a40c98a9ffef55f1770 Mon Sep 17 00:00:00 2001 From: rhoerbe Date: Thu, 17 Oct 2019 21:06:39 +0300 Subject: [PATCH 3/7] enable microservice-based flow to replay AuthnRequest (requires wsgi_app in registered callback) --- src/satosa/backends/base.py | 2 +- src/satosa/backends/github.py | 4 +- src/satosa/backends/linkedin.py | 4 +- src/satosa/backends/oauth.py | 4 +- src/satosa/backends/openid_connect.py | 4 +- src/satosa/backends/orcid.py | 2 +- src/satosa/backends/saml2.py | 25 ++++++------ src/satosa/base.py | 2 +- src/satosa/frontends/base.py | 2 +- src/satosa/frontends/openid_connect.py | 16 ++++---- src/satosa/frontends/ping.py | 4 +- src/satosa/frontends/saml2.py | 24 ++++++------ src/satosa/micro_services/account_linking.py | 4 +- .../micro_services/attribute_authorization.py | 2 +- src/satosa/micro_services/consent.py | 4 +- src/satosa/micro_services/redirect_url.py | 3 +- tests/satosa/backends/test_bitbucket.py | 9 +++-- tests/satosa/backends/test_oauth.py | 6 +-- tests/satosa/backends/test_openid_connect.py | 6 +-- tests/satosa/backends/test_orcid.py | 8 ++-- tests/satosa/backends/test_saml2.py | 38 +++++++++---------- tests/satosa/frontends/test_saml2.py | 37 ++++++++++-------- tests/satosa/micro_services/test_consent.py | 21 +++++----- .../micro_services/test_custom_routing.py | 2 +- tests/satosa/test_base.py | 8 ++-- tests/satosa/test_routing.py | 2 +- tests/util.py | 14 +++---- 27 files changed, 133 insertions(+), 124 deletions(-) diff --git a/src/satosa/backends/base.py b/src/satosa/backends/base.py index 8d0432da8..cdb4bf8c7 100644 --- a/src/satosa/backends/base.py +++ b/src/satosa/backends/base.py @@ -32,7 +32,7 @@ def __init__(self, auth_callback_func, internal_attributes, base_url, name): self.base_url = base_url self.name = name - def start_auth(self, context, internal_request): + def start_auth(self, context, internal_request, **kwargs): """ This is the start up function of the backend authorization. diff --git a/src/satosa/backends/github.py b/src/satosa/backends/github.py index 1da9dadbe..48b0a1dc0 100644 --- a/src/satosa/backends/github.py +++ b/src/satosa/backends/github.py @@ -45,7 +45,7 @@ def __init__(self, outgoing, internal_attributes, config, base_url, name): outgoing, internal_attributes, config, base_url, name, 'github', 'id') - def start_auth(self, context, internal_request, get_state=stateID): + def start_auth(self, context, internal_request, get_state=stateID, **kwargs): """ :param get_state: Generates a state to be used in authentication call @@ -75,7 +75,7 @@ def auth_info(self, requrest): UNSPECIFIED, None, self.config['server_info']['authorization_endpoint']) - def _authn_response(self, context): + def _authn_response(self, context, **kwargs): state_data = context.state[self.name] aresp = self.consumer.parse_response( AuthorizationResponse, info=json.dumps(context.request)) diff --git a/src/satosa/backends/linkedin.py b/src/satosa/backends/linkedin.py index cbaa2ea39..ea8709205 100644 --- a/src/satosa/backends/linkedin.py +++ b/src/satosa/backends/linkedin.py @@ -46,7 +46,7 @@ def __init__(self, outgoing, internal_attributes, config, base_url, name): outgoing, internal_attributes, config, base_url, name, 'linkedin', 'id') - def start_auth(self, context, internal_request, get_state=stateID): + def start_auth(self, context, internal_request, get_state=stateID, **kwargs): """ :param get_state: Generates a state to be used in authentication call @@ -76,7 +76,7 @@ def auth_info(self, requrest): UNSPECIFIED, None, self.config['server_info']['authorization_endpoint']) - def _authn_response(self, context): + def _authn_response(self, context, **kwargs): state_data = context.state[self.name] aresp = self.consumer.parse_response( AuthorizationResponse, info=json.dumps(context.request)) diff --git a/src/satosa/backends/oauth.py b/src/satosa/backends/oauth.py index 9136ce6d4..f641ab4a8 100644 --- a/src/satosa/backends/oauth.py +++ b/src/satosa/backends/oauth.py @@ -64,7 +64,7 @@ def __init__(self, outgoing, internal_attributes, config, base_url, name, extern response_type=self.config["response_type"]) self.consumer.client_secret = self.config["client_secret"] - def start_auth(self, context, internal_request, get_state=stateID): + def start_auth(self, context, internal_request, get_state=stateID, **kwargs): """ See super class method satosa.backends.base#start_auth :param get_state: Generates a state to be used in the authentication call. @@ -118,7 +118,7 @@ def _verify_state(self, resp, state_data, state): "Missing or invalid state [%s] in response!" % received_state) - def _authn_response(self, context): + def _authn_response(self, context, **kwargs): """ Handles the authentication response from the AS. diff --git a/src/satosa/backends/openid_connect.py b/src/satosa/backends/openid_connect.py index 60a17d7a3..b36a29420 100644 --- a/src/satosa/backends/openid_connect.py +++ b/src/satosa/backends/openid_connect.py @@ -64,7 +64,7 @@ def __init__(self, auth_callback_func, internal_attributes, config, base_url, na if "response_type" not in config["client"]["auth_req_params"]: config["auth_req_params"]["response_type"] = "code" - def start_auth(self, context, request_info): + def start_auth(self, context, request_info, **kwargs): """ See super class method satosa.backends.base#start_auth :type context: satosa.context.Context @@ -167,7 +167,7 @@ def _get_userinfo(self, state, context): self._check_error_response(userinfo_resp, context) return userinfo_resp.to_dict() - def response_endpoint(self, context, *args): + def response_endpoint(self, context, *args, **kwargs): """ Handles the authentication response from the OP. :type context: satosa.context.Context diff --git a/src/satosa/backends/orcid.py b/src/satosa/backends/orcid.py index aaa18b7e5..3582bce5d 100644 --- a/src/satosa/backends/orcid.py +++ b/src/satosa/backends/orcid.py @@ -60,7 +60,7 @@ def auth_info(self, requrest): UNSPECIFIED, None, self.config['server_info']['authorization_endpoint']) - def _authn_response(self, context): + def _authn_response(self, context, **kwargs): state_data = context.state[self.name] aresp = self.consumer.parse_response( AuthorizationResponse, info=json.dumps(context.request)) diff --git a/src/satosa/backends/saml2.py b/src/satosa/backends/saml2.py index d942817ac..63ca53f29 100644 --- a/src/satosa/backends/saml2.py +++ b/src/satosa/backends/saml2.py @@ -129,7 +129,7 @@ def __init__(self, outgoing, internal_attributes, config, base_url, name): with open(p) as key_file: self.encryption_keys.append(key_file.read()) - def get_idp_entity_id(self, context): + def get_idp_entity_id(self, context, **kwargs): """ :type context: satosa.context.Context :rtype: str | None @@ -164,7 +164,7 @@ def get_idp_entity_id(self, context): ) return entity_id - def start_auth(self, context, internal_req): + def start_auth(self, context, internal_req, **kwargs): """ See super class method satosa.backends.base.BackendModule#start_auth @@ -184,7 +184,7 @@ def start_auth(self, context, internal_req): return self.authn_request(context, entity_id) - def disco_query(self, context): + def disco_query(self, context, **kwargs): """ Makes a request to the discovery server @@ -236,7 +236,7 @@ def construct_requested_authn_context(self, entity_id): return authn_context - def authn_request(self, context, entity_id): + def authn_request(self, context, entity_id, **kwargs): """ Do an authorization request on idp with given entity id. This is the start of the authorization. @@ -294,7 +294,7 @@ def authn_request(self, context, entity_id): context.state[self.name] = {"relay_state": relay_state} return make_saml_response(binding, ht_args) - def authn_response(self, context, binding): + def authn_response(self, context, binding, **kwargs): """ Endpoint for the idp response :type context: satosa.context,Context @@ -326,11 +326,12 @@ def authn_response(self, context, binding): 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") + # if the response relay_state exists it must match that from the request + if self.name in context.state and "relay_state" in context.state[self.name]: + if context.state[self.name]["relay_state"] != context.request["RelayState"]: + logger.debug("State did not match relay state for state", extra={'state': context.state}) + raise SATOSAAuthenticationError(context.state, "State did not match relay state") + del context.state[self.name] context.decorate(Context.KEY_BACKEND_METADATA_STORE, self.sp.metadata) if self.config.get(SAMLBackend.KEY_MEMORIZE_IDP): @@ -340,7 +341,7 @@ def authn_response(self, context, binding): context.state.pop(Context.KEY_FORCE_AUTHN, None) return self.auth_callback_func(context, self._translate_response(authn_response, context.state)) - def disco_response(self, context): + def disco_response(self, context, **kwargs): """ Endpoint for the discovery server response @@ -406,7 +407,7 @@ def _translate_response(self, response, state): json.dumps(response.ava, indent=4), state) return internal_resp - def _metadata_endpoint(self, context): + def _metadata_endpoint(self, context, **kwargs): """ Endpoint for retrieving the backend metadata :type context: satosa.context.Context diff --git a/src/satosa/base.py b/src/satosa/base.py index ae041ab0e..856d8f17a 100644 --- a/src/satosa/base.py +++ b/src/satosa/base.py @@ -226,7 +226,7 @@ def _run_bound_endpoint(self, context, spec): :return: response """ try: - return spec(context) + return spec(context, wsgi_app=self) except SATOSAAuthenticationError as error: error.error_id = uuid.uuid4().urn state = json.dumps(error.state.state_dict, indent=4) diff --git a/src/satosa/frontends/base.py b/src/satosa/frontends/base.py index 52840a85c..a21ce4274 100644 --- a/src/satosa/frontends/base.py +++ b/src/satosa/frontends/base.py @@ -26,7 +26,7 @@ def __init__(self, auth_req_callback_func, internal_attributes, base_url, name): self.base_url = base_url self.name = name - def handle_authn_response(self, context, internal_resp): + def handle_authn_response(self, context, internal_resp, **kwargs): """ If an authorization has been successful in a backend, this function is called and is supposed to send an authorization response to the client. diff --git a/src/satosa/frontends/openid_connect.py b/src/satosa/frontends/openid_connect.py index bd5972511..0516a09c8 100644 --- a/src/satosa/frontends/openid_connect.py +++ b/src/satosa/frontends/openid_connect.py @@ -117,7 +117,7 @@ def _init_authorization_state(self): return AuthorizationState(HashBasedSubjectIdentifierFactory(sub_hash_salt), authz_code_db, access_token_db, refresh_token_db, sub_db, **token_lifetimes) - def handle_authn_response(self, context, internal_resp, extra_id_token_claims=None): + def handle_authn_response(self, context, internal_resp, extra_id_token_claims=None, **kwargs): """ See super class method satosa.frontends.base.FrontendModule#handle_authn_response :type context: satosa.context.Context @@ -232,7 +232,7 @@ def _get_authn_request_from_state(self, state): """ return AuthorizationRequest().deserialize(state[self.name]["oidc_request"]) - def client_registration(self, context): + def client_registration(self, context, **kwargs): """ Handle the OIDC dynamic client registration. :type context: satosa.context.Context @@ -247,7 +247,7 @@ def client_registration(self, context): except InvalidClientRegistrationRequest as e: return BadRequest(e.to_json(), content="application/json") - def provider_config(self, context): + def provider_config(self, context, **kwargs): """ Construct the provider configuration information (served at /.well-known/openid-configuration). :type context: satosa.context.Context @@ -270,7 +270,7 @@ def _get_approved_attributes(self, provider_supported_claims, authn_req): requested_claims.extend(authn_req["claims"][k].keys()) return set(provider_supported_claims).intersection(set(requested_claims)) - def _handle_authn_request(self, context): + def _handle_authn_request(self, context, **kwargs): """ Parse and verify the authentication request into an internal request. :type context: satosa.context.Context @@ -315,7 +315,7 @@ def _handle_authn_request(self, context): authn_req)) return internal_req - def handle_authn_request(self, context): + def handle_authn_request(self, context, **kwargs): """ Handle an authentication request and pass it on to the backend. :type context: satosa.context.Context @@ -329,7 +329,7 @@ def handle_authn_request(self, context): return internal_req return self.auth_req_callback_func(context, internal_req) - def jwks(self, context): + def jwks(self, context, **kwargs): """ Construct the JWKS document (served at /jwks). :type context: satosa.context.Context @@ -340,7 +340,7 @@ def jwks(self, context): """ return Response(json.dumps(self.provider.jwks), content="application/json") - def token_endpoint(self, context): + def token_endpoint(self, context, **kwargs): """ Handle token requests (served at /token). :type context: satosa.context.Context @@ -364,7 +364,7 @@ def token_endpoint(self, context): error_resp = TokenErrorResponse(error=e.oauth_error, error_description=str(e)) return BadRequest(error_resp.to_json(), content="application/json") - def userinfo_endpoint(self, context): + def userinfo_endpoint(self, context, **kwargs): headers = {"Authorization": context.request_authorization} try: diff --git a/src/satosa/frontends/ping.py b/src/satosa/frontends/ping.py index d0381210a..f9a2ed791 100644 --- a/src/satosa/frontends/ping.py +++ b/src/satosa/frontends/ping.py @@ -19,7 +19,7 @@ def __init__(self, auth_req_callback_func, internal_attributes, config, base_url self.config = config - def handle_authn_response(self, context, internal_resp, extra_id_token_claims=None): + def handle_authn_response(self, context, internal_resp, extra_id_token_claims=None, **kwargs): """ See super class method satosa.frontends.base.FrontendModule#handle_authn_response :type context: satosa.context.Context @@ -47,7 +47,7 @@ def register_endpoints(self, backend_names): return url_map - def ping_endpoint(self, context): + def ping_endpoint(self, context, **kwargs): """ """ logprefix = PingFrontend.logprefix diff --git a/src/satosa/frontends/saml2.py b/src/satosa/frontends/saml2.py index b65003054..3746c75ab 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -78,7 +78,7 @@ def __init__(self, auth_req_callback_func, internal_attributes, config, base_url self.KEY_CUSTOM_ATTR_RELEASE) self.idp = None - def handle_authn_response(self, context, internal_response): + def handle_authn_response(self, context, internal_response, **kwargs): """ See super class method satosa.frontends.base.FrontendModule#handle_authn_response :type context: satosa.context.Context @@ -87,7 +87,7 @@ def handle_authn_response(self, context, internal_response): """ return self._handle_authn_response(context, internal_response, self.idp) - def handle_authn_request(self, context, binding_in): + def handle_authn_request(self, context, binding_in, **kwargs): """ This method is bound to the starting endpoint of the authentication. @@ -174,7 +174,7 @@ def _validate_config(self, config): except KeyError as e: raise ValueError("Missing configuration key: %s" % key) from e - def _handle_authn_request(self, context, binding_in, idp): + def _handle_authn_request(self, context, binding_in, idp, **kwargs): """ See doc for handle_authn_request method. @@ -286,7 +286,7 @@ def _filter_attributes(self, idp, internal_response, context,): return attributes - def _handle_authn_response(self, context, internal_response, idp): + def _handle_authn_response(self, context, internal_response, idp, **kwargs): """ See super class satosa.frontends.base.FrontendModule @@ -425,7 +425,7 @@ def _handle_backend_error(self, exception, idp): satosa_logging(logger, logging.DEBUG, "HTTPargs: %s" % http_args, exception.state) return make_saml_response(resp_args["binding"], http_args) - def _metadata_endpoint(self, context): + def _metadata_endpoint(self, context, **kwargs): """ Endpoint for retrieving the backend metadata :type context: satosa.context.Context @@ -589,7 +589,7 @@ def _load_endpoints_to_config(self, provider, target_entity_id, config=None): idp_conf["service"]["idp"]["endpoints"][service] = idp_endpoints return idp_conf - def _load_idp_dynamic_endpoints(self, context): + def _load_idp_dynamic_endpoints(self, context, **kwargs): """ Loads an idp server that accepts the target backend name in the endpoint url ex: //sso/redirect @@ -621,7 +621,7 @@ def _load_idp_dynamic_entity_id(self, state): idp_config = IdPConfig().load(idp_config_file, metadata_construction=False) return Server(config=idp_config) - def handle_authn_request(self, context, binding_in): + def handle_authn_request(self, context, binding_in, **kwargs): """ Loads approved endpoints dynamically See super class satosa.frontends.saml2.SAMLFrontend#handle_authn_request @@ -637,7 +637,7 @@ def handle_authn_request(self, context, binding_in): idp = self._load_idp_dynamic_endpoints(context) return self._handle_authn_request(context, binding_in, idp) - def _create_state_data(self, context, resp_args, relay_state): + def _create_state_data(self, context, resp_args, relay_state, **kwargs): """ Adds the frontend idp entity id to state See super class satosa.frontends.saml2.SAMLFrontend#save_state @@ -661,7 +661,7 @@ def handle_backend_error(self, exception): idp = self._load_idp_dynamic_entity_id(exception.state) return self._handle_backend_error(exception, idp) - def handle_authn_response(self, context, internal_response): + def handle_authn_response(self, context, internal_response, **kwargs): """ See super class satosa.frontends.base.FrontendModule#handle_authn_response :param context: @@ -708,7 +708,7 @@ class SAMLVirtualCoFrontend(SAMLFrontend): KEY_ORGANIZATION = 'organization' KEY_ORGANIZATION_KEYS = ['display_name', 'name', 'url'] - def handle_authn_request(self, context, binding_in): + def handle_authn_request(self, context, binding_in, **kwargs): """ See super class satosa.frontends.saml2.SAMLFrontend#handle_authn_request @@ -723,7 +723,7 @@ def handle_authn_request(self, context, binding_in): idp = self._create_co_virtual_idp(context) return self._handle_authn_request(context, binding_in, idp) - def handle_authn_response(self, context, internal_response): + def handle_authn_response(self, context, internal_response, **kwargs): """ See super class satosa.frontends.base. FrontendModule#handle_authn_response @@ -734,7 +734,7 @@ def handle_authn_response(self, context, internal_response): return self._handle_authn_response(context, internal_response) - def _handle_authn_response(self, context, internal_response): + def _handle_authn_response(self, context, internal_response, **kwargs): """ """ # Using the context of the current request and saved state from the diff --git a/src/satosa/micro_services/account_linking.py b/src/satosa/micro_services/account_linking.py index 0e849f067..3d36a5974 100644 --- a/src/satosa/micro_services/account_linking.py +++ b/src/satosa/micro_services/account_linking.py @@ -35,7 +35,7 @@ def __init__(self, config, *args, **kwargs): self.id_to_attr = config.get("id_to_attr", None) logger.info("Account linking is active") - def _handle_al_response(self, context): + def _handle_al_response(self, context, **kwargs): """ Endpoint for handling account linking service response. When getting here user might have approved or rejected linking their account @@ -108,7 +108,7 @@ def process(self, context, internal_response): context.state[self.name] = internal_response.to_dict() return Redirect("%s/%s" % (self.redirect_url, jws)) - def _get_uuid(self, context, issuer, id): + def _get_uuid(self, context, issuer, id, **kwargs): """ Ask the account linking service for a uuid. If the given issuer/id pair is not linked, then the function will return a ticket. diff --git a/src/satosa/micro_services/attribute_authorization.py b/src/satosa/micro_services/attribute_authorization.py index 1bcaf8cda..3d07ffaa7 100644 --- a/src/satosa/micro_services/attribute_authorization.py +++ b/src/satosa/micro_services/attribute_authorization.py @@ -48,7 +48,7 @@ def __init__(self, config, *args, **kwargs): self.attribute_allow = config.get("attribute_allow", {}) self.attribute_deny = config.get("attribute_deny", {}) - def _check_authz(self, context, attributes, requester, provider): + def _check_authz(self, context, attributes, requester, provider, **kwargs): for attribute_name, attribute_filters in get_dict_defaults(self.attribute_allow, requester, provider).items(): if attribute_name in attributes: if not any([any(filter(re.compile(af).search, attributes[attribute_name])) for af in attribute_filters]): diff --git a/src/satosa/micro_services/consent.py b/src/satosa/micro_services/consent.py index 7841c5993..53e3d3842 100644 --- a/src/satosa/micro_services/consent.py +++ b/src/satosa/micro_services/consent.py @@ -44,7 +44,7 @@ def __init__(self, config, internal_attributes, *args, **kwargs): self.endpoint = "/handle_consent" logger.info("Consent flow is active") - def _handle_consent_response(self, context): + def _handle_consent_response(self, context, **kwargs): """ Endpoint for handling consent service response :type context: satosa.context.Context @@ -78,7 +78,7 @@ def _handle_consent_response(self, context): internal_response.attributes = self._filter_attributes(internal_response.attributes, consent_attributes) return self._end_consent(context, internal_response) - def _approve_new_consent(self, context, internal_response, id_hash): + def _approve_new_consent(self, context, internal_response, id_hash, **kwargs): context.state[STATE_KEY]["internal_resp"] = internal_response.to_dict() consent_args = { diff --git a/src/satosa/micro_services/redirect_url.py b/src/satosa/micro_services/redirect_url.py index d16b7da27..b0d5ec797 100644 --- a/src/satosa/micro_services/redirect_url.py +++ b/src/satosa/micro_services/redirect_url.py @@ -19,7 +19,8 @@ import sys from typing import Tuple import satosa -from .base import RequestMicroService, ResponseMicroService +from satosa.internal import InternalData +from satosa.micro_services.base import RequestMicroService, ResponseMicroService from satosa.micro_services.local_store import LocalStore MIN_PYTHON = (3, 6) diff --git a/tests/satosa/backends/test_bitbucket.py b/tests/satosa/backends/test_bitbucket.py index 192c55a84..6a636f4a0 100644 --- a/tests/satosa/backends/test_bitbucket.py +++ b/tests/satosa/backends/test_bitbucket.py @@ -78,7 +78,7 @@ def create_backend(self): BB_CONFIG, "base_url", "bitbucket") @pytest.fixture - def incoming_authn_response(self, context): + def incoming_authn_response(self, context, **kwargs): context.path = 'bitbucket/sso/redirect' state_data = dict(state=mock_get_state.return_value) context.state[self.bb_backend.name] = state_data @@ -127,7 +127,7 @@ def test_register_endpoints(self): expected_url_map = [('^bitbucket$', self.bb_backend._authn_response)] assert url_map == expected_url_map - def test_start_auth(self, context): + def test_start_auth(self, context, **kwargs): context.path = 'bitbucket/sso/redirect' internal_request = InternalData( subject_type=NAMEID_FORMAT_TRANSIENT, requester='test_requester' @@ -135,7 +135,8 @@ def test_start_auth(self, context): resp = self.bb_backend.start_auth(context, internal_request, - mock_get_state) + mock_get_state, + **kwargs) login_url = resp.message assert login_url.startswith( BB_CONFIG["server_info"]["authorization_endpoint"]) @@ -165,7 +166,7 @@ def test_authn_response(self, incoming_authn_response): self.assert_token_request(**mock_do_access_token_request.call_args[1]) @responses.activate - def test_entire_flow(self, context): + def test_entire_flow(self, context, **kwargs): """ Tests start of authentication (incoming auth req) and receiving auth response. diff --git a/tests/satosa/backends/test_oauth.py b/tests/satosa/backends/test_oauth.py index 0100cfaa9..ea819285b 100644 --- a/tests/satosa/backends/test_oauth.py +++ b/tests/satosa/backends/test_oauth.py @@ -68,7 +68,7 @@ def create_backend(self): self.fb_backend = FacebookBackend(Mock(), INTERNAL_ATTRIBUTES, FB_CONFIG, "base_url", "facebook") @pytest.fixture - def incoming_authn_response(self, context): + def incoming_authn_response(self, context, **kwargs): context.path = 'facebook/sso/redirect' state_data = dict(state=mock_get_state.return_value) context.state[self.fb_backend.name] = state_data @@ -110,7 +110,7 @@ def test_register_endpoints(self): expected_url_map = [('^facebook$', self.fb_backend._authn_response)] assert url_map == expected_url_map - def test_start_auth(self, context): + def test_start_auth(self, context, **kwargs): context.path = 'facebook/sso/redirect' internal_request = InternalData( subject_type=NAMEID_FORMAT_TRANSIENT, requester='test_requester' @@ -142,7 +142,7 @@ def test_authn_response(self, incoming_authn_response): self.assert_token_request(**mock_do_access_token_request.call_args[1]) @responses.activate - def test_entire_flow(self, context): + def test_entire_flow(self, context, **kwargs): """Tests start of authentication (incoming auth req) and receiving auth response.""" responses.add(responses.POST, "https://graph.facebook.com/v2.5/oauth/access_token", diff --git a/tests/satosa/backends/test_openid_connect.py b/tests/satosa/backends/test_openid_connect.py index b282e7725..9c87282d8 100644 --- a/tests/satosa/backends/test_openid_connect.py +++ b/tests/satosa/backends/test_openid_connect.py @@ -129,7 +129,7 @@ def get_redirect_uri_path(self, backend_config): return urlparse(backend_config["client"]["client_metadata"]["redirect_uris"][0]).path.lstrip("/") @pytest.fixture - def incoming_authn_response(self, context, backend_config): + def incoming_authn_response(self, context, backend_config, **kwargs): oidc_state = "my state" context.path = self.get_redirect_uri_path(backend_config) context.request = { @@ -170,7 +170,7 @@ def test_response_endpoint(self, backend_config, internal_attributes, userinfo, assert isinstance(args[1], InternalData) self.assert_expected_attributes(internal_attributes, userinfo, args[1].attributes) - def test_start_auth_redirects_to_provider_authorization_endpoint(self, context, backend_config): + def test_start_auth_redirects_to_provider_authorization_endpoint(self, context, backend_config, **kwargs): auth_response = self.oidc_backend.start_auth(context, None) assert isinstance(auth_response, Response) @@ -186,7 +186,7 @@ def test_start_auth_redirects_to_provider_authorization_endpoint(self, context, assert "nonce" in auth_params @responses.activate - def test_entire_flow(self, context, backend_config, internal_attributes, userinfo): + def test_entire_flow(self, context, backend_config, internal_attributes, userinfo, **kwargs): self.setup_userinfo_endpoint(backend_config["provider_metadata"]["userinfo_endpoint"], userinfo) auth_response = self.oidc_backend.start_auth(context, None) auth_params = dict(parse_qsl(urlparse(auth_response.message).query)) diff --git a/tests/satosa/backends/test_orcid.py b/tests/satosa/backends/test_orcid.py index 5120d4e89..44c326906 100644 --- a/tests/satosa/backends/test_orcid.py +++ b/tests/satosa/backends/test_orcid.py @@ -145,7 +145,7 @@ def setup_userinfo_endpoint(self, userinfo_endpoint_url, userinfo): ) @pytest.fixture - def incoming_authn_response(self, context, backend_config): + def incoming_authn_response(self, context, backend_config, **kwargs): context.path = backend_config["authz_page"] state_data = dict(state=mock_get_state.return_value) context.state[self.orcid_backend.name] = state_data @@ -156,7 +156,7 @@ def incoming_authn_response(self, context, backend_config): return context - def test_start_auth(self, context, backend_config): + def test_start_auth(self, context, backend_config, **kwargs): auth_response = self.orcid_backend.start_auth( context, None, mock_get_state) assert isinstance(auth_response, Response) @@ -191,7 +191,7 @@ def test_authn_response(self, backend_config, userinfo, incoming_authn_response) self.assert_expected_attributes(userinfo, args[1].attributes) @responses.activate - def test_user_information(self, context, backend_config, userinfo): + def test_user_information(self, context, backend_config, userinfo, **kwargs): self.setup_userinfo_endpoint( backend_config["server_info"]["user_info"], userinfo @@ -212,7 +212,7 @@ def test_user_information(self, context, backend_config, userinfo): assert user_attributes["surname"] == ORCID_PERSON_FAMILY_NAME @responses.activate - def test_user_information_private(self, context, backend_config, userinfo_private): + def test_user_information_private(self, context, backend_config, userinfo_private, **kwargs): self.setup_userinfo_endpoint( backend_config["server_info"]["user_info"], userinfo_private diff --git a/tests/satosa/backends/test_saml2.py b/tests/satosa/backends/test_saml2.py index e5e2d905c..44381f20f 100644 --- a/tests/satosa/backends/test_saml2.py +++ b/tests/satosa/backends/test_saml2.py @@ -103,11 +103,11 @@ def get_path_from_url(url): for endp in all_sp_endpoints: assert any(p.match(endp) for p in compiled_regex) - def test_start_auth_defaults_to_redirecting_to_discovery_server(self, context, sp_conf): + def test_start_auth_defaults_to_redirecting_to_discovery_server(self, context, sp_conf, **kwargs): resp = self.samlbackend.start_auth(context, InternalData()) assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL) - def test_discovery_server_set_in_context(self, context, sp_conf): + def test_discovery_server_set_in_context(self, context, sp_conf, **kwargs): discosrv_url = 'https://my.org/saml_discovery_service' context.decorate( SAMLBackend.KEY_SAML_DISCOVERY_SERVICE_URL, discosrv_url @@ -115,7 +115,7 @@ def test_discovery_server_set_in_context(self, context, sp_conf): resp = self.samlbackend.start_auth(context, InternalData()) assert_redirect_to_discovery_server(resp, sp_conf, discosrv_url) - def test_full_flow(self, context, idp_conf, sp_conf): + def test_full_flow(self, context, idp_conf, sp_conf, **kwargs): test_state_key = "test_state_key_456afgrh" response_binding = BINDING_HTTP_REDIRECT fakeidp = FakeIdP(USERS, config=IdPConfig().load(idp_conf, metadata_construction=False)) @@ -165,7 +165,7 @@ def test_start_auth_redirects_directly_to_mirrored_idp( resp = self.samlbackend.start_auth(context, InternalData()) assert_redirect_to_idp(resp, idp_conf) - def test_redirect_to_idp_if_only_one_idp_in_metadata(self, context, sp_conf, idp_conf): + def test_redirect_to_idp_if_only_one_idp_in_metadata(self, context, sp_conf, idp_conf, **kwargs): sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)] # instantiate new backend, without any discovery service configured samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend") @@ -173,13 +173,13 @@ def test_redirect_to_idp_if_only_one_idp_in_metadata(self, context, sp_conf, idp resp = samlbackend.start_auth(context, InternalData()) assert_redirect_to_idp(resp, idp_conf) - def test_authn_request(self, context, idp_conf): + def test_authn_request(self, context, idp_conf, **kwargs): resp = self.samlbackend.authn_request(context, idp_conf["entityid"]) assert_redirect_to_idp(resp, idp_conf) req_params = dict(parse_qsl(urlparse(resp.message).query)) assert context.state[self.samlbackend.name]["relay_state"] == req_params["RelayState"] - def test_authn_response(self, context, idp_conf, sp_conf): + def test_authn_response(self, context, idp_conf, sp_conf, **kwargs): response_binding = BINDING_HTTP_REDIRECT fakesp = FakeSP(SPConfig().load(sp_conf, metadata_construction=False)) fakeidp = FakeIdP(USERS, config=IdPConfig().load(idp_conf, metadata_construction=False)) @@ -199,7 +199,7 @@ def test_authn_response(self, context, idp_conf, sp_conf): @pytest.mark.skipif( saml2.__version__ < '4.6.1', reason="Optional NameID needs pysaml2 v4.6.1 or higher") - def test_authn_response_no_name_id(self, context, idp_conf, sp_conf): + def test_authn_response_no_name_id(self, context, idp_conf, sp_conf, **kwargs): response_binding = BINDING_HTTP_REDIRECT fakesp_conf = SPConfig().load(sp_conf, metadata_construction=False) @@ -232,7 +232,7 @@ def test_authn_response_no_name_id(self, context, idp_conf, sp_conf): assert_authn_response(internal_resp) assert backend.name not in context.state - def test_authn_response_with_encrypted_assertion(self, sp_conf, context): + def test_authn_response_with_encrypted_assertion(self, sp_conf, context, **kwargs): with open(os.path.join( TEST_RESOURCE_BASE_PATH, "idp_metadata_for_encrypted_signed_auth_response.xml" @@ -277,14 +277,14 @@ def test_authn_response_with_encrypted_assertion(self, sp_conf, context): context, internal_resp = samlbackend.auth_callback_func.call_args[0] assert Counter(internal_resp.attributes.keys()) == Counter({"mail", "givenname", "displayname", "surname"}) - def test_backend_reads_encryption_key_from_key_file(self, sp_conf): + def test_backend_reads_encryption_key_from_key_file(self, sp_conf, **kwargs): sp_conf["key_file"] = os.path.join(TEST_RESOURCE_BASE_PATH, "encryption_key.pem") samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf, "disco_srv": DISCOSRV_URL}, "base_url", "samlbackend") assert samlbackend.encryption_keys - def test_backend_reads_encryption_key_from_encryption_keypair(self, sp_conf): + def test_backend_reads_encryption_key_from_encryption_keypair(self, sp_conf, **kwargs): del sp_conf["key_file"] sp_conf["encryption_keypairs"] = [{"key_file": os.path.join(TEST_RESOURCE_BASE_PATH, "encryption_key.pem")}] samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf, @@ -292,13 +292,13 @@ def test_backend_reads_encryption_key_from_encryption_keypair(self, sp_conf): "base_url", "samlbackend") assert samlbackend.encryption_keys - def test_metadata_endpoint(self, context, sp_conf): + def test_metadata_endpoint(self, context, sp_conf, **kwargs): resp = self.samlbackend._metadata_endpoint(context) headers = dict(resp.headers) assert headers["Content-Type"] == "text/xml" assert sp_conf["entityid"] in resp.message - def test_get_metadata_desc(self, sp_conf, idp_conf): + def test_get_metadata_desc(self, sp_conf, idp_conf, **kwargs): sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)] # instantiate new backend, with a single backing IdP samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend") @@ -321,7 +321,7 @@ def test_get_metadata_desc(self, sp_conf, idp_conf): assert ui_info["description"] == expected_ui_info["description"] assert ui_info["logo"] == expected_ui_info["logo"] - def test_get_metadata_desc_with_logo_without_lang(self, sp_conf, idp_conf): + def test_get_metadata_desc_with_logo_without_lang(self, sp_conf, idp_conf, **kwargs): # add logo without 'lang' idp_conf["service"]["idp"]["ui_info"]["logo"] = [{"text": "https://idp.example.com/static/logo.png", "width": "120", "height": "60"}] @@ -351,8 +351,7 @@ def test_get_metadata_desc_with_logo_without_lang(self, sp_conf, idp_conf): class TestSAMLBackendRedirects: def test_default_redirect_to_discovery_service_if_using_mdq( - self, context, sp_conf, idp_conf - ): + self, context, sp_conf, idp_conf, **kwargs): # one IdP in the metadata, but MDQ also configured so should always redirect to the discovery service sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)] sp_conf["metadata"]["mdq"] = ["https://mdq.example.com"] @@ -362,8 +361,7 @@ def test_default_redirect_to_discovery_service_if_using_mdq( assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL) def test_use_of_disco_or_redirect_to_idp_when_using_mdq_and_forceauthn_is_not_set( - self, context, sp_conf, idp_conf - ): + self, context, sp_conf, idp_conf, **kwargs): sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)] sp_conf["metadata"]["mdq"] = ["https://mdq.example.com"] @@ -402,8 +400,7 @@ def test_use_of_disco_or_redirect_to_idp_when_using_mdq_and_forceauthn_is_not_se assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL) def test_use_of_disco_or_redirect_to_idp_when_using_mdq_and_forceauthn_is_set_true( - self, context, sp_conf, idp_conf - ): + self, context, sp_conf, idp_conf, **kwargs): sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)] sp_conf["metadata"]["mdq"] = ["https://mdq.example.com"] @@ -430,8 +427,7 @@ def test_use_of_disco_or_redirect_to_idp_when_using_mdq_and_forceauthn_is_set_tr assert_redirect_to_idp(resp, idp_conf) def test_use_of_disco_or_redirect_to_idp_when_using_mdq_and_forceauthn_is_set_1( - self, context, sp_conf, idp_conf - ): + self, context, sp_conf, idp_conf, **kwargs): sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)] sp_conf["metadata"]["mdq"] = ["https://mdq.example.com"] diff --git a/tests/satosa/frontends/test_saml2.py b/tests/satosa/frontends/test_saml2.py index 3e89fd2fa..a1eb006e7 100644 --- a/tests/satosa/frontends/test_saml2.py +++ b/tests/satosa/frontends/test_saml2.py @@ -61,7 +61,7 @@ def construct_base_url_from_entity_id(self, entity_id): def setup_for_authn_req(self, context, idp_conf, sp_conf, nameid_format=None, relay_state="relay_state", internal_attributes=INTERNAL_ATTRIBUTES, extra_config={}, - subject=None): + subject=None, **kwargs): config = {"idp_config": idp_conf, "endpoints": ENDPOINTS} config.update(extra_config) sp_metadata_str = create_metadata_from_config_dict(sp_conf) @@ -142,7 +142,7 @@ def get_path_from_url(url): for endp in all_idp_endpoints: assert any(p.match(endp) for p in compiled_regex) - def test_handle_authn_request(self, context, idp_conf, sp_conf, internal_response): + def test_handle_authn_request(self, context, idp_conf, sp_conf, internal_response, **kwargs): samlfrontend = self.setup_for_authn_req(context, idp_conf, sp_conf) _, internal_req = samlfrontend.handle_authn_request(context, BINDING_HTTP_REDIRECT) assert internal_req.requester == sp_conf["entityid"] @@ -156,7 +156,8 @@ def test_handle_authn_request(self, context, idp_conf, sp_conf, internal_respons for key in resp.ava: assert USERS["testuser1"][key] == resp.ava[key] - def test_create_authn_request_with_subject(self, context, idp_conf, sp_conf, internal_response): + def test_create_authn_request_with_subject(self, context, idp_conf, sp_conf, internal_response, + **kwargs): name_id_value = 'somenameid' name_id = NameID(format=NAMEID_FORMAT_UNSPECIFIED, text=name_id_value) subject = Subject(name_id=name_id) @@ -181,7 +182,8 @@ def test_handle_authn_request_without_name_id_policy_and_metadata_without_name_i _, internal_req = samlfrontend.handle_authn_request(context, BINDING_HTTP_REDIRECT) assert internal_req.subject_type == NAMEID_FORMAT_TRANSIENT - def test_handle_authn_response_without_relay_state(self, context, idp_conf, sp_conf, internal_response): + def test_handle_authn_response_without_relay_state(self, context, idp_conf, sp_conf, + internal_response, **kwargs): samlfrontend = self.setup_for_authn_req(context, idp_conf, sp_conf, relay_state=None) _, internal_req = samlfrontend.handle_authn_request(context, BINDING_HTTP_REDIRECT) assert internal_req.requester == sp_conf["entityid"] @@ -263,7 +265,8 @@ def test_get_filter_attributes_with_sp_requested_attributes_without_friendlyname assert set(filtered_attributes) == set(["edupersontargetedid", "edupersonprincipalname", "edupersonaffiliation", "mail", "displayname", "sn", "givenname"]) - def test_acr_mapping_in_authn_response(self, context, idp_conf, sp_conf, internal_response): + def test_acr_mapping_in_authn_response(self, context, idp_conf, sp_conf, internal_response, + **kwargs): eidas_loa_low = "http://eidas.europa.eu/LoA/low" loa = {"": eidas_loa_low} samlfrontend = self.setup_for_authn_req(context, idp_conf, sp_conf, extra_config={"acr_mapping": loa}) @@ -274,7 +277,8 @@ def test_acr_mapping_in_authn_response(self, context, idp_conf, sp_conf, interna authn_context_class_ref = resp.assertion.authn_statement[0].authn_context.authn_context_class_ref assert authn_context_class_ref.text == eidas_loa_low - def test_acr_mapping_per_idp_in_authn_response(self, context, idp_conf, sp_conf, internal_response): + def test_acr_mapping_per_idp_in_authn_response(self, context, idp_conf, sp_conf, + internal_response, **kwargs): expected_loa = "LoA1" loa = {"": "http://eidas.europa.eu/LoA/low", idp_conf["entityid"]: expected_loa} samlfrontend = self.setup_for_authn_req(context, idp_conf, sp_conf, extra_config={"acr_mapping": loa}) @@ -294,8 +298,9 @@ def test_acr_mapping_per_idp_in_authn_response(self, context, idp_conf, sp_conf, ([RESEARCH_AND_EDUCATION, NREN], "swamid", swamid.RELEASE[""] + swamid.RELEASE[(RESEARCH_AND_EDUCATION, NREN)]), ([SFS_1993_1153], "swamid", swamid.RELEASE[""] + swamid.RELEASE[SFS_1993_1153]), ]) - def test_respect_sp_entity_categories(self, context, entity_category, entity_category_module, expected_attributes, - idp_conf, sp_conf, internal_response): + def test_respect_sp_entity_categories(self, context, entity_category, entity_category_module, + expected_attributes, idp_conf, sp_conf, + internal_response, **kwargs): idp_metadata_str = create_metadata_from_config_dict(idp_conf) idp_conf["service"]["idp"]["policy"]["default"]["entity_categories"] = [entity_category_module] if all(entity_category): # don't insert empty entity category @@ -321,22 +326,23 @@ def test_respect_sp_entity_categories(self, context, entity_category, entity_cat resp = self.get_auth_response(samlfrontend, context, internal_response, sp_conf, idp_metadata_str) assert Counter(resp.ava.keys()) == Counter(expected_attributes) - def test_sp_metadata_including_uiinfo_display_name(self, context, idp_conf, sp_conf): + def test_sp_metadata_including_uiinfo_display_name(self, context, idp_conf, sp_conf, **kwargs): sp_conf["service"]["sp"]["ui_info"] = dict(display_name="Test SP") samlfrontend = self.setup_for_authn_req(context, idp_conf, sp_conf) display_names = samlfrontend._get_sp_display_name(samlfrontend.idp, sp_conf["entityid"]) assert display_names[0]["text"] == "Test SP" - def test_sp_metadata_including_uiinfo_without_display_name(self, context, idp_conf, sp_conf): + def test_sp_metadata_including_uiinfo_without_display_name(self, context, idp_conf, sp_conf, + **kwargs): sp_conf["service"]["sp"]["ui_info"] = dict(information_url="http://info.example.com") samlfrontend = self.setup_for_authn_req(context, idp_conf, sp_conf) assert samlfrontend._get_sp_display_name(samlfrontend.idp, sp_conf["entityid"]) is None - def test_sp_metadata_without_uiinfo(self, context, idp_conf, sp_conf): + def test_sp_metadata_without_uiinfo(self, context, idp_conf, sp_conf, **kwargs): samlfrontend = self.setup_for_authn_req(context, idp_conf, sp_conf) assert samlfrontend._get_sp_display_name(samlfrontend.idp, sp_conf["entityid"]) is None - def test_metadata_endpoint(self, context, idp_conf): + def test_metadata_endpoint(self, context, idp_conf, **kwargs): conf = {"idp_config": idp_conf, "endpoints": ENDPOINTS} samlfrontend = SAMLFrontend(lambda ctx, req: None, INTERNAL_ATTRIBUTES, conf, "base_url", "saml_frontend") samlfrontend.register_endpoints(["todo"]) @@ -345,8 +351,9 @@ def test_metadata_endpoint(self, context, idp_conf): assert headers["Content-Type"] == "text/xml" assert idp_conf["entityid"] in resp.message - def test_custom_attribute_release_with_less_attributes_than_entity_category(self, context, idp_conf, sp_conf, - internal_response): + def test_custom_attribute_release_with_less_attributes_than_entity_category( + self, context, idp_conf, sp_conf, + internal_response, **kwargs): idp_metadata_str = create_metadata_from_config_dict(idp_conf) idp_conf["service"]["idp"]["policy"]["default"]["entity_categories"] = ["swamid"] sp_conf["entity_category"] = [SFS_1993_1153] @@ -392,7 +399,7 @@ def test_load_endpoints_to_config(self): idp_config = self.frontend._load_endpoints_to_config(self.BACKEND, self.TARGET_ENTITY_ID) self.assert_dynamic_endpoints(idp_config["service"]["idp"]["endpoints"]["single_sign_on_service"]) - def test_load_idp_dynamic_endpoints(self, context): + def test_load_idp_dynamic_endpoints(self, context, **kwargs): context.path = "{}/{}/sso/redirect".format(self.BACKEND, self.TARGET_ENTITY_ID) context.target_backend = self.BACKEND idp = self.frontend._load_idp_dynamic_endpoints(context) diff --git a/tests/satosa/micro_services/test_consent.py b/tests/satosa/micro_services/test_consent.py index 247b74868..ebc39b9fc 100644 --- a/tests/satosa/micro_services/test_consent.py +++ b/tests/satosa/micro_services/test_consent.py @@ -123,7 +123,7 @@ def test_consent_registration(self, consent_config): @responses.activate def test_consent_handles_connection_error(self, context, internal_response, internal_request, - consent_verify_endpoint_regex): + consent_verify_endpoint_regex, **kwargs): responses.add(responses.GET, consent_verify_endpoint_regex, body=requests.ConnectionError("No connection")) @@ -139,7 +139,7 @@ def test_consent_handles_connection_error(self, context, internal_response, inte @responses.activate def test_consent_prev_given(self, context, internal_response, internal_request, - consent_verify_endpoint_regex): + consent_verify_endpoint_regex, **kwargs): responses.add(responses.GET, consent_verify_endpoint_regex, status=200, body=json.dumps(FILTER)) @@ -149,7 +149,8 @@ def test_consent_prev_given(self, context, internal_response, internal_request, assert "displayName" in internal_response.attributes def test_consent_full_flow(self, context, consent_config, internal_response, internal_request, - consent_verify_endpoint_regex, consent_registration_endpoint_regex): + consent_verify_endpoint_regex, consent_registration_endpoint_regex, + **kwargs): expected_ticket = "my_ticket" requester_name = [{"lang": "en", "text": "test requester"}] @@ -182,7 +183,8 @@ def test_consent_full_flow(self, context, consent_config, internal_response, int @responses.activate def test_consent_not_given(self, context, consent_config, internal_response, internal_request, - consent_verify_endpoint_regex, consent_registration_endpoint_regex): + consent_verify_endpoint_regex, consent_registration_endpoint_regex, + **kwargs): expected_ticket = "my_ticket" responses.add(responses.GET, consent_verify_endpoint_regex, status=401) @@ -217,9 +219,9 @@ def test_filter_attributes(self): assert Counter(filtered_attributes.keys()) == Counter(FILTER) @responses.activate - def test_manage_consent_filters_attributes_before_send_to_consent_service(self, context, internal_request, - internal_response, - consent_verify_endpoint_regex): + def test_manage_consent_filters_attributes_before_send_to_consent_service( + self, context, internal_request, + internal_response, consent_verify_endpoint_regex, **kwargs): approved_attributes = ["foo", "bar"] # fake previous consent responses.add(responses.GET, consent_verify_endpoint_regex, status=200, @@ -238,8 +240,9 @@ def test_manage_consent_filters_attributes_before_send_to_consent_service(self, assert consent_hash == expected_hash @responses.activate - def test_manage_consent_without_filter_passes_through_all_attributes(self, context, internal_response, - consent_verify_endpoint_regex): + def test_manage_consent_without_filter_passes_through_all_attributes( + self, context, internal_response, + consent_verify_endpoint_regex, **kwargs): # fake previous consent responses.add(responses.GET, consent_verify_endpoint_regex, status=200, body=json.dumps(list(internal_response.attributes.keys()))) diff --git a/tests/satosa/micro_services/test_custom_routing.py b/tests/satosa/micro_services/test_custom_routing.py index 7a5227250..6804957cb 100644 --- a/tests/satosa/micro_services/test_custom_routing.py +++ b/tests/satosa/micro_services/test_custom_routing.py @@ -144,7 +144,7 @@ def test_defaults_to_allow_all_requesters_for_target_entity_without_specific_rul req = InternalData(requester="test_requester") assert decide_service.process(target_context, req) - def test_missing_target_entity_id_from_context(self, context): + def test_missing_target_entity_id_from_context(self, context, **kwargs): target_entity = "entity1" rules = { target_entity: { diff --git a/tests/satosa/test_base.py b/tests/satosa/test_base.py index 0cb365742..32db8a73f 100644 --- a/tests/satosa/test_base.py +++ b/tests/satosa/test_base.py @@ -41,7 +41,7 @@ def test_constuctor_should_raise_exception_if_consent_is_not_last_in_micro_servi with pytest.raises(SATOSAConfigurationError): SATOSABase(satosa_config) - def test_auth_resp_callback_func_user_id_from_attrs_is_used_to_override_user_id(self, context, satosa_config): + def test_auth_resp_callback_func_user_id_from_attrs_is_used_to_override_user_id(self, context, satosa_config, **kwargs): satosa_config["INTERNAL_ATTRIBUTES"]["user_id_from_attrs"] = ["user_id", "domain"] base = SATOSABase(satosa_config) @@ -56,7 +56,7 @@ def test_auth_resp_callback_func_user_id_from_attrs_is_used_to_override_user_id( expected_user_id = "user@example.com" assert internal_resp.subject_id == expected_user_id - def test_auth_req_callback_stores_state_for_consent(self, context, satosa_config): + def test_auth_req_callback_stores_state_for_consent(self, context, satosa_config, **kwargs): base = SATOSABase(satosa_config) context.target_backend = satosa_config["BACKEND_MODULES"][0]["name"] @@ -70,7 +70,7 @@ def test_auth_req_callback_stores_state_for_consent(self, context, satosa_config assert context.state[consent.STATE_KEY]["requester_name"] == internal_req.requester_name assert context.state[consent.STATE_KEY]["filter"] == internal_req.attributes - def test_auth_resp_callback_func_hashes_all_specified_attributes(self, context, satosa_config): + def test_auth_resp_callback_func_hashes_all_specified_attributes(self, context, satosa_config, **kwargs): satosa_config["INTERNAL_ATTRIBUTES"]["hash"] = ["user_id", "mail"] base = SATOSABase(satosa_config) @@ -88,7 +88,7 @@ def test_auth_resp_callback_func_hashes_all_specified_attributes(self, context, for v in attributes[attr] ] - def test_auth_resp_callback_func_respects_user_id_to_attr(self, context, satosa_config): + def test_auth_resp_callback_func_respects_user_id_to_attr(self, context, satosa_config, **kwargs): satosa_config["INTERNAL_ATTRIBUTES"]["user_id_to_attr"] = "user_id" base = SATOSABase(satosa_config) diff --git a/tests/satosa/test_routing.py b/tests/satosa/test_routing.py index be23456ad..82a22fcb6 100644 --- a/tests/satosa/test_routing.py +++ b/tests/satosa/test_routing.py @@ -78,7 +78,7 @@ def test_module_routing(self, url_path, expected_frontend, expected_backend, con frontend = self.router.frontend_routing(context) assert frontend == self.router.frontends[expected_frontend]["instance"] - def test_endpoint_routing_with_unknown_endpoint(self, context): + def test_endpoint_routing_with_unknown_endpoint(self, context, **kwargs): context.path = "unknown" with pytest.raises(SATOSANoBoundEndpointError): self.router.endpoint_routing(context) diff --git a/tests/util.py b/tests/util.py index 12d9ec4ca..13c5402e4 100644 --- a/tests/util.py +++ b/tests/util.py @@ -381,7 +381,7 @@ def __init__(self, start_auth_func=None, internal_attributes=None, self.start_auth_func = start_auth_func self.register_endpoints_func = register_endpoints_func - def start_auth(self, context, request_info, state): + def start_auth(self, context, request_info, state, **kwargs): """ TODO comment :type context: TODO comment @@ -419,7 +419,7 @@ def __init__(self, handle_authn_request_func=None, internal_attributes=None, self.handle_authn_response_func = handle_authn_response_func self.register_endpoints_func = register_endpoints_func - def handle_authn_request(self, context, binding_in): + def handle_authn_request(self, context, binding_in, **kwargs): """ TODO comment @@ -434,7 +434,7 @@ def handle_authn_request(self, context, binding_in): return self.handle_authn_request_func(context, binding_in) return None - def handle_authn_response(self, context, internal_response, state): + def handle_authn_response(self, context, internal_response, state, **kwargs): """ TODO comment :type context: TODO comment @@ -462,10 +462,10 @@ def __init__(self, auth_callback_func, internal_attributes, config, base_url, na def register_endpoints(self): return [("^{}/response$".format(self.name), self.handle_response)] - def start_auth(self, context, internal_request): + def start_auth(self, context, internal_request, **kwargs): return Response("Auth request received, passed to test backend") - def handle_response(self, context): + def handle_response(self, context, **kwargs): auth_info = AuthenticationInformation("test", str(datetime.now()), "test_issuer") internal_resp = InternalData(auth_info=auth_info) internal_resp.attributes = context.request @@ -481,13 +481,13 @@ def register_endpoints(self, backend_names): url_map = [("^{}/{}/request$".format(p, self.name), self.handle_request) for p in backend_names] return url_map - def handle_request(self, context): + def handle_request(self, context, **kwargs): internal_req = InternalData( subject_type=NAMEID_FORMAT_TRANSIENT, requester="test_client" ) return self.auth_req_callback_func(context, internal_req) - def handle_authn_response(self, context, internal_resp): + def handle_authn_response(self, context, internal_resp, **kwargs): return Response("Auth response received, passed to test frontend") From 231ea51542dcf6c7afca3afa344704915a7ace38 Mon Sep 17 00:00:00 2001 From: rhoerbe Date: Tue, 22 Oct 2019 22:26:06 +0200 Subject: [PATCH 4/7] rename venv --- src/satosa/micro_services/filter_requester.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/satosa/micro_services/filter_requester.py diff --git a/src/satosa/micro_services/filter_requester.py b/src/satosa/micro_services/filter_requester.py new file mode 100644 index 000000000..206b3fba1 --- /dev/null +++ b/src/satosa/micro_services/filter_requester.py @@ -0,0 +1,60 @@ +import logging +from base64 import urlsafe_b64encode + +from satosa.context import Context + +from .base import RequestMicroService +from ..exception import SATOSAConfigurationError +from ..exception import SATOSAError + +logger = logging.getLogger(__name__) + + +class FilterRequester(RequestMicroService): + """ + Decide whether a requester is allowed to send an authentication request to the target entity based on a whitelist + """ + def __init__(self, config, *args, **kwargs): + super().__init__(*args, **kwargs) + + for target_entity, rules in config["rules"].items(): + conflicting_rules = set(rules.get("deny", [])).intersection(rules.get("allow", [])) + if conflicting_rules: + raise SATOSAConfigurationError("Conflicting requester rules for FilterRequester," + "{} is both denied and allowed".format(conflicting_rules)) + + self.rules = {self._b64_url(k): v for k, v in config["rules"].items()} + self.conf_target_entity_id = config.get('target_entity_id', None) + + def process(self, context, data): + target_entity_id = context.get_decoration(Context.KEY_TARGET_ENTITYID) or self.conf_target_entity_id + if None is target_entity_id: + msg_tpl = "{name} can only be used when a target entityid is set" + msg = msg_tpl.format(name=self.__class__.__name__) + logger.error(msg) + raise SATOSAError(msg) + + target_specific_rules = self.rules.get(target_entity_id) + # default to allowing everything if there are no specific rules + if not target_specific_rules: + logging.debug("Requester '%s' allowed by default to target entity '%s' due to no entity specific rules", + data.requester, target_entity_id) + return super().process(context, data) + + # deny rules takes precedence + deny_rules = target_specific_rules.get("deny", []) + if data.requester in deny_rules: + logging.debug("Requester '%s' is not allowed by target entity '%s' due to deny rules '%s'", data.requester, + target_entity_id, deny_rules) + raise SATOSAError("Requester is not allowed by target provider") + + allow_rules = target_specific_rules.get("allow", []) + allow_all = "*" in allow_rules + if data.requester in allow_rules or allow_all: + logging.debug("Requester '%s' allowed by target entity '%s' due to allow rules '%s", + data.requester, target_entity_id, allow_rules) + return super().process(context, data) + + logger.debug("Requester '%s' is not allowed by target entity '%s' due to final deny all rule in '%s'", + data.requester, target_entity_id, deny_rules) + raise SATOSAError("Requester is not allowed by target provider") From 6596de87f4fe8c627461c2bc2be99e971dd7fc9f Mon Sep 17 00:00:00 2001 From: rhoerbe Date: Wed, 23 Oct 2019 22:54:02 +0200 Subject: [PATCH 5/7] add redis to setup --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2b68c089d..f1765bbc3 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,8 @@ "pystache" ], extras_require={ - "ldap": ["ldap3"] + "ldap": ["ldap3"], + "redirecturl": ["redis"], }, zip_safe=False, classifiers=[ From db3f6927c71f85e7524872cc8748d1b9c672c150 Mon Sep 17 00:00:00 2001 From: rhoerbe Date: Thu, 24 Oct 2019 21:16:09 +0200 Subject: [PATCH 6/7] filter_requester v0 --- .../microservices/filter_requester.yaml | 10 +++ src/satosa/micro_services/filter_requester.py | 66 ++++++------------- 2 files changed, 29 insertions(+), 47 deletions(-) create mode 100644 example/plugins/microservices/filter_requester.yaml diff --git a/example/plugins/microservices/filter_requester.yaml b/example/plugins/microservices/filter_requester.yaml new file mode 100644 index 000000000..8e9514b06 --- /dev/null +++ b/example/plugins/microservices/filter_requester.yaml @@ -0,0 +1,10 @@ +# Requester whitelist: this microservice cannot be used for blacklisting +config: + allow: + # this list must not be empty. Use '*' to allow all requesters + - https://sp1.test.wpv.portalverbund.at/sp.xml + - https://sp3.test.wpv.portalverbund.at/sp.xml + - https://sp4.test.wpv.portalverbund.at/sp.xml + - https://useradmin-core.austrian-standards.at/saml/metadata/alias/austrian-standards-core-wpv +module: satosa.micro_services.filter_requester +name: FilterRequester diff --git a/src/satosa/micro_services/filter_requester.py b/src/satosa/micro_services/filter_requester.py index 206b3fba1..3bd316485 100644 --- a/src/satosa/micro_services/filter_requester.py +++ b/src/satosa/micro_services/filter_requester.py @@ -1,11 +1,10 @@ import logging -from base64 import urlsafe_b64encode +from typing import Tuple from satosa.context import Context - -from .base import RequestMicroService -from ..exception import SATOSAConfigurationError -from ..exception import SATOSAError +from satosa.exception import SATOSAConfigurationError, SATOSAError +from satosa.internal import InternalData +from satosa.micro_services.base import RequestMicroService logger = logging.getLogger(__name__) @@ -16,45 +15,18 @@ class FilterRequester(RequestMicroService): """ def __init__(self, config, *args, **kwargs): super().__init__(*args, **kwargs) - - for target_entity, rules in config["rules"].items(): - conflicting_rules = set(rules.get("deny", [])).intersection(rules.get("allow", [])) - if conflicting_rules: - raise SATOSAConfigurationError("Conflicting requester rules for FilterRequester," - "{} is both denied and allowed".format(conflicting_rules)) - - self.rules = {self._b64_url(k): v for k, v in config["rules"].items()} - self.conf_target_entity_id = config.get('target_entity_id', None) - - def process(self, context, data): - target_entity_id = context.get_decoration(Context.KEY_TARGET_ENTITYID) or self.conf_target_entity_id - if None is target_entity_id: - msg_tpl = "{name} can only be used when a target entityid is set" - msg = msg_tpl.format(name=self.__class__.__name__) - logger.error(msg) - raise SATOSAError(msg) - - target_specific_rules = self.rules.get(target_entity_id) - # default to allowing everything if there are no specific rules - if not target_specific_rules: - logging.debug("Requester '%s' allowed by default to target entity '%s' due to no entity specific rules", - data.requester, target_entity_id) - return super().process(context, data) - - # deny rules takes precedence - deny_rules = target_specific_rules.get("deny", []) - if data.requester in deny_rules: - logging.debug("Requester '%s' is not allowed by target entity '%s' due to deny rules '%s'", data.requester, - target_entity_id, deny_rules) - raise SATOSAError("Requester is not allowed by target provider") - - allow_rules = target_specific_rules.get("allow", []) - allow_all = "*" in allow_rules - if data.requester in allow_rules or allow_all: - logging.debug("Requester '%s' allowed by target entity '%s' due to allow rules '%s", - data.requester, target_entity_id, allow_rules) - return super().process(context, data) - - logger.debug("Requester '%s' is not allowed by target entity '%s' due to final deny all rule in '%s'", - data.requester, target_entity_id, deny_rules) - raise SATOSAError("Requester is not allowed by target provider") + errmsg = "FilterRequester: config must contain a key 'allow' with a non-empty list of entityIDs." + try: + self.rules = config["allow"] + except KeyError: + logging.error(errmsg) + raise SATOSAConfigurationError(errmsg) + if self.rules is None: + logging.error(errmsg) + raise SATOSAConfigurationError(errmsg) + + def process(self, context: Context, internal_request: InternalData) -> Tuple[Context, InternalData]: + if internal_request.requester not in self.rules and '*' not in self.rules: + errmsg = "Requester '%s' is not allowed in filter_requester configuration" % internal_request.requester + raise SATOSAError(errmsg) + return super().process(context, internal_request) From 2f0ad682958222d955ebba40ce875a773e64e444 Mon Sep 17 00:00:00 2001 From: rhoerbe Date: Thu, 24 Oct 2019 21:16:34 +0200 Subject: [PATCH 7/7] adding example config files for new micro services --- .../microservices/custom_routing.example | 11 ++++++ .../microservices/filter_requester.yaml | 3 +- .../redirect_url_request.yaml.example | 4 +++ .../redirect_url_response.yaml.example | 6 ++++ .../microservices/simpleconsent.yaml.example | 36 +++++++++++++++++++ 5 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 example/plugins/microservices/custom_routing.example create mode 100644 example/plugins/microservices/redirect_url_request.yaml.example create mode 100644 example/plugins/microservices/redirect_url_response.yaml.example create mode 100644 example/plugins/microservices/simpleconsent.yaml.example diff --git a/example/plugins/microservices/custom_routing.example b/example/plugins/microservices/custom_routing.example new file mode 100644 index 000000000..b6b0d9c3d --- /dev/null +++ b/example/plugins/microservices/custom_routing.example @@ -0,0 +1,11 @@ +module: satosa.micro_services.custom_routing.DecideIfRequesterIsAllowed +name: RequesterDecider +config: + rules: + target_entity_id1: + allow: ["requester1", "requester2"] + target_entity_id2: + deny: ["requester3"] + target_entity_id3: + allow: ["requester1"] + deny: ["*"] diff --git a/example/plugins/microservices/filter_requester.yaml b/example/plugins/microservices/filter_requester.yaml index 8e9514b06..072adad8a 100644 --- a/example/plugins/microservices/filter_requester.yaml +++ b/example/plugins/microservices/filter_requester.yaml @@ -5,6 +5,5 @@ config: - https://sp1.test.wpv.portalverbund.at/sp.xml - https://sp3.test.wpv.portalverbund.at/sp.xml - https://sp4.test.wpv.portalverbund.at/sp.xml - - https://useradmin-core.austrian-standards.at/saml/metadata/alias/austrian-standards-core-wpv -module: satosa.micro_services.filter_requester +module: satosa.micro_services.filter_requester.FilterRequester name: FilterRequester diff --git a/example/plugins/microservices/redirect_url_request.yaml.example b/example/plugins/microservices/redirect_url_request.yaml.example new file mode 100644 index 000000000..07371b31e --- /dev/null +++ b/example/plugins/microservices/redirect_url_request.yaml.example @@ -0,0 +1,4 @@ +module: satosa.micro_services.redirect_url.RedirectUrlRequest +name: RedirectUrlRequest +config: + db_encryption_key: "WQpuhOELqGAs/ct2mujCqw" \ No newline at end of file diff --git a/example/plugins/microservices/redirect_url_response.yaml.example b/example/plugins/microservices/redirect_url_response.yaml.example new file mode 100644 index 000000000..452c97b32 --- /dev/null +++ b/example/plugins/microservices/redirect_url_response.yaml.example @@ -0,0 +1,6 @@ +module: satosa.micro_services.redirect_url.RedirectUrlResponse +name: RedirectUrlResponse +config: + db_encryption_key: "WQpuhOELqGAs/ct2mujCqw" # must be same in redirect_url_request.yaml + redirect_attr_name: WkisRedirect + redir_entityid: http://wkis.qss.wko.at/adfs/services/trust diff --git a/example/plugins/microservices/simpleconsent.yaml.example b/example/plugins/microservices/simpleconsent.yaml.example new file mode 100644 index 000000000..872aa9c01 --- /dev/null +++ b/example/plugins/microservices/simpleconsent.yaml.example @@ -0,0 +1,36 @@ +# this file contains secret keys. Protect file or insert values from environment + +module: satosa.micro_services.simpleconsent.SimpleConsent +name: SimpleConsent +config: + consent_cookie_name: SATOSA_CONSENTID + consent_attr_not_displayed: + - addr_country + - authenticationClass + - possibleroles + - registrationClassOrg + - registrationClassUser + - wkis_roleextendeddescription + - wkis_roletypeid + consent_attrname_display: + uid: UserID + displayname: Anzeigename + givenname: Vorname + gln: "GLN der Firma" + mail: E-Mailadresse + name: Vor+Familienname + surname: Familienname + wkis_roledescription: "WKO Mitglied" + consent_service_api_auth: + userid: admin + password: adminadmin + id_hash_alg: md5 # md5 is shorter than sha-224, but may be missing in (rare) FIPS-compliant C-Python + # shard secret: configure same ASCII-value in proxy and consent app + PROXY_HMAC_KEY: "your random key (-> `openssh rand -base64 30`)" + request_consent_url: https://consent.example.org/request_consent + self_entityid: satosa.vnet/idp_proxy.xml + # consent display page: translate SP entityIDs to human readable names + # (work around, as metadata is not available in the micro service) + sp_entityid_names: + https://sp1.test.wpv.portalverbund.at/sp.xml: "Test SP1" + verify_consent_url: https://consent.example.org/has_consent