def test_session_state_in_auth_req_for_session_support(self): provider = Provider( "foo", SessionDB(SERVER_INFO["issuer"]), CDB, AUTHN_BROKER, USERINFO, AUTHZ, verify_client, SYMKEY, urlmap=URLMAP, keyjar=KEYJAR, capabilities={"check_session_iframe": "https://op.example.com/check_session"}, ) req_args = { "scope": ["openid"], "redirect_uri": "http://localhost:8087/authz", "response_type": ["code"], "client_id": "a1b2c3", } areq = AuthorizationRequest(**req_args) resp = provider.authorization_endpoint(request=areq.to_urlencoded()) aresp = self.cons.parse_response(AuthorizationResponse, resp.message, sformat="urlencoded") assert "session_state" in aresp
def test_session_state_in_auth_req_for_session_support(self): provider = Provider(SERVER_INFO["issuer"], SessionDB(SERVER_INFO["issuer"]), CDB, AUTHN_BROKER, USERINFO, AUTHZ, verify_client, SYMKEY, urlmap=URLMAP, keyjar=KEYJAR) provider.capabilities.update( {"check_session_iframe": "https://op.example.com/check_session"}) req_args = { "scope": ["openid"], "redirect_uri": "http://localhost:8087/authz", "response_type": ["code"], "client_id": "number5" } areq = AuthorizationRequest(**req_args) resp = provider.authorization_endpoint(request=areq.to_urlencoded()) aresp = self.cons.parse_response(AuthorizationResponse, resp.message, sformat="urlencoded") assert "session_state" in aresp
def __init__(self, name, sdb, cdb, authn_broker, userinfo, authz, client_authn, symkey, trusted_domains, verify_signer_ssl=True, **kwarg): Provider.__init__(self, name, sdb, cdb, authn_broker, userinfo, authz, client_authn, symkey, **kwarg) self.trusted_domains = trusted_domains self.verify_signer_ssl = verify_signer_ssl
def __init__(self, name, sdb, cdb, userinfo, client_authn, urlmap=None, ca_certs="", keyjar=None, hostname="", dist_claims_mode=None): Provider.__init__(self, name, sdb, cdb, None, userinfo, None, client_authn, "", urlmap, ca_certs, keyjar, hostname) if keyjar is None: keyjar = KeyJar(ca_certs) for cid, _dic in cdb.items(): try: keyjar.add_symmetric(cid, _dic["client_secret"], ["sig", "ver"]) except KeyError: pass self.srvmethod = OICCServer(keyjar=keyjar) self.dist_claims_mode = dist_claims_mode self.info_store = {} self.claims_userinfo_endpoint = ""
def setup_consumer(self, session_db_factory): client_config = { "client_id": CLIENT_ID, "client_authn_method": CLIENT_AUTHN_METHOD, } self.consumer = Consumer(DictSessionBackend(), CONFIG, client_config, SERVER_INFO) self.consumer.keyjar = CLIKEYS self.consumer.redirect_uris = ["https://example.com/authz"] self.consumer.client_secret = "hemlig" self.consumer.secret_type = "basic" self.consumer.issuer = ISSUER_ID self.provider = Provider( ISSUER_ID, session_db_factory(ISSUER_ID), CDB, AUTHN_BROKER, USERINFO, AUTHZ, verify_client, SYMKEY, urlmap=URLMAP, keyjar=SRVKEYS, ) self.provider.baseurl = self.provider.name
def __init__( self, name, sdb, cdb, userinfo, client_authn, urlmap=None, ca_certs="", keyjar=None, hostname="", dist_claims_mode=None, ): Provider.__init__( self, name, sdb, cdb, None, userinfo, None, client_authn, "", urlmap, ca_certs, keyjar, hostname ) if keyjar is None: keyjar = KeyJar(ca_certs) for cid, _dic in cdb.items(): try: keyjar.add_symmetric(cid, _dic["client_secret"], ["sig", "ver"]) except KeyError: pass self.srvmethod = OICCServer(keyjar=keyjar) self.dist_claims_mode = dist_claims_mode self.info_store = {} self.claims_userinfo_endpoint = ""
def __init__(self, name, sdb, cdb, function, userdb, urlmap=None, debug=0, ca_certs="", jwt_keys=None): Provider.__init__(self, name, sdb, cdb, function, userdb, urlmap, ca_certs, jwt_keys) self.test_mode = True self.trace_log = {} self.sessions = [] self.max_sessions = 100
def test_provider_key_setup(): provider = Provider("pyoicserv", SessionDB(), None, None, None) provider.baseurl = "http://www.example.com/" provider.key_setup("static", sig={"format": "jwk", "alg": "rsa"}) keys = provider.keyjar.get_signing_key("rsa") assert len(keys) == 1 assert provider.jwk[0] == "http://www.example.com/static/jwk.json"
def test_provider_key_setup(self): provider = Provider("pyoicserv", SessionDB(SERVER_INFO["issuer"]), None, None, None, None, None, "") provider.baseurl = "http://www.example.com/" provider.key_setup("static", sig={"format": "jwk", "alg": "RSA"}) keys = provider.keyjar.get_signing_key("RSA") assert len(keys) == 1 assert provider.jwks_uri == "http://www.example.com/static/jwks"
def test_provider_key_setup(): provider = Provider("pyoicserv", SessionDB(SERVER_INFO["issuer"]), None, None, None, None, None, "") provider.baseurl = "http://www.example.com/" provider.key_setup("static", sig={"format": "jwk", "alg": "RSA"}) keys = provider.keyjar.get_signing_key("RSA") assert len(keys) == 1 assert provider.jwks_uri == "http://www.example.com/static/jwks"
def test_provider_key_setup(self, tmpdir): path = tmpdir.strpath provider = Provider("pyoicserv", SessionDB(SERVER_INFO["issuer"]), None, None, None, None, None, "") provider.baseurl = "http://www.example.com" provider.key_setup(path, path, sig={"format": "jwk", "alg": "RSA"}) keys = provider.keyjar.get_signing_key("RSA") assert len(keys) == 1 assert provider.jwks_uri == "http://www.example.com/{}/jwks".format(path)
def test_provider_key_setup(self, tmpdir): path = tmpdir.strpath provider = Provider("pyoicserv", SessionDB(SERVER_INFO["issuer"]), None, None, None, None, None, "") provider.baseurl = "http://www.example.com" provider.key_setup(path, path, sig={"format": "jwk", "alg": "RSA"}) keys = provider.keyjar.get_signing_key("RSA") assert len(keys) == 1 assert provider.jwks_uri == "http://www.example.com/{}/jwks".format( path)
def create_provider(self): self.provider = Provider("pyoicserv", SessionDB(SERVER_INFO["issuer"]), CDB, AUTHN_BROKER, USERINFO, AUTHZ, verify_client, SYMKEY, urlmap=URLMAP, keyjar=KEYJAR) self.provider.baseurl = self.provider.name self.cons = Consumer({}, CONSUMER_CONFIG, CLIENT_CONFIG, server_info=SERVER_INFO, ) self.cons.behaviour = { "request_object_signing_alg": DEF_SIGN_ALG["openid_request_object"]} self.cons.keyjar[""] = KC_RSA
def __init__( self, name, sdb, cdb, userinfo, client_authn, urlmap=None, keyjar=None, hostname="", dist_claims_mode=None, verify_ssl=None, settings=None, ): self.settings = settings or OicProviderSettings() if verify_ssl is not None: warnings.warn( "`verify_ssl` is deprecated, please use `settings` instead if you need to set a non-default value.", DeprecationWarning, stacklevel=2, ) self.settings.verify_ssl = verify_ssl Provider.__init__( self, name, sdb, cdb, None, userinfo, None, client_authn, None, urlmap, keyjar, hostname, settings=self.settings, ) if keyjar is None: keyjar = KeyJar(verify_ssl=verify_ssl) for cid, _dic in cdb.items(): try: keyjar.add_symmetric(cid, _dic["client_secret"], ["sig", "ver"]) except KeyError: pass self.srvmethod = OICCServer(keyjar=keyjar) self.dist_claims_mode = dist_claims_mode self.info_store = {} # type: Dict[str, Any] self.claims_userinfo_endpoint = ""
def test_registered_redirect_uri_with_query_component(self): provider2 = Provider("FOOP", {}, {}, None, None, None, None, "") rr = RegistrationRequest(operation="register", redirect_uris=["http://example.org/cb?foo=bar"], response_types=["code"]) registration_req = rr.to_json() resp = provider2.registration_endpoint(request=registration_req) regresp = RegistrationResponse().from_json(resp.message) print regresp.to_dict() faulty = [ "http://example.org/cb", "http://example.org/cb/foo", "http://example.org/cb?got=you", "http://example.org/cb?foo=you" "http://example.org/cb?foo=bar&got=you", "http://example.org/cb?foo=you&foo=bar" ] correct = [ "http://example.org/cb?foo=bar", ] cid = regresp["client_id"] for ruri in faulty: areq = AuthorizationRequest(redirect_uri=ruri, client_id=cid, scope="openid", response_type="code") print areq try: provider2._verify_redirect_uri(areq) except RedirectURIError: pass for ruri in correct: areq = AuthorizationRequest(redirect_uri=ruri, client_id=cid, scope="openid", response_type="code") resp = provider2._verify_redirect_uri(areq) print resp assert resp is None
def _create_op(self, issuer, endpoint_baseurl, jwks_uri): """ Create the necessary Provider instance. :type issuer: str :type endpoint_baseurl: str :type jwks_uri: str :param issuer: issuer URL for the OP :param endpoint_baseurl: baseurl to build endpoint URL from :param jwks_uri: URL to where the JWKS will be published """ kj = KeyJar() signing_key = KeyBundle(source="file://{}".format(self.conf["signing_key_path"]), fileformat="der", keyusage=["sig"]) kj.add_kb("", signing_key) capabilities = { "response_types_supported": ["id_token"], "id_token_signing_alg_values_supported": [self.sign_alg], "response_modes_supported": ["fragment", "query"], "subject_types_supported": ["public", "pairwise"], "grant_types_supported": ["implicit"], "claim_types_supported": ["normal"], "claims_parameter_supported": True, "request_parameter_supported": False, "request_uri_parameter_supported": False, } if "client_db_path" in self.conf: cdb = shelve_wrapper.open(self.conf["client_db_path"]) else: cdb = {} # client db in memory only self.provider = Provider(issuer, None, cdb, None, None, None, None, None, keyjar=kj, capabilities=capabilities, jwks_uri=jwks_uri) self.provider.baseurl = endpoint_baseurl self.provider.endp = [RegistrationEndpoint, AuthorizationEndpoint]
def test_key_rollover(self): provider2 = Provider("FOOP", {}, {}, None, None, None, None, "") provider2.keyjar = KEYJAR # Number of KeyBundles assert len(provider2.keyjar.issuer_keys[""]) == 1 kb = ec_init({"type": "EC", "crv": "P-256", "use": ["sig"]}) provider2.do_key_rollover(json.loads(kb.jwks()), "b%d") assert len(provider2.keyjar.issuer_keys[""]) == 2 kb = ec_init({"type": "EC", "crv": "P-256", "use": ["sig"]}) provider2.do_key_rollover(json.loads(kb.jwks()), "b%d") assert len(provider2.keyjar.issuer_keys[""]) == 3 provider2.remove_inactive_keys(-1) assert len(provider2.keyjar.issuer_keys[""]) == 2
def __init__(self): op_base_url = TestConfiguration.get_instance().rp_config.OP_URL self.provider = Provider("https://op.tester.se/", SessionDB(op_base_url), CDB, AUTHN_BROKER, USERINFO, AUTHZ, verify_client, SYMKEY, urlmap=None, keyjar=KEYJAR) self.provider.baseurl = TestConfiguration.get_instance( ).rp_config.OP_URL self.op_base = TestConfiguration.get_instance().rp_config.OP_URL self.redirect_urls = TestConfiguration.get_instance( ).rp_config.CLIENTS[PROVIDER]["client_info"]["redirect_uris"]
def __init__(self, name, sdb, cdb, function, userdb, urlmap=None, debug=0, ca_certs="", jwt_keys=None): Provider.__init__(self, name, sdb, cdb, function, userdb, urlmap, ca_certs, jwt_keys) if jwt_keys is None: jwt_keys = [] for cid, _dic in cdb.items(): jwt_keys.append([_dic["client_secret"], "hmac", "sig", cid]) jwt_keys.append([_dic["client_secret"], "hmac", "ver", cid]) self.srvmethod = OICCServer(jwt_keys=jwt_keys) self.keystore = self.srvmethod.keystore self.claims_mode = "aggregate" self.info_store = {} self.claims_userinfo_endpoint = ""
def _create_op(self, issuer, endpoint_baseurl, jwks_uri): """ Create the necessary Provider instance. :type issuer: str :type endpoint_baseurl: str :type jwks_uri: str :param issuer: issuer URL for the OP :param endpoint_baseurl: baseurl to build endpoint URL from :param jwks_uri: URL to where the JWKS will be published """ kj = KeyJar() signing_key = KeyBundle(source="file://{}".format( self.conf["signing_key_path"]), fileformat="der", keyusage=["sig"]) kj.add_kb("", signing_key) capabilities = { "response_types_supported": ["id_token"], "id_token_signing_alg_values_supported": [self.sign_alg], "response_modes_supported": ["fragment", "query"], "subject_types_supported": ["public", "pairwise"], "grant_types_supported": ["implicit"], "claim_types_supported": ["normal"], "claims_parameter_supported": True, "request_parameter_supported": False, "request_uri_parameter_supported": False, } if "client_db_path" in self.conf: cdb = shelve_wrapper.open(self.conf["client_db_path"]) else: cdb = {} # client db in memory only self.provider = Provider(issuer, None, cdb, None, None, None, None, None, keyjar=kj, capabilities=capabilities, jwks_uri=jwks_uri) self.provider.baseurl = endpoint_baseurl self.provider.endp = [RegistrationEndpoint, AuthorizationEndpoint]
def test_registered_redirect_uri_with_query_component(): provider2 = Provider("FOOP", {}, {}, None, None) environ = {} rr = RegistrationRequest(operation="register", redirect_uris=["http://example.org/cb?foo=bar"]) registration_req = rr.to_urlencoded() resp = provider2.registration_endpoint(environ, start_response, query=registration_req) regresp = RegistrationResponse().from_json(resp[0]) print regresp.to_dict() faulty = [ "http://example.org/cb", "http://example.org/cb/foo", "http://example.org/cb?got=you", "http://example.org/cb?foo=you" ] correct = [ "http://example.org/cb?foo=bar", "http://example.org/cb?foo=bar&got=you", "http://example.org/cb?foo=bar&foo=you" ] for ruri in faulty: areq = AuthorizationRequest(redirect_uri=ruri, client_id=regresp["client_id"], scope="openid", response_type="code") print areq assert provider2._verify_redirect_uri(areq) != None for ruri in correct: areq = AuthorizationRequest(redirect_uri= ruri, client_id=regresp["client_id"]) resp = provider2._verify_redirect_uri(areq) print resp assert resp == None
def __init__( self, name, sdb, cdb, userinfo, client_authn, urlmap=None, keyjar=None, hostname="", dist_claims_mode=None, verify_ssl=True, ): Provider.__init__( self, name, sdb, cdb, None, userinfo, None, client_authn, None, urlmap, keyjar, hostname, verify_ssl=verify_ssl, ) if keyjar is None: keyjar = KeyJar(verify_ssl=verify_ssl) for cid, _dic in cdb.items(): try: keyjar.add_symmetric(cid, _dic["client_secret"], ["sig", "ver"]) except KeyError: pass self.srvmethod = OICCServer(keyjar=keyjar) self.dist_claims_mode = dist_claims_mode self.info_store = {} # type: Dict[str, Any] self.claims_userinfo_endpoint = ""
def setUp(self): self.server = Provider( "example", sentinel.session_db, {}, None, sentinel.userinfo, sentinel.authz, sentinel.client_authn, ) self.jwt = JWS({"iss": "some_cid"}).sign_compact()
def provider(session_db): issuer = "https://op.example.com" client_db = {} # type: Dict[str, Any] verification_function = verify_client authz_handler = AuthzHandling() symkey = None user_info_store = None authn_broker = None return Provider(issuer, session_db, client_db, authn_broker, user_info_store, authz_handler, verification_function, symkey)
def setup_class(self): self.server = Provider("pyoicserv", SessionDB(SERVER_INFO["issuer"]), CDB, AUTHN_BROKER, USERINFO, AUTHZ, verify_client, SYMKEY, urlmap=URLMAP, keyjar=KEYJAR) self.cons = Consumer({}, CONSUMER_CONFIG, CLIENT_CONFIG, server_info=SERVER_INFO, ) self.cons.behaviour = {"request_object_signing_alg": DEF_SIGN_ALG["openid_request_object"]} self.cons.debug = True self.cons.keyjar[""] = KC_RSA
def __init__(self, name, sdb, cdb, authn_broker, userinfo, authz, client_authn, symkey, urlmap=None, keyjar=None, hostname="", configuration=None, ca_certs="", template_lookup=None, verify_login_template=None, base_url=""): OIDCProvider.__init__(self, name, sdb, cdb, authn_broker, userinfo, authz, client_authn, symkey, urlmap, ca_certs, keyjar, hostname, template_lookup, verify_login_template) UmaAS.__init__(self, configuration, baseurl=base_url) if keyjar: self.keyjar = keyjar else: self.keyjar = KeyJar() self.hostname = hostname or socket.gethostname #self.jwks_uri = [] self.endp = UmaAS.endp[:] self.endp.extend(OIDCProvider.endp)
def provider(): issuer = "https://op.foo.com" client_db = {} session_db = SessionDB(issuer), verification_function = verify_client authz_handler = AuthzHandling() symkey = None user_info_store = None authn_broker = None return Provider(issuer, session_db, client_db, authn_broker, user_info_store, authz_handler, verification_function, symkey)
def main(): parser = argparse.ArgumentParser(description='Example OIDC Provider.') parser.add_argument("-p", "--port", default=80, type=int) parser.add_argument("-b", "--base", default="https://localhost", type=str) parser.add_argument("-d", "--debug", action="store_true") parser.add_argument("settings") args = parser.parse_args() # Load configuration with open(args.settings, "r") as f: settings = yaml.load(f) baseurl = args.base.rstrip("/") issuer = "{base}:{port}".format(base=baseurl, port=args.port) template_dirs = settings["server"].get("template_dirs", "templates") jinja_env = Environment(loader=FileSystemLoader(template_dirs)) authn_broker, auth_routing = setup_authentication_methods(settings["authn"], jinja_env) # Setup userinfo userinfo_conf = settings["userinfo"] cls = make_cls_from_name(userinfo_conf["class"]) i = cls(**userinfo_conf["kwargs"]) userinfo = UserInfo(i) client_db = {} provider = Provider(issuer, SessionDB(baseurl), client_db, authn_broker, userinfo, AuthzHandling(), verify_client, None) provider.baseurl = issuer provider.symkey = rndstr(16) # Setup keys path = os.path.join(os.path.dirname(__file__), "static") try: os.makedirs(os.path.dirname(path)) except OSError, e: if e.errno != errno.EEXIST: raise e pass
def create_provider(self): self.provider = Provider(SERVER_INFO["issuer"], SessionDB(SERVER_INFO["issuer"]), CDB, AUTHN_BROKER, USERINFO, AUTHZ, verify_client, SYMKEY, urlmap=URLMAP, keyjar=KEYJAR) self.provider.baseurl = self.provider.name self.cons = Consumer({}, CONSUMER_CONFIG, CLIENT_CONFIG, server_info=SERVER_INFO, ) self.cons.behaviour = { "request_object_signing_alg": DEF_SIGN_ALG["openid_request_object"]} self.cons.keyjar[""] = KC_RSA
def main(): parser = argparse.ArgumentParser(description='Example OIDC Provider.') parser.add_argument("-p", "--port", default=80, type=int) parser.add_argument("-b", "--base", default="https://localhost", type=str) parser.add_argument("-d", "--debug", action="store_true") parser.add_argument("settings") args = parser.parse_args() # Load configuration with open(args.settings, "r") as f: settings = yaml.load(f) issuer = args.base.rstrip("/") template_dirs = settings["server"].get("template_dirs", "templates") jinja_env = Environment(loader=FileSystemLoader(template_dirs)) authn_broker, auth_routing = setup_authentication_methods( settings["authn"], jinja_env) # Setup userinfo userinfo_conf = settings["userinfo"] cls = make_cls_from_name(userinfo_conf["class"]) i = cls(**userinfo_conf["kwargs"]) userinfo = UserInfo(i) client_db = {} provider = Provider(issuer, SessionDB(issuer), client_db, authn_broker, userinfo, AuthzHandling(), verify_client, None) provider.baseurl = issuer provider.symkey = rndstr(16) # Setup keys path = os.path.join(os.path.dirname(__file__), "static") try: os.makedirs(path) except OSError, e: if e.errno != errno.EEXIST: raise e pass
def registration_endpoint(self, environ, start_response, **kwargs): before = self.cdb.keys() res = Provider.l_registration_endpoint(self, environ, **kwargs) after = [i for i in self.cdb.keys() if i not in before] print >> sys.stderr, after cid = after[0] for err in ["A", "B", "C"]: self.cdb["%s_ERR%s" % (cid,err)] = self.cdb[cid] return res(environ, start_response)
def test_registered_redirect_uri_without_query_component(): provider = Provider("FOO", {}, {}, None, None) rr = RegistrationRequest(operation="register", redirect_uris=["http://example.org/cb"]) registration_req = rr.to_urlencoded() provider.registration_endpoint({}, start_response, query=registration_req) correct = [ "http://example.org/cb", "http://example.org/cb/foo", "http://example.org/cb?got=you", "http://example.org/cb/foo?got=you" ] faulty = [ "http://example.org/foo", "http://example.com/cb", ] for ruri in faulty: areq = AuthorizationRequest(redirect_uri=ruri, client_id=provider.cdb.keys()[0], response_type="code", scope="openid") print areq assert provider._verify_redirect_uri(areq) != None for ruri in correct: areq = AuthorizationRequest(redirect_uri= ruri, client_id=provider.cdb.keys()[0]) resp = provider._verify_redirect_uri(areq) if resp: print resp.message assert resp is None
def create_provider(self, session_db_factory): self.provider = Provider( SERVER_INFO["issuer"], session_db_factory(SERVER_INFO["issuer"]), self.CDB, AUTHN_BROKER, USERINFO, AUTHZ, verify_client, SYMKEY, urlmap=URLMAP, keyjar=KEYJAR, ) self.provider.baseurl = self.provider.name self.provider.logout_verify_url = "https://127.0.0.1/logout_verify.html" self.cons = Consumer( DictSessionBackend(), CONSUMER_CONFIG.copy(), CLIENT_CONFIG, server_info=SERVER_INFO, ) self.cons.behaviour = { "request_object_signing_alg": DEF_SIGN_ALG["openid_request_object"] } self.cons.keyjar[""] = KC_RSA self.cons.keyjar.import_jwks(self.provider.keyjar.export_jwks(), self.cons.issuer) self.cons2 = Consumer({}, CONSUMER_CONFIG.copy(), CLIENT_CONFIG_2, server_info=SERVER_INFO) self.cons2.behaviour = { "request_object_signing_alg": DEF_SIGN_ALG["openid_request_object"] } self.cons2.keyjar[""] = KC_RSA
def test_registered_redirect_uri_without_query_component(self): provider = Provider("FOO", {}, {}, None, None, None, None, "") rr = RegistrationRequest(operation="register", redirect_uris=["http://example.org/cb"], response_types=["code"]) registration_req = rr.to_json() provider.registration_endpoint(request=registration_req) correct = [ "http://example.org/cb", "http://example.org/cb/foo", ] faulty = [ "http://example.org/foo", "http://example.com/cb", "http://example.org/cb?got=you", "http://example.org/cb/foo?got=you" ] cid = self._client_id(provider.cdb) for ruri in faulty: areq = AuthorizationRequest(redirect_uri=ruri, client_id=cid, response_type="code", scope="openid") print areq try: provider._verify_redirect_uri(areq) assert False except RedirectURIError: pass for ruri in correct: areq = AuthorizationRequest(redirect_uri=ruri, client_id=cid, response_type="code", scope="openid") print areq try: provider._verify_redirect_uri(areq) except RedirectURIError, err: print err assert False
def __init__(self): op_base_url = TestConfiguration.get_instance().rp_config.OP_URL self.provider = Provider( "https://op.tester.se/", SessionDB(op_base_url), CDB, AUTHN_BROKER, USERINFO, AUTHZ, verify_client, SYMKEY, urlmap=None, keyjar=KEYJAR ) self.provider.baseurl = TestConfiguration.get_instance().rp_config.OP_URL self.op_base = TestConfiguration.get_instance().rp_config.OP_URL self.redirect_urls = TestConfiguration.get_instance().rp_config.CLIENTS[PROVIDER]["client_info"][ "redirect_uris"]
def test_registered_redirect_uri_with_query_component(self): provider2 = Provider("FOOP", {}, {}, None, None, None, None, "") rr = RegistrationRequest( operation="register", redirect_uris=["http://example.org/cb?foo=bar"], response_types=["code"]) registration_req = rr.to_json() resp = provider2.registration_endpoint(request=registration_req) regresp = RegistrationResponse().from_json(resp.message) print regresp.to_dict() faulty = [ "http://example.org/cb", "http://example.org/cb/foo", "http://example.org/cb?got=you", "http://example.org/cb?foo=you" "http://example.org/cb?foo=bar&got=you", "http://example.org/cb?foo=you&foo=bar" ] correct = [ "http://example.org/cb?foo=bar", ] cid = regresp["client_id"] for ruri in faulty: areq = AuthorizationRequest(redirect_uri=ruri, client_id=cid, scope="openid", response_type="code") print areq try: provider2._verify_redirect_uri(areq) except RedirectURIError: pass for ruri in correct: areq = AuthorizationRequest(redirect_uri=ruri, client_id=cid, scope="openid", response_type="code") resp = provider2._verify_redirect_uri(areq) print resp assert resp is None
def _get_user_info(self, user_attributes, requested_claims=None, scopes=None): """ Filter user attributes to return to the client (as claims in the ID Token) based on what was requested in request 'claims' parameter and in the 'scope'. :type user_attributes: dict[str, str] :type requested_claims: dict[str, Optional[dict]] :type scopes: list[str] :rtype: dict[str, str] :param user_attributes: attributes provided by the backend :param requested_claims: claims requested by the client through the 'claims' request param :param scopes: the scopes requested by the client :return: all attributes/claims to return to the client """ requested_claims = requested_claims or {} scopes = scopes or [] claims_requested_by_scope = Provider._scope2claims(scopes) claims_requested_by_scope.update( requested_claims) # let explicit claims request override scope return UserInfo().filter(user_attributes, claims_requested_by_scope)
def __init__(self, base_url, client_metadata_func): # Read OP configuration from file with open("conf/op_config.json", "r") as f: op_capabilities = json.load(f) for key, value in op_capabilities.iteritems(): if isinstance(value, basestring): op_capabilities[key] = value.format(base=base_url) # replace placeholder with the actual base name self.OP = Provider(base_url, {}, client_metadata_func, None, None, None, None, None, capabilities=op_capabilities) self.OP.baseurl = op_capabilities["issuer"] # Setup up keys for signing and encrypting self.OP.keyjar = KeyJar() kb = keybundle_from_local_file("inAcademia", "RSA", ["sig", "enc"]) self.OP.keyjar.add_kb("", kb) try: file_name = "static/jwks.json" dump_jwks([kb], file_name) self.OP.jwks_uri.append("{}/{}".format(base_url, file_name)) except Exception as e: logger.exception("Signing and encryption keys could not be written to jwks.json.") raise
"template": {"form_post": "form_response.mako"}, # "template_args": {"form_post": {"action": "form_post"}} } # Should I care about verifying the certificates used by other entities if args.insecure: kwargs["verify_ssl"] = False else: kwargs["verify_ssl"] = True if args.capabilities: kwargs["capabilities"] = json.loads(open(args.capabilities).read()) else: pass OAS = Provider(_issuer, SessionDB(_issuer), cdb, ac, None, authz, verify_client, config.SYM_KEY, **kwargs) OAS.baseurl = _issuer for authn in ac: authn.srv = OAS if config.USERINFO == "SIMPLE": # User info is a simple dictionary in this case statically defined in # the configuration file OAS.userinfo = UserInfo(config.USERDB) elif config.USERINFO == "SAML": OAS.userinfo = UserInfo(config.SAML) elif config.USERINFO == "AA": OAS.userinfo = AaUserInfo(config.SP_CONFIG, _issuer, config.SAML) else: raise Exception("Unsupported userinfo source")
kwargs = {"verify_ssl": False} else: kwargs = {"verify_ssl": True} if args.test: URLS.append((r'tracelog', trace_log)) OAS = TestProvider(config.issuer, SessionDB(config.baseurl), cdb, ac, None, authz, config.SYM_KEY) elif args.XpressConnect: from XpressConnect import XpressConnectProvider OAS = XpressConnectProvider(config.issuer, SessionDB(config.baseurl), cdb, ac, None, authz, verify_client, config.SYM_KEY) else: OAS = Provider(config.issuer, SessionDB(config.baseurl), cdb, ac, None, authz, verify_client, config.SYM_KEY, **kwargs) try: OAS.cookie_ttl = config.COOKIETTL except AttributeError: pass try: OAS.cookie_name = config.COOKIENAME except AttributeError: pass #print URLS if args.debug: OAS.debug = True if args.test:
def main(): parser = argparse.ArgumentParser(description='Example OIDC Provider.') parser.add_argument("-p", "--port", default=80, type=int) parser.add_argument("-b", "--base", default="https://localhost", type=str) parser.add_argument("-d", "--debug", action="store_true") parser.add_argument("settings") args = parser.parse_args() # Load configuration with open(args.settings, "r") as f: settings = yaml.load(f) issuer = args.base.rstrip("/") template_dirs = settings["server"].get("template_dirs", "templates") jinja_env = Environment(loader=FileSystemLoader(template_dirs)) authn_broker, auth_routing = setup_authentication_methods(settings["authn"], jinja_env) # Setup userinfo userinfo_conf = settings["userinfo"] cls = make_cls_from_name(userinfo_conf["class"]) i = cls(**userinfo_conf["kwargs"]) userinfo = UserInfo(i) client_db = {} session_db = create_session_db(issuer, secret=rndstr(32), password=rndstr(32)) provider = Provider(issuer, session_db, client_db, authn_broker, userinfo, AuthzHandling(), verify_client, None) provider.baseurl = issuer provider.symkey = rndstr(16) # Setup keys path = os.path.join(os.path.dirname(__file__), "static") try: os.makedirs(path) except OSError as e: if e.errno != errno.EEXIST: raise e pass jwks = keyjar_init(provider, settings["provider"]["keys"]) name = "jwks.json" with open(os.path.join(path, name), "w") as f: f.write(json.dumps(jwks)) #TODO: I take this out and it still works, what was this for? #provider.jwks_uri.append( # "{}/static/{}".format(provider.baseurl, name)) # Mount the WSGI callable object (app) on the root directory app_routing = setup_endpoints(provider) app_routing["/.well-known/openid-configuration"] = pyoidcMiddleware( provider.providerinfo_endpoint) app_routing["/.well-known/webfinger"] = pyoidcMiddleware( partial(_webfinger, provider)) routing = dict(list(auth_routing.items()) + list(app_routing.items())) routing["/static"] = make_static_handler(path) dispatcher = WSGIPathInfoDispatcher(routing) server = wsgiserver.CherryPyWSGIServer(('0.0.0.0', args.port), dispatcher) # Setup SSL if provider.baseurl.startswith("https://"): server.ssl_adapter = BuiltinSSLAdapter( settings["server"]["cert"], settings["server"]["key"], settings["server"]["cert_chain"]) # Start the CherryPy WSGI web server try: print("Server started: {}".format(issuer)) server.start() except KeyboardInterrupt: server.stop()
class TestProvider(object): @pytest.fixture(autouse=True) def create_provider(self, session_db_factory): self.provider = Provider(SERVER_INFO["issuer"], session_db_factory(SERVER_INFO["issuer"]), CDB, AUTHN_BROKER, USERINFO, AUTHZ, verify_client, SYMKEY, urlmap=URLMAP, keyjar=KEYJAR) self.provider.baseurl = self.provider.name self.cons = Consumer( {}, CONSUMER_CONFIG, CLIENT_CONFIG, server_info=SERVER_INFO, ) self.cons.behaviour = { "request_object_signing_alg": DEF_SIGN_ALG["openid_request_object"] } self.cons.keyjar[""] = KC_RSA def test_providerinfo(self): self.provider.baseurl = 'http://example.com/path1/path2' resp = self.provider.create_providerinfo() assert resp.to_dict( )['authorization_endpoint'] == 'http://example.com/path1/path2/authorization' def test_providerinfo_trailing(self): self.provider.baseurl = 'http://example.com/path1/path2/' resp = self.provider.create_providerinfo() assert resp.to_dict( )['authorization_endpoint'] == 'http://example.com/path1/path2/authorization' def test_authorization_endpoint(self): bib = { "scope": ["openid"], "state": "id-6da9ca0cc23959f5f33e8becd9b08cae", "redirect_uri": "http://*****:*****@patch('oic.oic.provider.utc_time_sans_frac', Mock(return_value=123456)) def test_client_secret_expiration_time(self): exp_time = self.provider.client_secret_expiration_time() assert exp_time == 209856 def test_registration_endpoint(self): req = RegistrationRequest() req["application_type"] = "web" req["client_name"] = "My super service" req["redirect_uris"] = ["http://example.com/authz"] req["contacts"] = ["*****@*****.**"] req["response_types"] = ["code"] resp = self.provider.registration_endpoint(request=req.to_json()) regresp = RegistrationResponse().deserialize(resp.message, "json") assert _eq(regresp.keys(), [ 'redirect_uris', 'contacts', 'application_type', 'client_name', 'registration_client_uri', 'client_secret_expires_at', 'registration_access_token', 'client_id', 'client_secret', 'client_id_issued_at', 'response_types' ]) def test_registration_endpoint_unicode(self): data = 'application_type=web&client_name=M%C3%A1+supe%C5%99+service&' \ 'redirect_uris=http%3A%2F%2Fexample.com%2Fauthz&response_types=code' resp = self.provider.registration_endpoint(request=data) regresp = RegistrationResponse().deserialize(resp.message, "json") assert _eq(regresp.keys(), [ 'redirect_uris', 'application_type', 'client_name', 'registration_client_uri', 'client_secret_expires_at', 'registration_access_token', 'client_id', 'client_secret', 'client_id_issued_at', 'response_types' ]) def test_registration_endpoint_with_non_https_redirect_uri_implicit_flow( self): params = { "application_type": "web", "redirect_uris": ["http://example.com/authz"], "response_types": ["id_token", "token"] } req = RegistrationRequest(**params) resp = self.provider.registration_endpoint(request=req.to_json()) assert resp.status == "400 Bad Request" error = json.loads(resp.message) assert error["error"] == "invalid_redirect_uri" def test_verify_redirect_uris_with_https_code_flow(self): params = { "application_type": "web", "redirect_uris": ["http://example.com/authz"], "response_types": ["code"] } request = RegistrationRequest(**params) verified_uris = self.provider.verify_redirect_uris(request) assert verified_uris == [("http://example.com/authz", None)] def test_verify_redirect_uris_with_non_https_redirect_uri_implicit_flow( self): params = { "application_type": "web", "redirect_uris": ["http://example.com/authz"], "response_types": ["id_token", "token"] } request = RegistrationRequest(**params) with pytest.raises(InvalidRedirectURIError) as exc_info: self.provider.verify_redirect_uris(request) assert str(exc_info.value) == "None https redirect_uri not allowed" def test_provider_key_setup(self, tmpdir, session_db_factory): path = tmpdir.strpath provider = Provider("pyoicserv", session_db_factory(SERVER_INFO["issuer"]), None, None, None, None, None, None) provider.baseurl = "http://www.example.com" provider.key_setup(path, path, sig={"format": "jwk", "alg": "RSA"}) keys = provider.keyjar.get_signing_key("RSA") assert len(keys) == 1 assert provider.jwks_uri == "http://www.example.com/{}/jwks".format( path) @pytest.mark.parametrize("uri", [ "http://example.org/foo", "http://example.com/cb", "http://example.org/cb?got=you", "http://example.org/cb/foo?got=you" ]) def test_verify_redirect_uri_faulty_without_query(self, uri): rr = RegistrationRequest(operation="register", redirect_uris=["http://example.org/cb"], response_types=["code"]) registration_req = rr.to_json() resp = self.provider.registration_endpoint(request=registration_req) regresp = RegistrationResponse().from_json(resp.message) cid = regresp["client_id"] areq = AuthorizationRequest(redirect_uri=uri, client_id=cid, response_type="code", scope="openid") with pytest.raises(RedirectURIError): self.provider._verify_redirect_uri(areq) @pytest.mark.parametrize("uri", [ "http://example.org/cb", ]) def test_verify_redirect_uri_correct_without_query(self, uri): rr = RegistrationRequest(operation="register", redirect_uris=["http://example.org/cb"], response_types=["code"]) registration_req = rr.to_json() resp = self.provider.registration_endpoint(request=registration_req) regresp = RegistrationResponse().from_json(resp.message) cid = regresp["client_id"] areq = AuthorizationRequest(redirect_uri=uri, client_id=cid, response_type="code", scope="openid") self.provider._verify_redirect_uri(areq) def test_verify_sector_identifier_nonreachable(self): rr = RegistrationRequest(operation="register", sector_identifier_uri="https://example.com") with responses.RequestsMock() as rsps, LogCapture( level=logging.DEBUG) as logcap: rsps.add(rsps.GET, "https://example.com", status=404) message = "Couldn't open sector_identifier_uri" with pytest.raises(InvalidSectorIdentifier, message=message): self.provider._verify_sector_identifier(rr) assert len(logcap.records) == 0 def test_verify_sector_identifier_error(self): rr = RegistrationRequest(operation="register", sector_identifier_uri="https://example.com") error = ConnectionError('broken connection') with responses.RequestsMock() as rsps, LogCapture( level=logging.DEBUG) as logcap: rsps.add(rsps.GET, "https://example.com", body=error) with pytest.raises(InvalidSectorIdentifier, message="Couldn't open sector_identifier_uri"): self.provider._verify_sector_identifier(rr) assert len(logcap.records) == 2 # First log record is from server... assert logcap.records[1].msg == error def test_verify_sector_identifier_malformed(self): rr = RegistrationRequest(operation="register", sector_identifier_uri="https://example.com") body = "This is not the JSON you are looking for" with responses.RequestsMock() as rsps, LogCapture( level=logging.DEBUG) as logcap: rsps.add(rsps.GET, "https://example.com", body=body) with pytest.raises( InvalidSectorIdentifier, message="Error deserializing sector_identifier_uri content" ): self.provider._verify_sector_identifier(rr) assert len(logcap.records) == 1 assert logcap.records[0].msg == "sector_identifier_uri => %s" assert logcap.records[0].args == (body, ) def test_verify_sector_identifier_ru_missing_in_si(self): """Redirect_uris is not present in the sector_identifier_uri content.""" rr = RegistrationRequest(operation="register", sector_identifier_uri="https://example.com", redirect_uris=["http://example.com/missing"]) with responses.RequestsMock() as rsps, LogCapture( level=logging.DEBUG) as logcap: rsps.add(rsps.GET, "https://example.com", body=json.dumps(["http://example.com/present"])) with pytest.raises( InvalidSectorIdentifier, message="redirect uri missing from sector_identifiers"): self.provider._verify_sector_identifier(rr) assert len(logcap.records) == 2 assert logcap.records[0].msg == "sector_identifier_uri => %s" assert logcap.records[0].args == ('["http://example.com/present"]', ) assert logcap.records[1].msg == "redirect_uris: %s" assert logcap.records[1].args == (["http://example.com/missing"], ) def test_verify_sector_identifier_ru_missing(self): """Redirect_uris is not present in the request.""" rr = RegistrationRequest(operation="register", sector_identifier_uri="https://example.com") redirects = ["http://example.com/present"] with responses.RequestsMock() as rsps, LogCapture( level=logging.DEBUG) as logcap: rsps.add(rsps.GET, "https://example.com", body=json.dumps(redirects)) si_redirects, si_url = self.provider._verify_sector_identifier(rr) assert si_url == "https://example.com" assert si_redirects == redirects assert len(logcap.records) == 1 assert logcap.records[0].msg == "sector_identifier_uri => %s" assert logcap.records[0].args == ('["http://example.com/present"]', ) def test_verify_sector_identifier_ru_ok(self): """Redirect_uris is present in the sector_identifier_uri content.""" rr = RegistrationRequest(operation="register", sector_identifier_uri="https://example.com", redirect_uris=["http://example.com/present"]) redirects = ["http://example.com/present"] with responses.RequestsMock() as rsps, LogCapture( level=logging.DEBUG) as logcap: rsps.add(rsps.GET, "https://example.com", body=json.dumps(redirects)) si_redirects, si_url = self.provider._verify_sector_identifier(rr) assert si_url == "https://example.com" assert si_redirects == redirects assert len(logcap.records) == 2 assert logcap.records[0].msg == "sector_identifier_uri => %s" assert logcap.records[0].args == ('["http://example.com/present"]', ) assert logcap.records[1].msg == "redirect_uris: %s" assert logcap.records[1].args == (["http://example.com/present"], ) @pytest.mark.parametrize("uri", [ "http://example.org/cb", "http://example.org/cb?got=you", "http://example.org/cb?foo=you" "http://example.org/cb?foo=bar&got=you", "http://example.org/cb?foo=you&foo=bar" ]) def test_registered_redirect_uri_faulty_with_query_component(self, uri): rr = RegistrationRequest( operation="register", redirect_uris=["http://example.org/cb?foo=bar"], response_types=["code"]) registration_req = rr.to_json() resp = self.provider.registration_endpoint(request=registration_req) regresp = RegistrationResponse().from_json(resp.message) cid = regresp["client_id"] areq = AuthorizationRequest(redirect_uri=uri, client_id=cid, scope="openid", response_type="code") with pytest.raises(RedirectURIError): self.provider._verify_redirect_uri(areq) def test_registered_redirect_uri_correct_with_query_component(self): rr = RegistrationRequest( operation="register", redirect_uris=["http://example.org/cb?foo=bar"], response_types=["code"]) registration_req = rr.to_json() resp = self.provider.registration_endpoint(request=registration_req) regresp = RegistrationResponse().from_json(resp.message) cid = regresp["client_id"] areq = AuthorizationRequest( redirect_uri="http://example.org/cb?foo=bar", client_id=cid, scope="openid", response_type="code") self.provider._verify_redirect_uri(areq) def test_verify_redirect_uri_native_http_localhost(self): areq = RegistrationRequest(redirect_uris=["http://localhost/cb"], application_type='native') self.provider.verify_redirect_uris(areq) def test_verify_redirect_uri_native_loopback(self): areq = RegistrationRequest(redirect_uris=["http://127.0.0.1/cb"], application_type='native') self.provider.verify_redirect_uris(areq) def test_verify_redirect_uri_native_http_non_localhost(self): areq = RegistrationRequest(redirect_uris=["http://example.org/cb"], application_type='native') try: self.provider.verify_redirect_uris(areq) except InvalidRedirectURIError: assert True def test_verify_redirect_uri_native_custom(self): areq = RegistrationRequest( redirect_uris=["com.example.app:/oauth2redirect"], application_type='native') self.provider.verify_redirect_uris(areq) def test_verify_redirect_uri_native_https(self): areq = RegistrationRequest(redirect_uris=["https://example.org/cb"], application_type='native') try: self.provider.verify_redirect_uris(areq) except InvalidRedirectURIError: assert True def test_read_registration(self): rr = RegistrationRequest(operation="register", redirect_uris=["http://example.org/new"], response_types=["code"]) registration_req = rr.to_json() resp = self.provider.registration_endpoint(request=registration_req) regresp = RegistrationResponse().from_json(resp.message) authn = ' '.join(['Bearer', regresp['registration_access_token']]) query = '='.join(['client_id', regresp['client_id']]) resp = self.provider.read_registration(authn, query) assert json.loads(resp.message) == regresp.to_dict() def test_read_registration_wrong_authn(self): resp = self.provider.read_registration('wrong string', 'request') assert resp.status == '400 Bad Request' assert json.loads(resp.message) == { 'error': 'invalid_request', 'error_description': None } def test_key_rollover(self): provider2 = Provider("FOOP", {}, {}, None, None, None, None, None) provider2.keyjar = KEYJAR # Number of KeyBundles assert len(provider2.keyjar.issuer_keys[""]) == 1 kb = ec_init({"type": "EC", "crv": "P-256", "use": ["sig"]}) provider2.do_key_rollover(json.loads(kb.jwks()), "b%d") assert len(provider2.keyjar.issuer_keys[""]) == 2 kb = ec_init({"type": "EC", "crv": "P-256", "use": ["sig"]}) provider2.do_key_rollover(json.loads(kb.jwks()), "b%d") assert len(provider2.keyjar.issuer_keys[""]) == 3 provider2.remove_inactive_keys(-1) assert len(provider2.keyjar.issuer_keys[""]) == 2 def test_endsession_endpoint(self): resp = self.provider.endsession_endpoint("") self._assert_cookies_expired(resp.headers) # End session not allowed if no cookie is sent (can't determine session) resp = self.provider.endsession_endpoint("", cookie="FAIL") assert resp.status == "400 Bad Request" def test_endsession_endpoint_with_id_token_hint(self): id_token = self._auth_with_id_token() assert self.provider.sdb.get_sids_by_sub( id_token["sub"]) # verify we got valid session id_token_hint = id_token.to_jwt(algorithm="none") resp = self.provider.endsession_endpoint( urlencode({"id_token_hint": id_token_hint})) assert not self.provider.sdb.get_sids_by_sub( id_token["sub"]) # verify session has been removed self._assert_cookies_expired(resp.headers) def test_endsession_endpoint_with_post_logout_redirect_uri(self): id_token = self._auth_with_id_token() assert self.provider.sdb.get_sids_by_sub( id_token["sub"]) # verify we got valid session post_logout_redirect_uri = \ CDB[CLIENT_CONFIG["client_id"]]["post_logout_redirect_uris"][0][0] resp = self.provider.endsession_endpoint( urlencode({"post_logout_redirect_uri": post_logout_redirect_uri})) assert isinstance(resp, SeeOther) assert not self.provider.sdb.get_sids_by_sub( id_token["sub"]) # verify session has been removed self._assert_cookies_expired(resp.headers) def test_session_state_in_auth_req_for_session_support( self, session_db_factory): provider = Provider(SERVER_INFO["issuer"], session_db_factory(SERVER_INFO["issuer"]), CDB, AUTHN_BROKER, USERINFO, AUTHZ, verify_client, SYMKEY, urlmap=URLMAP, keyjar=KEYJAR) provider.capabilities.update( {"check_session_iframe": "https://op.example.com/check_session"}) req_args = { "scope": ["openid"], "redirect_uri": "http://localhost:8087/authz", "response_type": ["code"], "client_id": "number5" } areq = AuthorizationRequest(**req_args) resp = provider.authorization_endpoint(request=areq.to_urlencoded()) aresp = self.cons.parse_response(AuthorizationResponse, resp.message, sformat="urlencoded") assert "session_state" in aresp def _assert_cookies_expired(self, http_headers): cookies_string = ";".join( [c[1] for c in http_headers if c[0] == "Set-Cookie"]) all_cookies = SimpleCookie() try: cookies_string = cookies_string.decode() except (AttributeError, UnicodeDecodeError): pass all_cookies.load(cookies_string) now = datetime.datetime.utcnow() # for c in [ self.provider.cookie_name, self.provider.session_cookie_name ]: dt = datetime.datetime.strptime(all_cookies[c]["expires"], "%a, %d-%b-%Y %H:%M:%S GMT") assert dt < now # make sure the cookies have expired to be cleared def _auth_with_id_token(self): state, location = self.cons.begin("openid", "id_token", path="http://localhost:8087") resp = self.provider.authorization_endpoint( request=location.split("?")[1]) aresp = self.cons.parse_response(AuthorizationResponse, resp.message, sformat="urlencoded") return aresp["id_token"] def test_id_token_RS512_sign(self): self.provider.capabilities['id_token_signing_alg_values_supported'] = [ 'RS512' ] self.provider.build_jwx_def() id_token = self._auth_with_id_token() assert id_token.jws_header['alg'] == "RS512" def test_refresh_access_token_request(self): authreq = AuthorizationRequest(state="state", redirect_uri="http://example.com/authz", client_id=CLIENT_ID, response_type="code", scope=["openid", 'offline_access'], prompt='consent') _sdb = self.provider.sdb sid = _sdb.access_token.key(user="******", areq=authreq) access_grant = _sdb.access_token(sid=sid) ae = AuthnEvent("user", "salt") _sdb[sid] = { "oauth_state": "authz", "authn_event": ae.to_json(), "authzreq": authreq.to_json(), "client_id": CLIENT_ID, "code": access_grant, "code_used": False, "scope": ["openid", 'offline_access'], "redirect_uri": "http://example.com/authz", } _sdb.do_sub(sid, "client_salt") # Construct Access token request areq = AccessTokenRequest(code=access_grant, client_id=CLIENT_ID, redirect_uri="http://example.com/authz", client_secret=CLIENT_SECRET, grant_type='authorization_code') txt = areq.to_urlencoded() resp = self.provider.token_endpoint(request=txt) atr = AccessTokenResponse().deserialize(resp.message, "json") rareq = RefreshAccessTokenRequest(grant_type="refresh_token", refresh_token=atr['refresh_token'], client_id=CLIENT_ID, client_secret=CLIENT_SECRET, scope=['openid']) resp = self.provider.token_endpoint(request=rareq.to_urlencoded()) atr2 = AccessTokenResponse().deserialize(resp.message, "json") assert atr2['access_token'] != atr['access_token'] assert atr2['refresh_token'] == atr['refresh_token'] assert atr2['token_type'] == 'Bearer'
def __init__(self, name, sdb, cdb, function, userdb, urlmap=None, debug=0, ca_certs="", jwt_keys=None): Provider.__init__(self, name, sdb, cdb, function, userdb, urlmap, ca_certs, jwt_keys)
class TestProvider(object): @pytest.fixture(autouse=True) def create_provider(self): self.provider = Provider("pyoicserv", SessionDB(SERVER_INFO["issuer"]), CDB, AUTHN_BROKER, USERINFO, AUTHZ, verify_client, SYMKEY, urlmap=URLMAP, keyjar=KEYJAR) self.provider.baseurl = self.provider.name self.cons = Consumer({}, CONSUMER_CONFIG, CLIENT_CONFIG, server_info=SERVER_INFO, ) self.cons.behaviour = { "request_object_signing_alg": DEF_SIGN_ALG["openid_request_object"]} self.cons.keyjar[""] = KC_RSA def test_authorization_endpoint(self): bib = {"scope": ["openid"], "state": "id-6da9ca0cc23959f5f33e8becd9b08cae", "redirect_uri": "http://*****:*****@example.com"] req["response_types"] = ["code"] resp = self.provider.registration_endpoint(request=req.to_json()) regresp = RegistrationResponse().deserialize(resp.message, "json") assert _eq(regresp.keys(), ['redirect_uris', 'contacts', 'application_type', 'client_name', 'registration_client_uri', 'client_secret_expires_at', 'registration_access_token', 'client_id', 'client_secret', 'client_id_issued_at', 'response_types']) def test_registration_endpoint_with_non_https_redirect_uri_implicit_flow( self): params = {"application_type": "web", "redirect_uris": ["http://example.com/authz"], "response_types": ["id_token", "token"]} req = RegistrationRequest(**params) resp = self.provider.registration_endpoint(request=req.to_json()) assert resp.status == "400 Bad Request" error = json.loads(resp.message) assert error["error"] == "invalid_redirect_uri" def test_verify_redirect_uris_with_https_code_flow(self): params = {"application_type": "web", "redirect_uris": ["http://example.com/authz"], "response_types": ["code"]} request = RegistrationRequest(**params) verified_uris = self.provider._verify_redirect_uris(request) assert verified_uris == [("http://example.com/authz", None)] def test_verify_redirect_uris_with_non_https_redirect_uri_implicit_flow(self): params = {"application_type": "web", "redirect_uris": ["http://example.com/authz"], "response_types": ["id_token", "token"]} request = RegistrationRequest(**params) with pytest.raises(InvalidRedirectURIError) as exc_info: self.provider._verify_redirect_uris(request) assert str(exc_info.value) == "None https redirect_uri not allowed" @pytest.mark.network def test_registration_endpoint_openid4us(self): req = RegistrationRequest( **{'token_endpoint_auth_method': u'client_secret_post', 'redirect_uris': [ u'https://connect.openid4.us:5443/phpRp/index.php/callback', u'https://connect.openid4.us:5443/phpRp/authcheck.php/authcheckcb'], 'jwks_uri': u'https://connect.openid4.us:5443/phpRp/rp/rp.jwk', 'userinfo_encrypted_response_alg': u'RSA1_5', 'contacts': [u'*****@*****.**'], 'userinfo_encrypted_response_enc': u'A128CBC-HS256', 'application_type': u'web', 'client_name': u'ABRP-17', 'grant_types': [u'authorization_code', u'implicit'], 'post_logout_redirect_uris': [ u'https://connect.openid4.us:5443/phpRp/index.php/logoutcb'], 'subject_type': u'public', 'response_types': [u'code', u'token', u'id_token', u'code token', u'code id_token', u'id_token token', u'code id_token token'], 'policy_uri': u'https://connect.openid4.us:5443/phpRp/index.php/policy', 'logo_uri': u'https://connect.openid4.us:5443/phpRp/media/logo.png'}) resp = self.provider.registration_endpoint(request=req.to_json()) regresp = RegistrationResponse().deserialize(resp.message, "json") assert _eq(regresp.keys(), list(req.keys()) + ['registration_client_uri', 'client_secret_expires_at', 'registration_access_token', 'client_id', 'client_secret', 'client_id_issued_at']) def test_provider_key_setup(self, tmpdir): path = tmpdir.strpath provider = Provider("pyoicserv", SessionDB(SERVER_INFO["issuer"]), None, None, None, None, None, "") provider.baseurl = "http://www.example.com" provider.key_setup(path, path, sig={"format": "jwk", "alg": "RSA"}) keys = provider.keyjar.get_signing_key("RSA") assert len(keys) == 1 assert provider.jwks_uri == "http://www.example.com/{}/jwks".format( path) @pytest.mark.parametrize("uri", [ "http://example.org/foo", "http://example.com/cb", "http://example.org/cb?got=you", "http://example.org/cb/foo?got=you" ]) def test_verify_redirect_uri_faulty_without_query(self, uri): rr = RegistrationRequest(operation="register", redirect_uris=["http://example.org/cb"], response_types=["code"]) registration_req = rr.to_json() resp = self.provider.registration_endpoint(request=registration_req) regresp = RegistrationResponse().from_json(resp.message) cid = regresp["client_id"] areq = AuthorizationRequest(redirect_uri=uri, client_id=cid, response_type="code", scope="openid") with pytest.raises(RedirectURIError): self.provider._verify_redirect_uri(areq) @pytest.mark.parametrize("uri", [ "http://example.org/cb", "http://example.org/cb/foo" ]) def test_verify_redirect_uri_correct_without_query(self, uri): rr = RegistrationRequest(operation="register", redirect_uris=["http://example.org/cb"], response_types=["code"]) registration_req = rr.to_json() resp = self.provider.registration_endpoint(request=registration_req) regresp = RegistrationResponse().from_json(resp.message) cid = regresp["client_id"] areq = AuthorizationRequest(redirect_uri=uri, client_id=cid, response_type="code", scope="openid") self.provider._verify_redirect_uri(areq) @pytest.mark.parametrize("uri", [ "http://example.org/cb", "http://example.org/cb/foo", "http://example.org/cb?got=you", "http://example.org/cb?foo=you" "http://example.org/cb?foo=bar&got=you", "http://example.org/cb?foo=you&foo=bar" ]) def test_registered_redirect_uri_faulty_with_query_component(self, uri): rr = RegistrationRequest(operation="register", redirect_uris=[ "http://example.org/cb?foo=bar"], response_types=["code"]) registration_req = rr.to_json() resp = self.provider.registration_endpoint(request=registration_req) regresp = RegistrationResponse().from_json(resp.message) cid = regresp["client_id"] areq = AuthorizationRequest(redirect_uri=uri, client_id=cid, scope="openid", response_type="code") with pytest.raises(RedirectURIError): self.provider._verify_redirect_uri(areq) def test_registered_redirect_uri_correct_with_query_component(self): rr = RegistrationRequest(operation="register", redirect_uris=[ "http://example.org/cb?foo=bar"], response_types=["code"]) registration_req = rr.to_json() resp = self.provider.registration_endpoint(request=registration_req) regresp = RegistrationResponse().from_json(resp.message) cid = regresp["client_id"] areq = AuthorizationRequest( redirect_uri="http://example.org/cb?foo=bar", client_id=cid, scope="openid", response_type="code") self.provider._verify_redirect_uri(areq) def test_key_rollover(self): provider2 = Provider("FOOP", {}, {}, None, None, None, None, "") provider2.keyjar = KEYJAR # Number of KeyBundles assert len(provider2.keyjar.issuer_keys[""]) == 1 kb = ec_init({"type": "EC", "crv": "P-256", "use": ["sig"]}) provider2.do_key_rollover(json.loads(kb.jwks()), "b%d") assert len(provider2.keyjar.issuer_keys[""]) == 2 kb = ec_init({"type": "EC", "crv": "P-256", "use": ["sig"]}) provider2.do_key_rollover(json.loads(kb.jwks()), "b%d") assert len(provider2.keyjar.issuer_keys[""]) == 3 provider2.remove_inactive_keys(-1) assert len(provider2.keyjar.issuer_keys[""]) == 2 def test_endsession_endpoint(self): resp = self.provider.endsession_endpoint("") self._assert_cookies_expired(resp.headers) # End session not allowed if no cookie is sent (can't determine session) resp = self.provider.endsession_endpoint("", cookie="FAIL") assert resp.status == "400 Bad Request" def test_endsession_endpoint_with_id_token_hint(self): id_token = self._auth_with_id_token() assert self.provider.sdb.get_sids_by_sub( id_token["sub"]) # verify we got valid session id_token_hint = id_token.to_jwt(algorithm="none") resp = self.provider.endsession_endpoint( urlencode({"id_token_hint": id_token_hint})) assert not self.provider.sdb.get_sids_by_sub( id_token["sub"]) # verify session has been removed self._assert_cookies_expired(resp.headers) def test_endsession_endpoint_with_post_logout_redirect_uri(self): id_token = self._auth_with_id_token() assert self.provider.sdb.get_sids_by_sub( id_token["sub"]) # verify we got valid session post_logout_redirect_uri = \ CDB[CLIENT_CONFIG["client_id"]]["post_logout_redirect_uris"][0][0] resp = self.provider.endsession_endpoint(urlencode( {"post_logout_redirect_uri": post_logout_redirect_uri})) assert isinstance(resp, Redirect) assert not self.provider.sdb.get_sids_by_sub( id_token["sub"]) # verify session has been removed self._assert_cookies_expired(resp.headers) def test_session_state_in_auth_req_for_session_support(self): provider = Provider("foo", SessionDB(SERVER_INFO["issuer"]), CDB, AUTHN_BROKER, USERINFO, AUTHZ, verify_client, SYMKEY, urlmap=URLMAP, keyjar=KEYJAR, capabilities={ "check_session_iframe": "https://op.example.com/check_session"}) req_args = {"scope": ["openid"], "redirect_uri": "http://localhost:8087/authz", "response_type": ["code"], "client_id": "a1b2c3" } areq = AuthorizationRequest(**req_args) resp = provider.authorization_endpoint( request=areq.to_urlencoded()) aresp = self.cons.parse_response(AuthorizationResponse, resp.message, sformat="urlencoded") assert "session_state" in aresp def _assert_cookies_expired(self, http_headers): cookies_string = ";".join( [c[1] for c in http_headers if c[0] == "Set-Cookie"]) all_cookies = SimpleCookie() all_cookies.load(cookies_string) now = datetime.datetime.now() for c in [self.provider.cookie_name, self.provider.session_cookie_name]: dt = datetime.datetime.strptime(all_cookies[c]["expires"], "%a, %d-%b-%Y %H:%M:%S GMT") assert dt < now # make sure the cookies have expired to be cleared def _auth_with_id_token(self): state, location = self.cons.begin("openid", "id_token", path="http://localhost:8087") resp = self.provider.authorization_endpoint( request=location.split("?")[1]) aresp = self.cons.parse_response(AuthorizationResponse, resp.message, sformat="urlencoded") return aresp["id_token"]
class TestOICProvider(object): def setup_class(self): self.server = Provider("pyoicserv", SessionDB(SERVER_INFO["issuer"]), CDB, AUTHN_BROKER, USERINFO, AUTHZ, verify_client, SYMKEY, urlmap=URLMAP, keyjar=KEYJAR) self.cons = Consumer({}, CONSUMER_CONFIG, CLIENT_CONFIG, server_info=SERVER_INFO, ) self.cons.behaviour = {"request_object_signing_alg": DEF_SIGN_ALG["openid_request_object"]} self.cons.debug = True self.cons.keyjar[""] = KC_RSA def test_server_init(self): assert self.server assert self.server.authn_broker == AUTHN_BROKER print self.server.urlmap assert self.server.urlmap["client_1"] == ["https://example.com/authz"] def test_server_authorization_endpoint(self): bib = {"scope": ["openid"], "state": "id-6da9ca0cc23959f5f33e8becd9b08cae", "redirect_uri": "http://*****:*****@example.com"] req["response_types"] = ["code"] print req.to_dict() resp = self.server.registration_endpoint(request=req.to_json()) print resp.message regresp = RegistrationResponse().deserialize(resp.message, "json") print regresp.keys() assert _eq(regresp.keys(), ['redirect_uris', 'contacts', 'application_type', 'client_name', 'registration_client_uri', 'client_secret_expires_at', 'registration_access_token', 'client_id', 'client_secret', 'client_id_issued_at', 'response_types']) def test_provider_key_setup(self): provider = Provider("pyoicserv", SessionDB(SERVER_INFO["issuer"]), None, None, None, None, None, "") provider.baseurl = "http://www.example.com/" provider.key_setup("static", sig={"format": "jwk", "alg": "RSA"}) keys = provider.keyjar.get_signing_key("RSA") assert len(keys) == 1 assert provider.jwks_uri == "http://www.example.com/static/jwks" def _client_id(self, cdb): cid = None for k, item in cdb.items(): if item in cdb.keys(): cid = item break return cid def test_registered_redirect_uri_without_query_component(self): provider = Provider("FOO", {}, {}, None, None, None, None, "") rr = RegistrationRequest(operation="register", redirect_uris=["http://example.org/cb"], response_types=["code"]) registration_req = rr.to_json() provider.registration_endpoint(request=registration_req) correct = [ "http://example.org/cb", "http://example.org/cb/foo", ] faulty = [ "http://example.org/foo", "http://example.com/cb", "http://example.org/cb?got=you", "http://example.org/cb/foo?got=you" ] cid = self._client_id(provider.cdb) for ruri in faulty: areq = AuthorizationRequest(redirect_uri=ruri, client_id=cid, response_type="code", scope="openid") print areq try: provider._verify_redirect_uri(areq) assert False except RedirectURIError: pass for ruri in correct: areq = AuthorizationRequest(redirect_uri=ruri, client_id=cid, response_type="code", scope="openid") print areq try: provider._verify_redirect_uri(areq) except RedirectURIError, err: print err assert False
# In-Memory non-persistent SessionDB issuing DefaultTokens sessionDB = create_session_db(config.ISSUER, secret=rndstr(32), password=rndstr(32)) provider = Provider( name=config.ISSUER, # name sdb=sessionDB, # session database. cdb=clientDB, # client database authn_broker=authnBroker, # authn broker userinfo=None, # user information authz=authz, # authz client_authn=verify_client, # client authentication symkey=config.SYM_KEY, # Used for Symmetric key authentication # urlmap = None, # ? # keyjar = None, # ? # hostname = "", # ? template_renderer=mako_renderer, # Rendering custom templates # verify_ssl = True, # Enable SSL certs # capabilities = None, # ? # schema = OpenIDSchema, # ? # jwks_uri = '', # ? # jwks_name = '', # ? baseurl=config.ISSUER, # client_cert = None # ? ) # SessionDB: # This is database where the provider keeps information about # the authenticated/authorised users. It includes information # such as "what has been asked for (claims, scopes, and etc. )"
class OIDCFrontend(FrontendModule): """ A OpenID Connect frontend module """ MANDATORY_CONFIG = {"issuer", "signing_key_path"} def __init__(self, auth_req_callback_func, internal_attributes, conf): self._validate_config(conf) super(OIDCFrontend, self).__init__(auth_req_callback_func, internal_attributes) self.state_id = type(self).__name__ self.sign_alg = "RS256" self.subject_type_default = "pairwise" self.conf = conf def handle_authn_response(self, context, internal_resp): """ See super class method satosa.frontends.base.FrontendModule#handle_authn_response :type context: satosa.context.Context :type internal_response: satosa.internal_data.InternalResponse :rtype oic.utils.http_util.Response """ auth_req = self._get_authn_request_from_state(context.state) # filter attributes to return in ID Token as claims attributes = self.converter.from_internal( "openid", internal_resp.get_attributes()) satosa_logging( LOGGER, logging.DEBUG, "Attributes delivered by backend to OIDC frontend: {}".format( json.dumps(attributes)), context.state) flattened_attributes = {k: v[0] for k, v in attributes.items()} requested_id_token_claims = auth_req.get("claims", {}).get("id_token") user_claims = self._get_user_info(flattened_attributes, requested_id_token_claims, auth_req["scope"]) satosa_logging( LOGGER, logging.DEBUG, "Attributes filtered by requested claims/scope: {}".format( json.dumps(user_claims)), context.state) # construct epoch timestamp of reported authentication time auth_time = datetime.datetime.strptime( internal_resp.auth_info.timestamp, "%Y-%m-%dT%H:%M:%SZ") epoch_timestamp = (auth_time - datetime.datetime(1970, 1, 1)).total_seconds() base_claims = { "client_id": auth_req["client_id"], "sub": internal_resp.get_user_id(), "nonce": auth_req["nonce"] } id_token = self.provider.id_token_as_signed_jwt( base_claims, user_info=user_claims, auth_time=epoch_timestamp, loa="", alg=self.sign_alg) oidc_client_state = auth_req.get("state") kwargs = {} if oidc_client_state: # inlcude any optional 'state' sent by the client in the authn req kwargs["state"] = oidc_client_state auth_resp = AuthorizationResponse(id_token=id_token, **kwargs) http_response = auth_resp.request( auth_req["redirect_uri"], self._should_fragment_encode(auth_req)) return SeeOther(http_response) def handle_backend_error(self, exception): """ See super class satosa.frontends.base.FrontendModule :type exception: satosa.exception.SATOSAError :rtype: oic.utils.http_util.Response """ auth_req = self._get_authn_request_from_state(exception.state) error_resp = AuthorizationErrorResponse( error="access_denied", error_description=exception.message) satosa_logging(LOGGER, logging.DEBUG, exception.message, exception.state) return SeeOther( error_resp.request(auth_req["redirect_uri"], self._should_fragment_encode(auth_req))) def register_endpoints(self, providers): """ See super class satosa.frontends.base.FrontendModule :type providers: list[str] :rtype: list[(str, ((satosa.context.Context, Any) -> satosa.response.Response, Any))] :raise ValueError: if more than one backend is configured """ if len(providers) != 1: raise ValueError( "OpenID Connect frontend only supports one backend.") backend = providers[0] endpoint_baseurl = "{}/{}".format(self.conf["issuer"], backend) jwks_uri = "{}/jwks".format(self.conf["issuer"]) self._create_op(self.conf["issuer"], endpoint_baseurl, jwks_uri) provider_config = ("^.well-known/openid-configuration$", self._provider_config) jwks_uri = ("^jwks$", self._jwks) dynamic_client_registration = ("^{}/{}".format( backend, RegistrationEndpoint.url), self._register_client) authentication = ("^{}/{}".format(backend, AuthorizationEndpoint.url), self.handle_authn_request) url_map = [ provider_config, jwks_uri, dynamic_client_registration, authentication ] return url_map def _create_op(self, issuer, endpoint_baseurl, jwks_uri): """ Create the necessary Provider instance. :type issuer: str :type endpoint_baseurl: str :type jwks_uri: str :param issuer: issuer URL for the OP :param endpoint_baseurl: baseurl to build endpoint URL from :param jwks_uri: URL to where the JWKS will be published """ kj = KeyJar() signing_key = KeyBundle(source="file://{}".format( self.conf["signing_key_path"]), fileformat="der", keyusage=["sig"]) kj.add_kb("", signing_key) capabilities = { "response_types_supported": ["id_token"], "id_token_signing_alg_values_supported": [self.sign_alg], "response_modes_supported": ["fragment", "query"], "subject_types_supported": ["public", "pairwise"], "grant_types_supported": ["implicit"], "claim_types_supported": ["normal"], "claims_parameter_supported": True, "request_parameter_supported": False, "request_uri_parameter_supported": False, } if "client_db_path" in self.conf: cdb = shelve_wrapper.open(self.conf["client_db_path"]) else: cdb = {} # client db in memory only self.provider = Provider(issuer, None, cdb, None, None, None, None, None, keyjar=kj, capabilities=capabilities, jwks_uri=jwks_uri) self.provider.baseurl = endpoint_baseurl self.provider.endp = [RegistrationEndpoint, AuthorizationEndpoint] def _get_user_info(self, user_attributes, requested_claims=None, scopes=None): """ Filter user attributes to return to the client (as claims in the ID Token) based on what was requested in request 'claims' parameter and in the 'scope'. :type user_attributes: dict[str, str] :type requested_claims: dict[str, Optional[dict]] :type scopes: list[str] :rtype: dict[str, str] :param user_attributes: attributes provided by the backend :param requested_claims: claims requested by the client through the 'claims' request param :param scopes: the scopes requested by the client :return: all attributes/claims to return to the client """ requested_claims = requested_claims or {} scopes = scopes or [] claims_requested_by_scope = Provider._scope2claims(scopes) claims_requested_by_scope.update( requested_claims) # let explicit claims request override scope return UserInfo().filter(user_attributes, claims_requested_by_scope) def _validate_config(self, config): """ Validates that all necessary config parameters are specified. :type config: dict[str, dict[str, Any] | str] :param config: the module config """ if config is None: raise ValueError("OIDCFrontend conf can't be 'None'.") for k in self.MANDATORY_CONFIG: if k not in config: raise ValueError( "Missing configuration parameter '{}' for OpenID Connect frontend." .format(k)) def _should_fragment_encode(self, authn_req): """ Determine, based on the clients request, whether the authentication/error response should be fragment encoded or not. :type authn_req: oic.oic.message.AuthorizationRequest :rtype: bool :param authn_req: parsed authentication request from the client :return: True if the response should be fragment encoded """ return authn_req.get("response_mode", "fragment") == "fragment" def _get_authn_request_from_state(self, state): """ Extract the clietns request stoed in the SATOSA state. :type state: satosa.state.State :rtype: oic.oic.message.AuthorizationRequest :param state: the current state :return: the parsed authentication request """ stored_state = state.get(self.state_id) oidc_request = stored_state["oidc_request"] return AuthorizationRequest().deserialize(oidc_request) def _register_client(self, context): """ Handle the OIDC dynamic client registration. :type context: satosa.context.Context :rtype: oic.utils.http_util.Response :param context: the current context :return: HTTP response to the client """ http_resp = self.provider.registration_endpoint( json.dumps(context.request)) if not isinstance(http_resp, Created): return http_resp return self._fixup_registration_response(http_resp) def _fixup_registration_response(self, http_resp): # remove client_secret since no token endpoint is published response = RegistrationResponse().deserialize(http_resp.message, "json") del response["client_secret"] # specify supported id token signing alg response["id_token_signed_response_alg"] = self.sign_alg http_resp.message = response.to_json() return http_resp def _provider_config(self, context): """ Construct the provider configuration information (served at /.well-known/openid-configuration). :type context: satosa.context.Context :rtype: oic.utils.http_util.Response :param context: the current context :return: HTTP response to the client """ http_resp = self.provider.providerinfo_endpoint() if not isinstance(http_resp, Response): return http_resp provider_config = ProviderConfigurationResponse().deserialize( http_resp.message, "json") del provider_config["token_endpoint_auth_methods_supported"] del provider_config["require_request_uri_registration"] http_resp.message = provider_config.to_json() return http_resp 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 _jwks(self, context): """ Construct the JWKS document (served at /jwks). :type context: satosa.context.Context :rtype: oic.utils.http_util.Response :param context: the current context :return: HTTP response to the client """ return Response(json.dumps(self.provider.keyjar.export_jwks()), content="application/json")
kwargs = { "template_lookup": LOOKUP, "template": { "form_post": "form_response.mako" }, #"template_args": {"form_post": {"action": "form_post"}} } # Should I care about verifying the certificates used other entities if args.insecure: kwargs["verify_ssl"] = False else: kwargs["verify_ssl"] = True OAS = Provider(config.issuer, SessionDB(), cdb, ac, None, authz, verify_client, config.SYM_KEY, **kwargs) for authn in ac: authn.srv = OAS if config.USERINFO == "SIMPLE": # User info is a simple dictionary in this case statically defined in # the configuration file OAS.userinfo = UserInfo(config.USERDB) elif config.USERINFO == "SAML": OAS.userinfo = UserInfo(config.SAML) else: raise Exception("Unsupported userinfo source") try: OAS.cookie_ttl = config.COOKIETTL
class TestProvider(object): CDB = { "number5": { "password": "******", "client_secret": "drickyoughurt", "redirect_uris": [("http://localhost:8087/authz", None)], "post_logout_redirect_uris": [("https://example.com/post_logout", None)], "client_salt": "salted", "response_types": [ "code", "token", "code id_token", "none", "code token", "id_token", ], }, "a1b2c3": { "redirect_uris": [("http://localhost:8088/authz", None)], "client_salt": "salted", "client_secret": "very_secret", "response_types": ["code", "token", "code id_token"], }, "client0": { "redirect_uris": [("http://www.example.org/authz", None)], "client_secret": "very_secret", "post_logout_redirect_uris": [("https://www.example.org/post_logout", None)], "client_salt": "salted", "response_types": ["code", "token", "code id_token"], }, CLIENT_ID: { "client_secret": CLIENT_SECRET, "redirect_uris": [("http://localhost:8087/authz", None)], "client_salt": "salted", "token_endpoint_auth_method": "client_secret_post", "response_types": ["code", "token", "code id_token"], }, } # type: Dict[str, Dict[str, Any]] @pytest.fixture(autouse=True) def create_provider(self, session_db_factory): self.provider = Provider( SERVER_INFO["issuer"], session_db_factory(SERVER_INFO["issuer"]), self.CDB, AUTHN_BROKER, USERINFO, AUTHZ, verify_client, SYMKEY, urlmap=URLMAP, keyjar=KEYJAR, ) self.provider.baseurl = self.provider.name self.provider.logout_verify_url = "https://127.0.0.1/logout_verify.html" self.cons = Consumer( DictSessionBackend(), CONSUMER_CONFIG.copy(), CLIENT_CONFIG, server_info=SERVER_INFO, ) self.cons.behaviour = { "request_object_signing_alg": DEF_SIGN_ALG["openid_request_object"] } self.cons.keyjar[""] = KC_RSA self.cons.keyjar.import_jwks(self.provider.keyjar.export_jwks(), self.cons.issuer) self.cons2 = Consumer({}, CONSUMER_CONFIG.copy(), CLIENT_CONFIG_2, server_info=SERVER_INFO) self.cons2.behaviour = { "request_object_signing_alg": DEF_SIGN_ALG["openid_request_object"] } self.cons2.keyjar[""] = KC_RSA def _code_auth(self): state, location = self.cons.begin("openid", "code", path="http://localhost:8087") return self.provider.authorization_endpoint( request=location.split("?")[1]) def _code_auth2(self): state, location = self.cons2.begin("openid", "code", path="http://www.example.org") return self.provider.authorization_endpoint( request=location.split("?")[1]) def _auth_with_id_token(self): state, location = self.cons.begin("openid", "id_token", path="http://localhost:8087") resp = self.provider.authorization_endpoint( request=location.split("?")[1]) aresp = self.cons.parse_response(AuthorizationResponse, resp.message, sformat="urlencoded") return aresp["id_token"] def _create_cookie(self, user, client_id, c_type="sso"): cd = CookieDealer(self.provider) set_cookie = cd.create_cookie("{}][{}".format(user, client_id), c_type, self.provider.sso_cookie_name) cookies_string = set_cookie[1] all_cookies = SimpleCookie() # type: SimpleCookie try: cookies_string = cookies_string.decode() except (AttributeError, UnicodeDecodeError): pass all_cookies.load(cookies_string) return all_cookies def test_missing_post_logout_redirect_uri(self): esr = EndSessionRequest(state="foo") assert self.provider.verify_post_logout_redirect_uri(esr, CLIENT_ID) is None def test_wrong_post_logout_redirect_uri(self): self.provider.cdb[CLIENT_ID]["post_logout_redirect_uris"] = [ "https://example.com/plru" ] esr = EndSessionRequest( state="foo", post_logout_redirect_uri="https://localhost:8087/plru") assert self.provider.verify_post_logout_redirect_uri(esr, CLIENT_ID) is None def test_no_post_logout_redirect_uri(self): self.provider.cdb[CLIENT_ID]["post_logout_redirect_uris"] = [ "https://example.com/plru", "https://example.com/plru2", ] esr = EndSessionRequest(state="foo") assert self.provider.verify_post_logout_redirect_uri(esr, CLIENT_ID) is None def test_let_user_verify_logout(self): self.provider.cdb[CLIENT_ID]["post_logout_redirect_uris"] = [ "https://localhost:8087/plru" ] esr = EndSessionRequest( state="foo", post_logout_redirect_uri="https://localhost:8087/plru") res = self.provider.let_user_verify_logout("user", esr, None, None) assert isinstance(res, Response) assert res.headers == [("Content-type", "text/html")] assert res.status_code == 200 def test_let_user_verify_logout_with_cookie(self): self.provider.cdb[CLIENT_ID]["post_logout_redirect_uris"] = [ "https://localhost:8087/plru" ] esr = EndSessionRequest( state="foo", post_logout_redirect_uri="https://localhost:8087/plru") res = self.provider.let_user_verify_logout("user", esr, [("Set-Cookie", "kaka")], None) assert isinstance(res, Response) assert set(res.headers) == { ("Content-type", "text/html"), ("Set-Cookie", "kaka"), } assert res.status_code == 200 def test_let_user_verify_logout_with_redirect(self): self.provider.cdb[CLIENT_ID]["post_logout_redirect_uris"] = [ "https://localhost:8087/plru" ] esr = EndSessionRequest( state="foo", post_logout_redirect_uri="https://localhost:8087/plru") res = self.provider.let_user_verify_logout( "user", esr, None, "https://example.com/redirect") assert isinstance(res, Response) assert set(res.headers) == {("Content-type", "text/html")} assert res.status_code == 200 # make sure the redirect was propagated txt = '<input type="hidden" name="{}" value="{}"/>'.format( "post_logout_redirect_uri", "https://localhost:8087/plru") assert txt in res.message def test_let_user_verify_logout_with_id_token_hint(self): self.provider.cdb[CLIENT_ID]["post_logout_redirect_uris"] = [ "https://localhost:8087/plru" ] esr = EndSessionRequest( state="foo", post_logout_redirect_uri="https://localhost:8087/plru", id_token_hint="J.W.S", ) res = self.provider.let_user_verify_logout("user", esr, None, None) assert isinstance(res, Response) assert set(res.headers) == {("Content-type", "text/html")} assert res.status_code == 200 # make sure the id_token_hint was propagated txt = '<input type="hidden" name="{}" value="{}"/>'.format( "id_token_hint", "J.W.S") assert txt in res.message def test_end_session_endpoint_with_cookie(self): self.provider.events = DummyEventStore() self._code_auth() cookie = self._create_cookie("username", "number5") resp = self.provider.end_session_endpoint(urlencode({"state": "abcde"}), cookie=cookie) # returns a SeeOther instance p = urlparse(resp.message) qs = parse_qs(p.query) jwt_info = self.provider.unpack_signed_jwt(qs["sjwt"][0]) assert jwt_info["state"] == "abcde" assert jwt_info["uid"] == "username" assert jwt_info["client_id"] == "number5" assert jwt_info["redirect_uri"] == "https://example.com/post_logout" def test_end_session_endpoint_with_wrong_cookie(self): # Need cookie and ID Token to figure this out id_token = self._auth_with_id_token() assert session_get(self.provider.sdb, "sub", id_token["sub"]) # verify we got valid session id_token_hint = id_token.to_jwt(algorithm="none") cookie = self._create_cookie("diggins", "number5") resp = self.provider.end_session_endpoint(urlencode( {"id_token_hint": id_token_hint}), cookie=cookie) assert isinstance(resp, Response) _err = ErrorResponse().from_json(resp.message) assert _err["error"] == "invalid_request" assert _err["error_description"] == "Wrong user" def test_end_session_endpoint_with_cookie_wrong_user(self): # Need cookie and ID Token to figure this out id_token = self._auth_with_id_token() assert session_get(self.provider.sdb, "sub", id_token["sub"]) id_token_hint = id_token.to_jwt(algorithm="none") cookie = self._create_cookie("diggins", "number5") resp = self.provider.end_session_endpoint(urlencode( {"id_token_hint": id_token_hint}), cookie=cookie) assert isinstance(resp, Response) _err = ErrorResponse().from_json(resp.message) assert _err["error"] == "invalid_request" assert _err["error_description"] == "Wrong user" def test_end_session_endpoint_with_cookie_wrong_client(self): # Need cookie and ID Token to figure this out id_token = self._auth_with_id_token() assert session_get(self.provider.sdb, "sub", id_token["sub"]) id_token_hint = id_token.to_jwt(algorithm="none") # Wrong client_id cookie = self._create_cookie("username", "a1b2c3") resp = self.provider.end_session_endpoint(urlencode( {"id_token_hint": id_token_hint}), cookie=cookie) assert isinstance(resp, Response) _err = ErrorResponse().from_json(resp.message) assert _err["error"] == "invalid_request" def test_end_session_endpoint_with_cookie_dual_login(self): self._code_auth() self._code_auth2() cookie = self._create_cookie("username", "client0") resp = self.provider.end_session_endpoint(urlencode({"state": "abcde"}), cookie=cookie) # returns a SeeOther instance p = urlparse(resp.message) qs = parse_qs(p.query) jwt_info = self.provider.unpack_signed_jwt(qs["sjwt"][0]) assert jwt_info["state"] == "abcde" assert jwt_info["uid"] == "username" assert jwt_info["client_id"] == "client0" assert jwt_info[ "redirect_uri"] == "https://www.example.org/post_logout" def test_end_session_endpoint_with_cookie_dual_login_wrong_client(self): self._code_auth() self._code_auth2() # The cookie states that a user has a session at a client and this # statement is false. cookie = self._create_cookie("username", "a1b2c3") resp = self.provider.end_session_endpoint(urlencode({"state": "abcde"}), cookie=cookie) assert isinstance(resp, Response) _err = ErrorResponse().from_json(resp.message) assert _err["error"] == "invalid_request" def test_end_session_endpoint_with_id_token_hint_only(self): id_token = self._auth_with_id_token() assert session_get(self.provider.sdb, "sub", id_token["sub"]) id_token_hint = id_token.to_jwt(algorithm="none") resp = self.provider.end_session_endpoint( urlencode({"id_token_hint": id_token_hint})) # returns a SeeOther instance p = urlparse(resp.message) qs = parse_qs(p.query) jwt_info = self.provider.unpack_signed_jwt(qs["sjwt"][0]) assert jwt_info["uid"] == "username" assert jwt_info["client_id"] == "number5" assert jwt_info["redirect_uri"] == "https://example.com/post_logout" def test_end_session_endpoint_with_id_token_hint_and_cookie(self): id_token = self._auth_with_id_token() assert session_get(self.provider.sdb, "sub", id_token["sub"]) id_token_hint = id_token.to_jwt(algorithm="none") cookie = self._create_cookie("username", "number5") resp = self.provider.end_session_endpoint(urlencode( {"id_token_hint": id_token_hint}), cookie=cookie) # returns a SeeOther instance p = urlparse(resp.message) qs = parse_qs(p.query) jwt_info = self.provider.unpack_signed_jwt(qs["sjwt"][0]) assert jwt_info["uid"] == "username" assert jwt_info["client_id"] == "number5" assert jwt_info["redirect_uri"] == "https://example.com/post_logout" def test_end_session_endpoint_with_post_logout_redirect_uri(self): self._code_auth() cookie = self._create_cookie("username", "number5") post_logout_redirect_uri = self.CDB[str( CLIENT_CONFIG["client_id"])]["post_logout_redirect_uris"][0][0] resp = self.provider.end_session_endpoint( urlencode({ "post_logout_redirect_uri": post_logout_redirect_uri, "state": "abcde" }), cookie=cookie, ) # returns a SeeOther instance p = urlparse(resp.message) qs = parse_qs(p.query) jwt_info = self.provider.unpack_signed_jwt(qs["sjwt"][0]) assert jwt_info["state"] == "abcde" assert jwt_info["uid"] == "username" assert jwt_info["client_id"] == "number5" assert jwt_info["redirect_uri"] == "https://example.com/post_logout" def test_end_session_endpoint_without_post_logout_redirect_uri(self): # default post logout page registered self.provider.post_logout_page = "https://foo.example.com/def_post" # No post_logout_redirect_uris registered _plru = self.provider.cdb["number5"]["post_logout_redirect_uris"] del self.provider.cdb["number5"]["post_logout_redirect_uris"] self._code_auth() cookie = self._create_cookie("username", "number5") resp = self.provider.end_session_endpoint(urlencode({"state": "abcde"}), cookie=cookie) # returns a SeeOther instance p = urlparse(resp.message) qs = parse_qs(p.query) jwt_info = self.provider.unpack_signed_jwt(qs["sjwt"][0]) assert jwt_info["state"] == "abcde" assert jwt_info["uid"] == "username" assert jwt_info["client_id"] == "number5" assert jwt_info["redirect_uri"] == "https://foo.example.com/def_post" # restore self.provider.cdb["number5"]["post_logout_redirect_uris"] = _plru def test_end_session_endpoint_without_post_logout_redirect_uri_no_default( self): # No post_logout_redirect_uris registered _plru = self.provider.cdb["number5"]["post_logout_redirect_uris"] del self.provider.cdb["number5"]["post_logout_redirect_uris"] self._code_auth() cookie = self._create_cookie("username", "number5") resp = self.provider.end_session_endpoint(urlencode({"state": "abcde"}), cookie=cookie) assert isinstance(resp, Response) _err = ErrorResponse().from_json(resp.message) assert _err["error"] == "server_error" # restore self.provider.cdb["number5"]["post_logout_redirect_uris"] = _plru def test_end_session_endpoint_bogus_sjwt(self): self._code_auth() cookie = self._create_cookie("username", "number5") post_logout_redirect_uri = self.CDB[str( CLIENT_CONFIG["client_id"])]["post_logout_redirect_uris"][0][0] resp = self.provider.end_session_endpoint( urlencode({ "post_logout_redirect_uri": post_logout_redirect_uri, "state": "abcde" }), cookie=cookie, ) # returns a SeeOther instance p = urlparse(resp.message) qs = parse_qs(p.query) _sjwt = qs["sjwt"][0] _sjwt = ".".join(_sjwt.split(".")[:2]) + "." # Not signed with pytest.raises(ValueError): self.provider.unpack_signed_jwt(_sjwt) def test_end_session_endpoint_with_wrong_post_logout_redirect_uri(self): self._code_auth() cookie = self._create_cookie("username", "number5") post_logout_redirect_uri = "https://www.example.com/logout" resp = self.provider.end_session_endpoint( urlencode({ "post_logout_redirect_uri": post_logout_redirect_uri, "state": "abcde" }), cookie=cookie, ) assert isinstance(resp, Response) _err = ErrorResponse().from_json(resp.message) assert _err["error"] == "invalid_request" def test_end_session_endpoint_with_registered_post_logout_redirect_uri_with_query_part( self, ): self._code_auth() cookie = self._create_cookie("username", "number5") self.provider.cdb["number5"]["post_logout_redirect_uris"] = [ ("https://www.example.com/logout", { "foo": ["bar"] }) ] # No post_logout_redirect_uri in request resp = self.provider.end_session_endpoint(urlencode({"state": "abcde"}), cookie=cookie) assert isinstance(resp, Response) _qp = parse_qs(resp.message.split("?")[1]) _jwt = self.provider.unpack_signed_jwt(_qp["sjwt"][0]) assert _jwt["redirect_uri"] == "https://www.example.com/logout?foo=bar" def test_back_channel_logout_no_uri(self): self._code_auth() res = self.provider.do_back_channel_logout( self.provider.cdb[CLIENT_ID], "username", "sid") assert res is None def test_back_channel_logout(self): self._code_auth() _cdb = copy.copy(self.provider.cdb[CLIENT_ID]) _cdb["backchannel_logout_uri"] = "https://example.com/bc_logout" _cdb["client_id"] = CLIENT_ID res = self.provider.do_back_channel_logout(_cdb, "username", "_sid_") assert isinstance(res, tuple) assert res[0] == "https://example.com/bc_logout" _jwt = self.provider.unpack_signed_jwt(res[1]) assert _jwt assert _jwt["iss"] == SERVER_INFO["issuer"] assert _jwt["aud"] == [CLIENT_ID] assert _jwt["sub"] == "username" assert _jwt["sid"] == "_sid_" def test_front_channel_logout(self): self._code_auth() _cdb = copy.copy(self.provider.cdb[CLIENT_ID]) _cdb["frontchannel_logout_uri"] = "https://example.com/fc_logout" _cdb["client_id"] = CLIENT_ID res = self.provider.do_front_channel_logout_iframe( _cdb, str(SERVER_INFO["issuer"]), "_sid_") assert res == '<iframe src="https://example.com/fc_logout">' def test_front_channel_logout_session_required(self): self._code_auth() _cdb = copy.copy(self.provider.cdb[CLIENT_ID]) _cdb["frontchannel_logout_uri"] = "https://example.com/fc_logout" _cdb["frontchannel_logout_session_required"] = True _cdb["client_id"] = CLIENT_ID res = self.provider.do_front_channel_logout_iframe( _cdb, str(SERVER_INFO["issuer"]), "_sid_") m = re.match(r'<iframe src="([^"]+)">', str(res)) assert m _q = parse_qs(str(m.group(1)).split("?")[1]) assert set(_q.keys()) == {"iss", "sid"} def test_front_channel_logout_session_required_uri_query(self): self._code_auth() _cdb = copy.copy(self.provider.cdb[CLIENT_ID]) _cdb[ "frontchannel_logout_uri"] = "https://example.com/fc_logout?foo=bar" _cdb["frontchannel_logout_session_required"] = True _cdb["client_id"] = CLIENT_ID res = self.provider.do_front_channel_logout_iframe( _cdb, str(SERVER_INFO["issuer"]), "_sid_") m = re.match(r'<iframe src="([^"]+)">', str(res)) assert m _q = parse_qs(str(m.group(1)).split("?")[1]) assert set(_q.keys()) == {"foo", "iss", "sid"} def test_front_channel_logout_missing_url(self): self._code_auth() _cdb = copy.copy(self.provider.cdb[CLIENT_ID]) _cdb["client_id"] = CLIENT_ID res = self.provider.do_front_channel_logout_iframe( _cdb, str(SERVER_INFO["issuer"]), "_sid_") assert res is None def test_logout_from_client_bc(self): self._code_auth() self.provider.cdb[CLIENT_ID][ "backchannel_logout_uri"] = "https://example.com/bc_logout" self.provider.cdb[CLIENT_ID]["client_id"] = CLIENT_ID # Get a session ID, anyone will do. # I know the session backend DB is a DictSessionBackend so I can use that _sid = list(self.provider.sdb._db.storage.keys())[0] res = self.provider.logout_info_for_one_client(_sid, CLIENT_ID) assert set(res.keys()) == {"back_channel", "front_channel"} assert res["back_channel"] != {} assert res["front_channel"] == {} assert set(res["back_channel"].keys()) == {CLIENT_ID} _spec = res["back_channel"][CLIENT_ID] assert _spec[0] == "https://example.com/bc_logout" _jwt = self.provider.unpack_signed_jwt(_spec[1]) assert _jwt assert _jwt["iss"] == SERVER_INFO["issuer"] assert _jwt["aud"] == [CLIENT_ID] assert _jwt["sid"] == _sid def test_logout_from_client_fc(self): self._code_auth() del self.provider.cdb[CLIENT_ID]["backchannel_logout_uri"] self.provider.cdb[CLIENT_ID][ "frontchannel_logout_uri"] = "https://example.com/fc_logout" self.provider.cdb[CLIENT_ID]["client_id"] = CLIENT_ID # Get a session ID, anyone will do. # I know the session backend DB is a DictSessionBackend so I can use that _sid = list(self.provider.sdb._db.storage.keys())[0] res = self.provider.logout_info_for_one_client(_sid, CLIENT_ID) assert set(res.keys()) == {"front_channel", "back_channel"} assert res["back_channel"] == {} assert set(res["front_channel"].keys()) == {CLIENT_ID} _spec = res["front_channel"][CLIENT_ID] assert _spec == '<iframe src="https://example.com/fc_logout">' def test_logout_from_client(self): self._code_auth() self._code_auth2() # client0 self.provider.cdb["client0"][ "backchannel_logout_uri"] = "https://example.com/bc_logout" self.provider.cdb["client0"]["client_id"] = "client0" self.provider.cdb["number5"][ "frontchannel_logout_uri"] = "https://example.com/fc_logout" self.provider.cdb["number5"]["client_id"] = CLIENT_ID # Get a session ID, anyone will do. # I know the session backend DB is a DictSessionBackend so I can use that _sid = list(self.provider.sdb._db.storage.keys())[0] res = self.provider.logout_info_for_all_clients(sid=_sid) assert res assert set(res.keys()) == {"back_channel", "front_channel"} assert set(res["front_channel"].keys()) == {"number5"} _spec = res["front_channel"]["number5"] assert _spec == '<iframe src="https://example.com/fc_logout">' assert set(res["back_channel"].keys()) == {"client0"} _spec = res["back_channel"]["client0"] assert _spec[0] == "https://example.com/bc_logout" _jwt = self.provider.unpack_signed_jwt(_spec[1]) assert _jwt assert _jwt["iss"] == SERVER_INFO["issuer"] assert _jwt["aud"] == ["client0"] def test_logout_spec_all(self): self._code_auth() self._code_auth2() # client0 self.provider.cdb["client0"][ "backchannel_logout_uri"] = "https://example.com/bc_logout" self.provider.cdb["client0"]["client_id"] = "client0" self.provider.cdb["number5"][ "frontchannel_logout_uri"] = "https://example.com/fc_logout" self.provider.cdb["number5"]["client_id"] = CLIENT_ID # Get a session ID, anyone will do. # I know the session backend DB is a DictSessionBackend so I can use that _sid = list(self.provider.sdb._db.storage.keys())[0] logout_spec_all = self.provider.logout_info_for_all_clients(sid=_sid) assert set(logout_spec_all.keys()) == {"back_channel", "front_channel"} assert set(logout_spec_all["back_channel"].keys()) == {"client0"} assert set(logout_spec_all["front_channel"].keys()) == {"number5"} def test_do_verified_logout_all(self): self._code_auth() self._code_auth2() # client0 self.provider.cdb["client0"][ "backchannel_logout_uri"] = "https://example.com/bc_logout" self.provider.cdb["client0"]["client_id"] = "client0" self.provider.cdb["number5"][ "frontchannel_logout_uri"] = "https://example.com/fc_logout" self.provider.cdb["number5"]["client_id"] = CLIENT_ID # Get a session ID, anyone will do. # I know the session backend DB is a DictSessionBackend so I can use that _sid = list(self.provider.sdb._db.storage.keys())[0] with responses.RequestsMock() as rsps: rsps.add(rsps.POST, "https://example.com/bc_logout", status=200) res = self.provider.do_verified_logout(_sid, CLIENT_ID, alla=True) assert set(res.keys()) == {"iframe", "cookie"} def test_do_verified_logout_just_the_one(self): self.provider.events = DummyEventStore() self._code_auth() self._code_auth2() # client0 self.provider.cdb["client0"][ "backchannel_logout_uri"] = "https://example.com/bc_logout" self.provider.cdb["client0"]["client_id"] = "client0" self.provider.cdb["number5"][ "frontchannel_logout_uri"] = "https://example.com/fc_logout" self.provider.cdb["number5"]["client_id"] = CLIENT_ID # Get a session ID, anyone will do. # I know the session backend DB is a DictSessionBackend so I can use that _sid = list(self.provider.sdb._db.storage.keys())[0] # There is no back channel logout, hence there should be no HTTP POST exception = requests.ConnectionError() with responses.RequestsMock( assert_all_requests_are_fired=False) as rsps: rsps.add(responses.POST, "https://example.com/bc_logout", body=exception) res = self.provider.do_verified_logout(_sid, CLIENT_ID, alla=False) assert set(res.keys()) == {"iframe", "cookie"} def test_do_verified_logout_the_other(self): self._code_auth() self._code_auth2() # client0 self.provider.cdb["client0"][ "backchannel_logout_uri"] = "https://example.com/bc_logout" self.provider.cdb["client0"]["client_id"] = "client0" self.provider.cdb["number5"][ "frontchannel_logout_uri"] = "https://example.com/fc_logout" self.provider.cdb["number5"]["client_id"] = CLIENT_ID # Get a session ID, anyone will do. # I know the session backend DB is a DictSessionBackend so I can use that _sid = list(self.provider.sdb._db.storage.keys())[0] # This only does back channel logout with responses.RequestsMock() as rsps: rsps.add(rsps.POST, "https://example.com/bc_logout", status=200) res = self.provider.do_verified_logout(_sid, "client0", alla=False) assert set(res.keys()) == {"cookie"} def test_do_verified_logout_the_other_back_channel_failed(self): self._code_auth() self._code_auth2() # client0 self.provider.cdb["client0"][ "backchannel_logout_uri"] = "https://example.com/bc_logout" self.provider.cdb["client0"]["client_id"] = "client0" self.provider.cdb["number5"][ "frontchannel_logout_uri"] = "https://example.com/fc_logout" self.provider.cdb["number5"]["client_id"] = CLIENT_ID # Get a session ID, anyone will do. # I know the session backend DB is a DictSessionBackend so I can use that _sid = list(self.provider.sdb._db.storage.keys())[0] # Does back channel logout and it will fail with responses.RequestsMock() as rsps: rsps.add(rsps.POST, "https://example.com/bc_logout", status=400) res = self.provider.do_verified_logout(_sid, "client0", alla=False) assert list(res.keys()) == [] def test_end_session_endpoint_no_post_logout_redirect_uri(self): self._code_auth() cookie = self._create_cookie("username", "number5") self.provider.cdb["number5"]["post_logout_redirect_uris"] = [ ("https://example.com/plru", ""), ("https://example.com/plru2", ""), ] res = self.provider.end_session_endpoint(urlencode({"state": "abcde"}), cookie=cookie) assert isinstance(res, Response) assert res.status_code == 400 def test_logout_info_for_all_clients_no_params(self): with pytest.raises(ParameterError): self.provider.logout_info_for_all_clients() def test_do_back_channel_logout_no_backchannel(self): self._code_auth() # Get a session ID, anyone will do. # I know the session backend DB is a DictSessionBackend so I can use that _sid = list(self.provider.sdb._db.storage.keys())[0] _sub = self.provider.sdb[_sid]["sub"] # if "backchannel_logout_uri" in self.provider.cdb["number5"]: del self.provider.cdb["number5"]["backchannel_logout_uri"] res = self.provider.do_back_channel_logout( self.provider.cdb["number5"], _sub, _sid) assert res is None def test_id_token_hint_multiple_aud(self): id_token = self._auth_with_id_token() assert session_get(self.provider.sdb, "sub", id_token["sub"]) # verify we got valid session self.provider.cdb["number5"]["post_logout_redirect_uris"] = [ ("https://example.com/plru", "") ] # add another aud and an azp. id_token["azp"] = id_token["aud"][0] id_token["aud"].append("foobar") id_token_hint = id_token.to_jwt(algorithm="none") resp = self.provider.end_session_endpoint( urlencode({"id_token_hint": id_token_hint})) assert isinstance(resp, SeeOther) def test_id_token_hint_aud_does_not_match_client_id(self): id_token = self._auth_with_id_token() assert session_get(self.provider.sdb, "sub", id_token["sub"]) # verify we got valid session # add another aud and an azp. id_token_hint = id_token.to_jwt(algorithm="none") # Mess with the session DB _sid = list(self.provider.sdb._db.storage.keys())[0] self.provider.sdb[_sid]["client_id"] = "something else" resp = self.provider.end_session_endpoint( urlencode({"id_token_hint": id_token_hint})) assert isinstance(resp, Response) assert resp.status_code == 400 def test_no_back_or_front_channel_logout(self): self._code_auth() # Mess with client DB for c in ["backchannel_logout_uri", "frontchannel_logout_uri"]: if c in self.provider.cdb["number5"]: del self.provider.cdb["number5"][c] resp = self.provider.do_verified_logout(sid=list( self.provider.sdb._db.storage.keys())[0], client_id="number5") assert resp == {} def test_back_channel_logout_fails(self): self._code_auth() # client0 self.provider.cdb["client0"][ "backchannel_logout_uri"] = "https://example.com/bc_logout" self.provider.cdb["client0"]["client_id"] = "client0" # Get a session ID, anyone will do. # I know the session backend DB is a DictSessionBackend so I can use that _sid = list(self.provider.sdb._db.storage.keys())[0] # There is no back channel logout, hence there should be no HTTP POST with responses.RequestsMock(): res = self.provider.do_verified_logout(_sid, "client0", alla=False) assert res == {} def test_logout_info_for_one_client_no_logout_info(self): self._code_auth() # Mess with client DB for c in ["backchannel_logout_uri", "frontchannel_logout_uri"]: if c in self.provider.cdb["number5"]: del self.provider.cdb["number5"][c] # Get a session ID, anyone will do. # I know the session backend DB is a DictSessionBackend so I can use that _sid = list(self.provider.sdb._db.storage.keys())[0] resp = self.provider.logout_info_for_one_client(_sid, "number5") assert resp == {"back_channel": {}, "front_channel": {}} def test_unknown_client(self): self._code_auth() cookie = self._create_cookie("username", "unknown") resp = self.provider.end_session_endpoint(urlencode({"state": "abcde"}), cookie=cookie) assert isinstance(resp, Response) assert resp.status_code == 400 def test_no_cookie_no_id_token_hint(self): self._code_auth() resp = self.provider.end_session_endpoint(urlencode({"state": "abcde"})) assert isinstance(resp, Response) assert resp.status_code == 400 def test_back_channel_logout_failed_front_channel_logout_exists(self): self._code_auth() # client0 self.provider.cdb["number5"][ "backchannel_logout_uri"] = "https://example.com/bc_logout" self.provider.cdb["number5"][ "frontchannel_logout_uri"] = "https://example.com/fc_logout" self.provider.cdb["number5"]["client_id"] = "number5" # Get a session ID, anyone will do. # I know the session backend DB is a DictSessionBackend so I can use that _sid = list(self.provider.sdb._db.storage.keys())[0] # Does back channel logout and it will fail with responses.RequestsMock() as rsps: rsps.add(rsps.POST, "https://example.com/bc_logout", status=400) res = self.provider.do_verified_logout(_sid, "client0", alla=True) assert set(res.keys()) == {"cookie", "iframe"} def test_logout_from_clients_one_without_logout_info(self): self._code_auth() self._code_auth2() # Mess with client DB # neither back channel nor front channel for c in ["backchannel_logout_uri", "frontchannel_logout_uri"]: if c in self.provider.cdb["client0"]: del self.provider.cdb["client0"][c] self.provider.cdb["client0"]["client_id"] = "client0" # both back channel and front channel self.provider.cdb["number5"][ "frontchannel_logout_uri"] = "https://example.com/fc_logout" self.provider.cdb["number5"]["client_id"] = "number5" # Get a session ID, anyone will do. # I know the session backend DB is a DictSessionBackend so I can use that _sid = list(self.provider.sdb._db.storage.keys())[0] res = self.provider.logout_info_for_all_clients(sid=_sid) assert set(res.keys()) == {"back_channel", "front_channel"} assert set(res["back_channel"].keys()) == {"number5"} assert set(res["front_channel"].keys()) == {"number5"}
#AUTHN = UsernamePasswordMako(None, "login.mako", tl, PASSWD, "authenticated") AUTHN_BROKER = AuthnBroker() AUTHN_BROKER.add("UNDEFINED", DummyAuthn(None, "username")) # dealing with authorization AUTHZ = AuthzHandling() SYMKEY = rndstr(16) # symmetric key used to encrypt cookie info USERINFO = UserInfo(USERDB) provider_init = Provider("pyoicserv", SessionDB(SERVER_INFO["issuer"]), CDB, AUTHN_BROKER, USERINFO, AUTHZ, verify_client, SYMKEY, urlmap=URLMAP, keyjar=KEYJAR) def _eq(l1, l2): return set(l1) == set(l2) def test_server_init(): server = provider_init assert server assert server.authn_broker == AUTHN_BROKER
class OIDCFrontend(FrontendModule): """ A OpenID Connect frontend module """ MANDATORY_CONFIG = {"issuer", "signing_key_path"} def __init__(self, auth_req_callback_func, internal_attributes, conf): self._validate_config(conf) super(OIDCFrontend, self).__init__(auth_req_callback_func, internal_attributes) self.state_id = type(self).__name__ self.sign_alg = "RS256" self.subject_type_default = "pairwise" self.conf = conf def handle_authn_response(self, context, internal_resp): """ See super class method satosa.frontends.base.FrontendModule#handle_authn_response :type context: satosa.context.Context :type internal_response: satosa.internal_data.InternalResponse :rtype oic.utils.http_util.Response """ auth_req = self._get_authn_request_from_state(context.state) # filter attributes to return in ID Token as claims attributes = self.converter.from_internal("openid", internal_resp.get_attributes()) satosa_logging(LOGGER, logging.DEBUG, "Attributes delivered by backend to OIDC frontend: {}".format( json.dumps(attributes)), context.state) flattened_attributes = {k: v[0] for k, v in attributes.items()} requested_id_token_claims = auth_req.get("claims", {}).get("id_token") user_claims = self._get_user_info(flattened_attributes, requested_id_token_claims, auth_req["scope"]) satosa_logging(LOGGER, logging.DEBUG, "Attributes filtered by requested claims/scope: {}".format( json.dumps(user_claims)), context.state) # construct epoch timestamp of reported authentication time auth_time = datetime.datetime.strptime(internal_resp.auth_info.timestamp, "%Y-%m-%dT%H:%M:%SZ") epoch_timestamp = (auth_time - datetime.datetime(1970, 1, 1)).total_seconds() base_claims = {"client_id": auth_req["client_id"], "sub": internal_resp.get_user_id(), "nonce": auth_req["nonce"]} id_token = self.provider.id_token_as_signed_jwt(base_claims, user_info=user_claims, auth_time=epoch_timestamp, loa="", alg=self.sign_alg) oidc_client_state = auth_req.get("state") kwargs = {} if oidc_client_state: # inlcude any optional 'state' sent by the client in the authn req kwargs["state"] = oidc_client_state auth_resp = AuthorizationResponse(id_token=id_token, **kwargs) http_response = auth_resp.request(auth_req["redirect_uri"], self._should_fragment_encode(auth_req)) return SeeOther(http_response) def handle_backend_error(self, exception): """ See super class satosa.frontends.base.FrontendModule :type exception: satosa.exception.SATOSAError :rtype: oic.utils.http_util.Response """ auth_req = self._get_authn_request_from_state(exception.state) error_resp = AuthorizationErrorResponse(error="access_denied", error_description=exception.message) satosa_logging(LOGGER, logging.DEBUG, exception.message, exception.state) return SeeOther( error_resp.request(auth_req["redirect_uri"], self._should_fragment_encode(auth_req))) def register_endpoints(self, providers): """ See super class satosa.frontends.base.FrontendModule :type providers: list[str] :rtype: list[(str, ((satosa.context.Context, Any) -> satosa.response.Response, Any))] :raise ValueError: if more than one backend is configured """ if len(providers) != 1: raise ValueError("OpenID Connect frontend only supports one backend.") backend = providers[0] endpoint_baseurl = "{}/{}".format(self.conf["issuer"], backend) jwks_uri = "{}/jwks".format(self.conf["issuer"]) self._create_op(self.conf["issuer"], endpoint_baseurl, jwks_uri) provider_config = ( "^.well-known/openid-configuration$", self._provider_config) jwks_uri = ("^jwks$", self._jwks) dynamic_client_registration = ( "^{}/{}".format(backend, RegistrationEndpoint.url), self._register_client) authentication = ( "^{}/{}".format(backend, AuthorizationEndpoint.url), self.handle_authn_request) url_map = [provider_config, jwks_uri, dynamic_client_registration, authentication] return url_map def _create_op(self, issuer, endpoint_baseurl, jwks_uri): """ Create the necessary Provider instance. :type issuer: str :type endpoint_baseurl: str :type jwks_uri: str :param issuer: issuer URL for the OP :param endpoint_baseurl: baseurl to build endpoint URL from :param jwks_uri: URL to where the JWKS will be published """ kj = KeyJar() signing_key = KeyBundle(source="file://{}".format(self.conf["signing_key_path"]), fileformat="der", keyusage=["sig"]) kj.add_kb("", signing_key) capabilities = { "response_types_supported": ["id_token"], "id_token_signing_alg_values_supported": [self.sign_alg], "response_modes_supported": ["fragment", "query"], "subject_types_supported": ["public", "pairwise"], "grant_types_supported": ["implicit"], "claim_types_supported": ["normal"], "claims_parameter_supported": True, "request_parameter_supported": False, "request_uri_parameter_supported": False, } if "client_db_path" in self.conf: cdb = shelve_wrapper.open(self.conf["client_db_path"]) else: cdb = {} # client db in memory only self.provider = Provider(issuer, None, cdb, None, None, None, None, None, keyjar=kj, capabilities=capabilities, jwks_uri=jwks_uri) self.provider.baseurl = endpoint_baseurl self.provider.endp = [RegistrationEndpoint, AuthorizationEndpoint] def _get_user_info(self, user_attributes, requested_claims=None, scopes=None): """ Filter user attributes to return to the client (as claims in the ID Token) based on what was requested in request 'claims' parameter and in the 'scope'. :type user_attributes: dict[str, str] :type requested_claims: dict[str, Optional[dict]] :type scopes: list[str] :rtype: dict[str, str] :param user_attributes: attributes provided by the backend :param requested_claims: claims requested by the client through the 'claims' request param :param scopes: the scopes requested by the client :return: all attributes/claims to return to the client """ requested_claims = requested_claims or {} scopes = scopes or [] claims_requested_by_scope = Provider._scope2claims(scopes) claims_requested_by_scope.update( requested_claims) # let explicit claims request override scope return UserInfo().filter(user_attributes, claims_requested_by_scope) def _validate_config(self, config): """ Validates that all necessary config parameters are specified. :type config: dict[str, dict[str, Any] | str] :param config: the module config """ if config is None: raise ValueError("OIDCFrontend conf can't be 'None'.") for k in self.MANDATORY_CONFIG: if k not in config: raise ValueError( "Missing configuration parameter '{}' for OpenID Connect frontend.".format( k)) def _should_fragment_encode(self, authn_req): """ Determine, based on the clients request, whether the authentication/error response should be fragment encoded or not. :type authn_req: oic.oic.message.AuthorizationRequest :rtype: bool :param authn_req: parsed authentication request from the client :return: True if the response should be fragment encoded """ return authn_req.get("response_mode", "fragment") == "fragment" def _get_authn_request_from_state(self, state): """ Extract the clietns request stoed in the SATOSA state. :type state: satosa.state.State :rtype: oic.oic.message.AuthorizationRequest :param state: the current state :return: the parsed authentication request """ stored_state = state.get(self.state_id) oidc_request = stored_state["oidc_request"] return AuthorizationRequest().deserialize(oidc_request) def _register_client(self, context): """ Handle the OIDC dynamic client registration. :type context: satosa.context.Context :rtype: oic.utils.http_util.Response :param context: the current context :return: HTTP response to the client """ http_resp = self.provider.registration_endpoint(json.dumps(context.request)) if not isinstance(http_resp, Created): return http_resp return self._fixup_registration_response(http_resp) def _fixup_registration_response(self, http_resp): # remove client_secret since no token endpoint is published response = RegistrationResponse().deserialize(http_resp.message, "json") del response["client_secret"] # specify supported id token signing alg response["id_token_signed_response_alg"] = self.sign_alg http_resp.message = response.to_json() return http_resp def _provider_config(self, context): """ Construct the provider configuration information (served at /.well-known/openid-configuration). :type context: satosa.context.Context :rtype: oic.utils.http_util.Response :param context: the current context :return: HTTP response to the client """ http_resp = self.provider.providerinfo_endpoint() if not isinstance(http_resp, Response): return http_resp provider_config = ProviderConfigurationResponse().deserialize(http_resp.message, "json") del provider_config["token_endpoint_auth_methods_supported"] del provider_config["require_request_uri_registration"] http_resp.message = provider_config.to_json() return http_resp 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 _jwks(self, context): """ Construct the JWKS document (served at /jwks). :type context: satosa.context.Context :rtype: oic.utils.http_util.Response :param context: the current context :return: HTTP response to the client """ return Response(json.dumps(self.provider.keyjar.export_jwks()), content="application/json")
# dealing with authorization authz = AuthzHandling() kwargs = { "template_lookup": LOOKUP, "template": {"form_post": "form_response.mako"}, #"template_args": {"form_post": {"action": "form_post"}} } # Should I care about verifying the certificates used by other entities if args.insecure: kwargs["verify_ssl"] = False else: kwargs["verify_ssl"] = True OAS = Provider(config.issuer, SessionDB(), cdb, ac, None, authz, verify_client, config.SYM_KEY, **kwargs) for authn in ac: authn.srv = OAS if config.USERINFO == "SIMPLE": # User info is a simple dictionary in this case statically defined in # the configuration file OAS.userinfo = UserInfo(config.USERDB) elif config.USERINFO == "SAML": OAS.userinfo = UserInfo(config.SAML) elif config.USERINFO == "AA": OAS.userinfo = AaUserInfo(config.SP_CONFIG, config.issuer, config.SAML) else: raise Exception("Unsupported userinfo source")
}, #"template_args": {"form_post": {"action": "form_post"}} } # Should I care about verifying the certificates used by other entities if args.insecure: kwargs["verify_ssl"] = False else: kwargs["verify_ssl"] = True if args.capabilities: kwargs["capabilities"] = json.loads(open(args.capabilities).read()) else: pass OAS = Provider(config.issuer, SessionDB(config.baseurl), cdb, ac, None, authz, verify_client, config.SYM_KEY, **kwargs) OAS.baseurl = config.issuer for authn in ac: authn.srv = OAS if config.USERINFO == "SIMPLE": # User info is a simple dictionary in this case statically defined in # the configuration file OAS.userinfo = UserInfo(config.USERDB) elif config.USERINFO == "SAML": OAS.userinfo = UserInfo(config.SAML) elif config.USERINFO == "AA": OAS.userinfo = AaUserInfo(config.SP_CONFIG, config.issuer, config.SAML) else: raise Exception("Unsupported userinfo source")
def main(): parser = argparse.ArgumentParser(description='Example OIDC Provider.') parser.add_argument("-p", "--port", default=80, type=int) parser.add_argument("-b", "--base", default="https://localhost", type=str) parser.add_argument("-d", "--debug", action="store_true") parser.add_argument("settings") args = parser.parse_args() # Load configuration with open(args.settings, "r") as f: settings = yaml.load(f) issuer = args.base.rstrip("/") template_dirs = settings["server"].get("template_dirs", "templates") jinja_env = Environment(loader=FileSystemLoader(template_dirs)) authn_broker, auth_routing = setup_authentication_methods( settings["authn"], jinja_env) # Setup userinfo userinfo_conf = settings["userinfo"] cls = make_cls_from_name(userinfo_conf["class"]) i = cls(**userinfo_conf["kwargs"]) userinfo = UserInfo(i) client_db = {} provider = Provider(issuer, SessionDB(issuer), client_db, authn_broker, userinfo, AuthzHandling(), verify_client, None) provider.baseurl = issuer provider.symkey = rndstr(16) # Setup keys path = os.path.join(os.path.dirname(__file__), "static") try: os.makedirs(path) except OSError as e: if e.errno != errno.EEXIST: raise e pass jwks = keyjar_init(provider, settings["provider"]["keys"]) name = "jwks.json" with open(os.path.join(path, name), "w") as f: f.write(json.dumps(jwks)) provider.jwks_uri.append("{}/static/{}".format(provider.baseurl, name)) # Mount the WSGI callable object (app) on the root directory app_routing = setup_endpoints(provider) app_routing["/.well-known/openid-configuration"] = pyoidcMiddleware( provider.providerinfo_endpoint) app_routing["/.well-known/webfinger"] = pyoidcMiddleware( partial(_webfinger, provider)) routing = dict(list(auth_routing.items()) + list(app_routing.items())) routing["/static"] = make_static_handler(path) dispatcher = WSGIPathInfoDispatcher(routing) server = wsgiserver.CherryPyWSGIServer(('0.0.0.0', args.port), dispatcher) # Setup SSL if provider.baseurl.startswith("https://"): server.ssl_adapter = BuiltinSSLAdapter( settings["server"]["cert"], settings["server"]["key"], settings["server"]["cert_chain"]) # Start the CherryPy WSGI web server try: print("Server started: {}".format(issuer)) server.start() except KeyboardInterrupt: server.stop()
class InAcademiaOpenIDConnectFrontend(object): def __init__(self, base_url, client_metadata_func): # Read OP configuration from file with open("conf/op_config.json", "r") as f: op_capabilities = json.load(f) for key, value in op_capabilities.iteritems(): if isinstance(value, basestring): op_capabilities[key] = value.format(base=base_url) # replace placeholder with the actual base name self.OP = Provider(base_url, {}, client_metadata_func, None, None, None, None, None, capabilities=op_capabilities) self.OP.baseurl = op_capabilities["issuer"] # Setup up keys for signing and encrypting self.OP.keyjar = KeyJar() kb = keybundle_from_local_file("inAcademia", "RSA", ["sig", "enc"]) self.OP.keyjar.add_kb("", kb) try: file_name = "static/jwks.json" dump_jwks([kb], file_name) self.OP.jwks_uri.append("{}/{}".format(base_url, file_name)) except Exception as e: logger.exception("Signing and encryption keys could not be written to jwks.json.") raise def id_token(self, released_claims, idp_entity_id, transaction_id, transaction_session): """Make a JWT encoded id token and pass it to the redirect URI. :param released_claims: dictionary containing the following user_id: identifier for the user (as delivered by the IdP, dependent on whether transient or persistent id was requested) auth_time: time of the authentication reported from the IdP idp_entity_id: entity id of the selected IdP :param transaction_id: :return: raises cherrypy.HTTPRedirect. """ identifier = released_claims["Identifier"] auth_time = released_claims["Authentication time"] # have to convert text representation into seconds since epoch _time = time.mktime(str_to_time(auth_time)) # construct the OIDC response transaction_session["sub"] = identifier extra_claims = {k.lower(): released_claims[k] for k in ["Country", "Domain"] if k in released_claims} _jwt = self.OP.id_token_as_signed_jwt(transaction_session, loa="", auth_time=_time, exp={"minutes": 30}, extra_claims=extra_claims) _elapsed_transaction_time = get_timestamp() - transaction_session["start_time"] log_transaction_complete(logger, cherrypy.request, transaction_id, transaction_session["client_id"], idp_entity_id, _time, _elapsed_transaction_time, extra_claims, _jwt) try: _state = transaction_session["state"] except KeyError: _state = None authzresp = AuthorizationResponse(state=_state, id_token=_jwt) if "redirect_uri" in transaction_session: _ruri = transaction_session["redirect_uri"] else: _error_msg = _("We could not complete your validation because an error occurred while " "handling your request. Please return to the service which initiated the " "validation request and try again.") try: cinfo = self.OP.cdb[transaction_session["client_id"]] _ruri = cinfo["redirect_uris"][0] except KeyError as e: abort_with_enduser_error(transaction_id, transaction_session["client_id"], cherrypy.request, logger, _error_msg, "Unknown RP client id '{}': '{}'.".format(transaction_session["client_id"], str(e))) location = authzresp.request(_ruri, True) logger.debug("Redirected to: '{}' ({})".format(location, type(location))) raise cherrypy.HTTPRedirect(location) def _verify_scope(self, scope, client_id): """Verifies the scope received from the RP. Only one affiliation request is allowed to be specified, and if 'persistent' is specified 'transient' is not allowed. In addition, the requested scope is verified against the clients permissions. :param scope: requested scope from the RP :return: True if the values in scope are valid, otherwise False. """ # Malformed scope requesting validation of more than one affiliation type requested_affiliations = [a for a in AFFILIATIONS if a in scope] if len(requested_affiliations) != 1: return False # Malformed scope containing both 'persistent' and 'transient' if PERSISTENT_NAMEID in scope and TRANSIENT_NAMEID in scope: return False # Verify the client is allowed to request this scope try: client_info = self.OP.cdb[client_id] allowed = client_info.get("allowed_scope_values", []) except KeyError as e: allowed = [] for value in scope: if value == "openid": # Always allow 'openid' in scope continue elif value in SCOPE_VALUES and value not in allowed: # A scope we understand, but client not allowed log_internal(logger, "Scope value '{}' not in '{}' for client.".format(value, allowed), None, client_id=client_id) return False return True def verify_authn_request(self, query_string): """Verify the incoming authentication request from the RP. :param query_string: query string in the request :param key_bundle: keys for encrypting the transaction state :return: tuple containing the encoded state and the scope requested by the RP. """ try: areq = self.OP.server.parse_authorization_request(query=query_string) except Exception as e: abort_with_enduser_error("-", "-", cherrypy.request, logger, _("The authentication request could not be processed. Please return to the " "service which initiated the validation request and try again."), "The authentication request '{}' could not be processed.".format(query_string), exc_info=True) # Verify it's a client_id I recognize client_id = areq["client_id"] _error_msg = _("Configuration error for the service.") try: client_info = self.OP.cdb[client_id] except KeyError as e: abort_with_enduser_error("-", client_id, cherrypy.request, logger, _error_msg, "Unknown RP client id '{}': '{}'.".format(client_id, str(e))) # verify that the redirect_uri is sound if "redirect_uri" not in areq: abort_with_enduser_error("-", client_id, cherrypy.request, logger, _error_msg, "Missing redirect URI in authentication request.") elif areq["redirect_uri"] not in client_info["redirect_uris"]: abort_with_enduser_error("-", client_id, cherrypy.request, logger, _error_msg, "Unknown redirect URI in authentication request: '{}' not in '{}'".format( areq["redirect_uri"], client_info["redirect_uris"])) # Create the state variable transaction_session = { "client_id": client_id, "redirect_uri": areq["redirect_uri"], "nonce": areq["nonce"], "scope": areq["scope"], } if "state" in areq: transaction_session["state"] = areq["state"] if "claims" in areq: transaction_session["claims"] = areq["claims"]["id_token"].to_dict() # Verify that the response_type if present is id_token try: assert areq["response_type"] == ["id_token"] except (KeyError, AssertionError) as err: # has to be there and match abort_with_client_error("-", transaction_session, cherrypy.request, logger, "Unsupported response_type '{}'".format(areq["response_type"]), error="unsupported_response_type", error_description="Only response_type 'id_token' is supported.") if not self._verify_scope(areq["scope"], client_id): abort_with_client_error("-", transaction_session, cherrypy.request, logger, "Invalid scope '{}'".format(areq["scope"]), error="invalid_scope", error_description="The specified scope '{}' is not valid.".format(" ".join(areq["scope"]))) transaction_session.update({ "start_time": get_timestamp() }) return transaction_session def get_claims_to_release(self, user_id, affiliation, identity, auth_time, idp_entity_id, idp_metadata_func, transaction_session): """ Compile a dictionary of a all claims we will release to the client. :param user_id: identifier for the user :param identity: assertions about the user from the IdP :param auth_time: time of authentication reported from the IdP :param idp_entity_id: id of the IdP :param idp_metadata_func: callable to fetch idp metadata :param transaction_session: transaction data :return: """ attributes = [N_("Affiliation"), N_("Identifier"), N_("Authentication time")] values = [affiliation, self._generate_subject_id(transaction_session["client_id"], user_id, idp_entity_id), auth_time] l = zip(attributes, values) extra_claims = self._get_extra_claims(idp_metadata_func, identity, idp_entity_id, transaction_session.get("claims", []), transaction_session["client_id"]) l.extend(extra_claims) return dict(l) def _get_extra_claims(self, idp_metadata_func, identity, idp_entity_id, requested_claims, client_id): """Create the extra claims requested by the RP. Extra claims will only be returned if the RP is allowed to request them and we got them from the IdP. :param idp_metadata_func: callable to fetch idp metadata :param identity: assertions from the IdP about the user :param idp_entity_id: entity id of the IdP :param requested_claims: the requested claims from the RP :param client_id: RP client id :return: a list of tuples with any extra claims to return to the RP with the id token. """ # Verify the client is allowed to request these claims try: client_info = self.OP.cdb[client_id] allowed = client_info.get("allowed_claims", []) except KeyError as e: allowed = [] for value in requested_claims: if value not in allowed: log_internal(logger, "Claim '{}' not in '{}' for client.".format(value, allowed), None, client_id=client_id) claims = [] if DOMAIN in requested_claims and DOMAIN in allowed: if "schacHomeOrganization" in identity: claims.append((N_("Domain"), identity["schacHomeOrganization"][0])) if COUNTRY in requested_claims and COUNTRY in allowed: country = self._get_idp_country(idp_metadata_func, idp_entity_id) if country is not None: claims.append((N_("Country"), country)) return claims def _generate_subject_id(self, client_id, user_id, idp_entity_id): """Construct the subject identifier for the ID Token. :param client_id: id of the client (RP) :param user_id: id of the end user :param idp_entity_id: id of the IdP """ return hashlib.sha512(client_id + user_id + idp_entity_id).hexdigest() def _get_idp_country(self, metadata, entity_id): """Get the country of the IdP. :param metadata: function fetching the IdP metadata :param entity_id: entity id of the IdP """ idp_info = metadata[entity_id] return idp_info.get("country", None)