def test_start_auth_no_request_info(self, sp_conf): """ Performs a complete test for the module satosa.backends.saml2. The flow should be accepted. """ disco_srv = "https://my.dicso.com/role/idp.ds" samlbackend = SamlBackend( None, INTERNAL_ATTRIBUTES, { "config": sp_conf, "disco_srv": disco_srv, "state_id": "saml_backend_test_id" }) internal_data = InternalRequest(None, None) state = State() context = Context() context.state = state resp = samlbackend.start_auth(context, internal_data) assert resp.status == "303 See Other", "Must be a redirect to the discovery server." assert resp.message.startswith("https://my.dicso.com/role/idp.ds"), \ "Redirect to wrong URL." # create_name_id_policy_transient() state = State() context = Context() context.state = state user_id_hash_type = UserIdHashType.transient internal_data = InternalRequest(user_id_hash_type, None) resp = samlbackend.start_auth(context, internal_data) assert resp.status == "303 See Other", "Must be a redirect to the discovery server."
def test_auth_req_callback_stores_state_for_consent(self, context, satosa_config): base = SATOSABase(satosa_config) context.target_backend = satosa_config["BACKEND_MODULES"][0]["name"] requester_name = [{"lang": "en", "text": "Test EN"}, {"lang": "sv", "text": "Test SV"}] internal_req = InternalRequest(UserIdHashType.transient, None, requester_name) internal_req.approved_attributes = ["attr1", "attr2"] base._auth_req_callback_func(context, internal_req) assert context.state[consent.STATE_KEY]["requester_name"] == internal_req.requester_name assert context.state[consent.STATE_KEY]["filter"] == internal_req.approved_attributes
def test_allow_one_requester(self, target_context): rules = { TARGET_ENTITY: { "allow": ["test_requester"], } } decide_service = self.create_decide_service(rules) req = InternalRequest(None, "test_requester", None) assert decide_service.process(target_context, req) req.requester = "somebody else" with pytest.raises(SATOSAError): decide_service.process(target_context, req)
def test_entire_flow(self, context): """Tests start of authentication (incoming auth req) and receiving auth response.""" responses.add(responses.POST, "https://graph.facebook.com/v2.5/oauth/access_token", body=json.dumps({ "access_token": "qwerty", "token_type": "bearer", "expires_in": 9999999999999 }), status=200, content_type='application/json') self.setup_facebook_response() context.path = 'facebook/sso/redirect' internal_request = InternalRequest(UserIdHashType.transient, 'test_requester') self.fb_backend.start_auth(context, internal_request, mock_get_state) context.request = { "code": FB_RESPONSE_CODE, "state": mock_get_state.return_value } self.fb_backend._authn_response(context) assert self.fb_backend.name not in context.state self.assert_expected_attributes()
def test_defaults_to_allow_all_requesters_for_target_entity_without_specific_rules( self, target_context): rules = {"some other entity": {"allow": ["foobar"]}} decide_service = self.create_decide_service(rules) req = InternalRequest(None, "test_requester", None) assert decide_service.process(target_context, req)
def disco_response(self, context): """ Endpoint for the discovery server response :type context: satosa.context.Context :rtype: satosa.response.Response :param context: The current context :return: response """ info = context.request state = context.state try: entity_id = info[self.idp_disco_query_param] except KeyError as err: satosa_logging(LOGGER, logging.DEBUG, "No IDP chosen for state", state, exc_info=True) raise SATOSAAuthenticationError(state, "No IDP chosen") from err else: request_info = InternalRequest(None, None) return self.authn_request(context, entity_id, request_info)
def test_with_pyoidc(self): responses.add(responses.POST, "https://graph.facebook.com/v2.5/oauth/access_token", body=json.dumps({ "access_token": "qwerty", "token_type": "bearer", "expires_in": 9999999999999 }), adding_headers={"set-cookie": "TEST=testing; path=/"}, status=200, content_type='application/json') responses.add(responses.GET, "https://graph.facebook.com/v2.5/me", match_querystring=False, body=json.dumps(FB_RESPONSE), status=200, content_type='application/json') context = Context() context.path = 'facebook/sso/redirect' context.state = State() internal_request = InternalRequest(UserIdHashType.transient, 'http://localhost:8087/sp.xml') get_state = Mock() get_state.return_value = STATE resp = self.fb_backend.start_auth(context, internal_request, get_state) context.cookie = resp.headers[0][1] context.request = {"code": FB_RESPONSE_CODE, "state": STATE} self.fb_backend.auth_callback_func = self.verify_callback self.fb_backend.authn_response(context)
def test_auth_resp_callback_func_user_id_from_attrs_is_used_to_override_user_id( self, context, satosa_config): satosa_config["INTERNAL_ATTRIBUTES"]["user_id_from_attrs"] = [ "user_id", "domain" ] base = SATOSABase(satosa_config) internal_resp = InternalResponse(AuthenticationInformation("", "", "")) internal_resp.attributes = { "user_id": ["user"], "domain": ["@example.com"] } internal_resp.requester = "test_requester" context.state[satosa.base.STATE_KEY] = {"requester": "test_requester"} context.state[satosa.routing. STATE_KEY] = satosa_config["FRONTEND_MODULES"][0]["name"] UserIdHasher.save_state(InternalRequest(UserIdHashType.persistent, ""), context.state) base._auth_resp_callback_func(context, internal_resp) expected_user_id = UserIdHasher.hash_data( satosa_config["USER_ID_HASH_SALT"], "*****@*****.**") expected_user_id = UserIdHasher.hash_id( satosa_config["USER_ID_HASH_SALT"], expected_user_id, internal_resp.requester, context.state) assert internal_resp.user_id == expected_user_id
def handle_authn_request(self, context): """ Parse and verify the authentication request and pass it on to the backend. :type context: satosa.context.Context :rtype: oic.utils.http_util.Response :param context: the current context :return: HTTP response to the client """ # verify auth req (correct redirect_uri, contains nonce and response_type='id_token') request = urlencode(context.request) satosa_logging(LOGGER, logging.DEBUG, "Authn req from client: {}".format(request), context.state) info = self.provider.auth_init(request, request_class=AuthorizationRequest) if isinstance(info, Response): satosa_logging(LOGGER, logging.ERROR, "Error in authn req: {}".format(info.message), context.state) return info client_id = info["areq"]["client_id"] context.state.add(self.state_id, {"oidc_request": request}) hash_type = oidc_subject_type_to_hash_type( self.provider.cdb[client_id].get("subject_type", self.subject_type_default)) internal_req = InternalRequest( hash_type, client_id, self.provider.cdb[client_id].get("client_name")) return self.auth_req_callback_func(context, internal_req)
def test_start_auth_name_id_policy(self, sp_conf): """ Performs a complete test for the module satosa.backends.saml2. The flow should be accepted. """ samlbackend = SamlBackend( None, INTERNAL_ATTRIBUTES, { "config": sp_conf, "disco_srv": "https://my.dicso.com/role/idp.ds", "state_id": "saml_backend_test_id" }) test_state_key = "sauyghj34589fdh" state = State() state.add(test_state_key, "my_state") context = Context() context.state = state internal_req = InternalRequest(UserIdHashType.transient, None) resp = samlbackend.start_auth(context, internal_req) assert resp.status == "303 See Other", "Must be a redirect to the discovery server." disco_resp = parse_qs(urlparse(resp.message).query) sp_disco_resp = \ sp_conf["service"]["sp"]["endpoints"]["discovery_response"][0][0] assert "return" in disco_resp and disco_resp["return"][0].startswith(sp_disco_resp), \ "Not a valid return url in the call to the discovery server" assert "entityID" in disco_resp and disco_resp["entityID"][0] == sp_conf["entityid"], \ "Not a valid entity id in the call to the discovery server" request_info_tmp = context.state assert request_info_tmp.get( test_state_key) == "my_state", "Wrong state!"
def _get_id(requestor, user_id, hash_type): state = State() internal_request = InternalRequest(hash_type, requestor) UserIdHasher.save_state(internal_request, state) return UserIdHasher.hash_id(SALT, user_id, requestor, state)
def test_allow_takes_precedence_over_deny_all(self, target_context): requester = "test_requester" rules = { TARGET_ENTITY: { "allow": requester, "deny": ["*"], } } decide_service = self.create_decide_service(rules) req = InternalRequest(None, requester, None) assert decide_service.process(target_context, req) req.requester = "somebody else" with pytest.raises(SATOSAError): decide_service.process(target_context, req)
def test_start_auth_redirects_directly_to_mirrored_idp( self, context, idp_conf): context.internal_data["mirror.target_entity_id"] = urlsafe_b64encode( idp_conf["entityid"].encode("utf-8")).decode("utf-8") resp = self.samlbackend.start_auth(context, InternalRequest(None, None)) self.assert_redirect_to_idp(resp, idp_conf)
def test_allow_all_requesters(self, target_context, requester): rules = { TARGET_ENTITY: { "allow": ["*"], } } decide_service = self.create_decide_service(rules) req = InternalRequest(None, requester, None) assert decide_service.process(target_context, req)
def test_start_auth_redirects_directly_to_mirrored_idp( self, context, idp_conf): entityid = idp_conf["entityid"] entityid_bytes = entityid.encode("utf-8") entityid_b64_str = urlsafe_b64encode(entityid_bytes).decode("utf-8") context.decorate(Context.KEY_MIRROR_TARGET_ENTITYID, entityid_b64_str) resp = self.samlbackend.start_auth(context, InternalRequest(None, None)) self.assert_redirect_to_idp(resp, idp_conf)
def test_get_filter_attributes_with_sp_requested_attributes_without_friendlyname( self, idp_conf): sp_metadata_str = """<?xml version="1.0"?> <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="http://sp.example.com"> <md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:oasis:names:tc:SAML:2.0:protocol"> <md:AttributeConsumingService> <md:RequestedAttribute Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.10" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" isRequired="true"/> <md:RequestedAttribute Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.6" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" isRequired="true"/> <md:RequestedAttribute Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.1" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"/> <md:RequestedAttribute Name="urn:oid:0.9.2342.19200300.100.1.3" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" isRequired="true"/> <md:RequestedAttribute Name="urn:oid:2.16.840.1.113730.3.1.241" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"/> <md:RequestedAttribute Name="urn:oid:2.5.4.4" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"/> <md:RequestedAttribute Name="urn:oid:2.5.4.42" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"/> </md:AttributeConsumingService> </md:SPSSODescriptor> </md:EntityDescriptor> """ idp_conf["metadata"] = {"inline": [sp_metadata_str]} base = self.construct_base_url_from_entity_id(idp_conf["entityid"]) conf = { "idp_config": idp_conf, "endpoints": ENDPOINTS, "base": base, "state_id": "state_id" } internal_attributes = { "attributes": { attr_name: { "saml": [attr_name] } for attr_name in [ "edupersontargetedid", "edupersonprincipalname", "edupersonaffiliation", "mail", "displayname", "sn", "givenname" ] } } # no op mapping for saml attribute names samlfrontend = SamlFrontend(None, internal_attributes, conf) samlfrontend.register_endpoints(["testprovider"]) internal_req = InternalRequest( saml_name_format_to_hash_type(NAMEID_FORMAT_PERSISTENT), "http://sp.example.com", "Example SP") filtered_attributes = samlfrontend.get_filter_attributes( samlfrontend.idp, samlfrontend.idp.config.getattr("policy", "idp"), internal_req.requestor, None) assert set(filtered_attributes) == set([ "edupersontargetedid", "edupersonprincipalname", "edupersonaffiliation", "mail", "displayname", "sn", "givenname" ])
def test_deny_one_requester(self, target_context): rules = { TARGET_ENTITY: { "deny": ["test_requester"], } } decide_service = self.create_decide_service(rules) req = InternalRequest(None, "test_requester", None) with pytest.raises(SATOSAError): assert decide_service.process(target_context, req)
def test_deny_all_requesters(self, target_context, requester): rules = { TARGET_ENTITY: { "deny": ["*"], } } decide_service = self.create_decide_service(rules) req = InternalRequest(None, requester, None) with pytest.raises(SATOSAError): decide_service.process(target_context, req)
def test_redirect_to_idp_if_only_one_idp_in_metadata( self, context, sp_conf, idp_conf): 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") resp = samlbackend.start_auth(context, InternalRequest(None, None)) self.assert_redirect_to_idp(resp, idp_conf)
def test_missing_target_entity_id_from_context(self, context): target_entity = "entity1" rules = { target_entity: { "deny": ["*"], } } decide_service = self.create_decide_service(rules) req = InternalRequest(None, "test_requester", None) with pytest.raises(SATOSAError): decide_service.process(context, req)
def test_auth_req_callback_stores_state_for_consent( self, context, satosa_config): base = SATOSABase(satosa_config) context.target_backend = satosa_config["BACKEND_MODULES"][0]["name"] requester_name = [{ "lang": "en", "text": "Test EN" }, { "lang": "sv", "text": "Test SV" }] internal_req = InternalRequest(UserIdHashType.transient, None, requester_name) internal_req.approved_attributes = ["attr1", "attr2"] base._auth_req_callback_func(context, internal_req) assert context.state[ consent.STATE_KEY]["requester_name"] == internal_req.requester_name assert context.state[ consent.STATE_KEY]["filter"] == internal_req.approved_attributes
def test_always_redirect_to_discovery_service_if_using_mdq( self, context, sp_conf, idp_conf): # 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"] samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, { "sp_config": sp_conf, "disco_srv": DISCOSRV_URL, }, "base_url", "saml_backend") resp = samlbackend.start_auth(context, InternalRequest(None, None)) self.assert_redirect_to_discovery_server(resp, sp_conf)
def test_auth_resp_callback_func_respects_user_id_to_attr( self, context, satosa_config): satosa_config["INTERNAL_ATTRIBUTES"]["user_id_to_attr"] = "user_id" base = SATOSABase(satosa_config) internal_resp = InternalResponse(AuthenticationInformation("", "", "")) internal_resp.user_id = "user1234" context.state[satosa.base.STATE_KEY] = {"requester": "test_requester"} context.state[satosa.routing. STATE_KEY] = satosa_config["FRONTEND_MODULES"][0]["name"] UserIdHasher.save_state(InternalRequest(UserIdHashType.transient, ""), context.state) base._auth_resp_callback_func(context, internal_resp) assert internal_resp.attributes["user_id"] == [internal_resp.user_id]
def test_start_auth(self, context): context.path = 'facebook/sso/redirect' internal_request = InternalRequest(UserIdHashType.transient, 'test_requester') resp = self.fb_backend.start_auth(context, internal_request, mock_get_state) login_url = resp.message assert login_url.startswith(FB_AUTH_ENDPOINT) expected_params = { "client_id": CLIENT_ID, "state": mock_get_state.return_value, "response_type": "code", "redirect_uri": "%s/%s" % (BASE_URL, AUTHZ_PAGE) } actual_params = dict(parse_qsl(urlparse(login_url).query)) assert actual_params == expected_params
def test_full_flow(self, context, idp_conf, sp_conf): test_state_key = "test_state_key_456afgrh" response_binding = BINDING_HTTP_REDIRECT fakeidp = FakeIdP(USERS, config=IdPConfig().load(idp_conf, metadata_construction=False)) context.state[test_state_key] = "my_state" # start auth flow (redirecting to discovery server) resp = self.samlbackend.start_auth(context, InternalRequest(None, None)) self.assert_redirect_to_discovery_server(resp, sp_conf) # fake response from discovery server disco_resp = parse_qs(urlparse(resp.message).query) info = parse_qs(urlparse(disco_resp["return"][0]).query) info["entityID"] = idp_conf["entityid"] request_context = Context() request_context.request = info request_context.state = context.state # pass discovery response to backend and check that it redirects to the selected IdP resp = self.samlbackend.disco_response(request_context) self.assert_redirect_to_idp(resp, idp_conf) # fake auth response to the auth request req_params = dict(parse_qsl(urlparse(resp.message).query)) url, fake_idp_resp = fakeidp.handle_auth_req( req_params["SAMLRequest"], req_params["RelayState"], BINDING_HTTP_REDIRECT, "testuser1", response_binding=response_binding) response_context = Context() response_context.request = fake_idp_resp response_context.state = request_context.state # pass auth response to backend and verify behavior self.samlbackend.authn_response(response_context, response_binding) context, internal_resp = self.samlbackend.auth_callback_func.call_args[ 0] assert self.samlbackend.name not in context.state assert context.state[test_state_key] == "my_state" self.assert_authn_response(internal_resp)
def test_authn_response(self): context = Context() context.path = 'facebook/sso/redirect' context.state = State() internal_request = InternalRequest(UserIdHashType.transient, 'http://localhost:8087/sp.xml') get_state = Mock() get_state.return_value = STATE resp = self.fb_backend.start_auth(context, internal_request, get_state) context.cookie = resp.headers[0][1] context.request = {"code": FB_RESPONSE_CODE, "state": STATE} # context.request = json.dumps(context.request) self.fb_backend.auth_callback_func = self.verify_callback tmp_consumer = self.fb_backend.get_consumer() tmp_consumer.do_access_token_request = self.verify_do_access_token_request self.fb_backend.get_consumer = Mock() self.fb_backend.get_consumer.return_value = tmp_consumer self.fb_backend.request_fb = self.verify_request_fb self.fb_backend.authn_response(context)
def test_start_auth(self): context = Context() context.path = 'facebook/sso/redirect' context.state = State() internal_request = InternalRequest(UserIdHashType.transient, 'http://localhost:8087/sp.xml') get_state = Mock() get_state.return_value = STATE resp = self.fb_backend.start_auth(context, internal_request, get_state) # assert resp.headers[0][0] == "Set-Cookie", "Not the correct return cookie" # assert len(resp.headers[0][1]) > 1, "Not the correct return cookie" resp_url = resp.message.split("?") test_url = FB_REDIRECT_URL.split("?") resp_attr = parse_qs(resp_url[1]) test_attr = parse_qs(test_url[1]) assert resp_url[0] == test_url[0] assert len(resp_attr) == len(test_attr), "Redirect url is not correct!" for key in test_attr: assert key in resp_attr, "Redirect url is not correct!" assert test_attr[key] == resp_attr[ key], "Redirect url is not correct!"
def test_auth_resp_callback_func_hashes_all_specified_attributes( self, context, satosa_config): satosa_config["INTERNAL_ATTRIBUTES"]["hash"] = ["user_id", "mail"] base = SATOSABase(satosa_config) attributes = { "user_id": ["user"], "mail": ["*****@*****.**", "*****@*****.**"] } internal_resp = InternalResponse(AuthenticationInformation("", "", "")) internal_resp.attributes = copy.copy(attributes) internal_resp.user_id = "test_user" UserIdHasher.save_state(InternalRequest(UserIdHashType.transient, ""), context.state) context.state[satosa.base.STATE_KEY] = {"requester": "test_requester"} context.state[satosa.routing. STATE_KEY] = satosa_config["FRONTEND_MODULES"][0]["name"] base._auth_resp_callback_func(context, internal_resp) for attr in satosa_config["INTERNAL_ATTRIBUTES"]["hash"]: assert internal_resp.attributes[attr] == [ UserIdHasher.hash_data(satosa_config["USER_ID_HASH_SALT"], v) for v in attributes[attr] ]
def test_redirect_to_idp_if_only_one_idp_in_metadata( self, sp_conf, idp_conf): sp_conf["metadata"]["inline"] = [ create_metadata_from_config_dict(idp_conf) ] samlbackend = SamlBackend(None, INTERNAL_ATTRIBUTES, { "config": sp_conf, "state_id": "saml_backend_test_id" }) state = State() state.add("test", "state") context = Context() context.state = state internal_req = InternalRequest(UserIdHashType.transient, None) resp = samlbackend.start_auth(context, internal_req) assert resp.status == "303 See Other" parsed = urlparse(resp.message) assert "{parsed.scheme}://{parsed.netloc}{parsed.path}".format( parsed=parsed) == \ idp_conf["service"]["idp"]["endpoints"]["single_sign_on_service"][0][0] assert "SAMLRequest" in parse_qs(parsed.query)
def internal_request(): req = InternalRequest(UserIdHashType.persistent, "example_requestor") req.add_filter(FILTER) return req
def handle_request(self, context): internal_req = InternalRequest(UserIdHashType.transient, "test_client", None) return self.auth_req_callback_func(context, internal_req)
def _handle_authn_request(self, context, binding_in, idp): """ See doc for handle_authn_request method. :type context: satosa.context.Context :type binding_in: str :type idp: saml.server.Server :rtype: satosa.response.Response :param context: The current context :param binding_in: The pysaml binding type :param idp: The saml frontend idp server :return: response """ request = context.request try: extracted_request = self.extract_request(idp, request["SAMLRequest"], binding_in, context.state) except UnknownPrincipal as excp: satosa_logging(LOGGER, logging.ERROR, "UnknownPrincipal", context.state, exc_info=True) return ServiceError("UnknownPrincipal: %s" % excp) except UnsupportedBinding as excp: satosa_logging(LOGGER, logging.ERROR, "UnsupportedBinding", context.state, exc_info=True) return ServiceError("UnsupportedBinding: %s" % excp) _binding = extracted_request["resp_args"]["binding"] if extracted_request["response"]: # An error response http_args = idp.apply_binding( _binding, "%s" % extracted_request["response"], extracted_request["resp_args"]["destination"], request["RelayState"], response=True, ) satosa_logging(LOGGER, logging.DEBUG, "HTTPargs: %s" % http_args, context.state, exc_info=True) return response(_binding, http_args) else: try: context.internal_data["saml2.target_entity_id"] = request["entityID"] except KeyError: pass request_state = self.save_state( context, idp.response_args(extracted_request["authn_req"]), request["RelayState"] ) context.state.add(self.state_id, request_state) extensions = idp.metadata.extension( extracted_request["resp_args"]["sp_entity_id"], "spsso_descriptor", "urn:oasis:names:tc:SAML:metadata:ui&UIInfo", ) requester_name = None try: requester_name = extensions[0]["display_name"] except IndexError: pass name_format = None if "name_id_policy" in extracted_request["req_args"]: name_format = saml_name_format_to_hash_type(extracted_request["req_args"]["name_id_policy"].format) if name_format is None: # default to requesting transient name id name_format = UserIdHashType.transient internal_req = InternalRequest(name_format, extracted_request["resp_args"]["sp_entity_id"], requester_name) # Get attribute filter idp_policy = idp.config.getattr("policy", "idp") if idp_policy: attribute_filter = self.get_filter_attributes(idp, idp_policy, internal_req.requestor, context.state) internal_req.add_filter(attribute_filter) return self.auth_req_callback_func(context, internal_req)
def internal_request(self): req = InternalRequest(UserIdHashType.persistent, "example_requester") req.approved_attributes = FILTER + ["sn"] return req