From 56570dd90bed1d0cb0fd1cf350c1fa1d7b59355b Mon Sep 17 00:00:00 2001 From: James Stone Date: Wed, 16 Jun 2021 14:41:05 -0700 Subject: [PATCH] Do not try to auto refresh a token on a revoked user (#4747) * do not try to auto refresh a token on a revoked user * more robust testing for disable and revoke user * refactoring some tests to clean up duplicated code --- CHANGELOG.md | 2 +- src/realm/object-store/sync/sync_session.cpp | 32 +- test/object-store/sync/app.cpp | 437 +++++++++++-------- test/object-store/util/baas_admin_api.cpp | 53 +++ test/object-store/util/baas_admin_api.hpp | 4 + 5 files changed, 338 insertions(+), 190 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ed1095a14d..bc2111fefdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Fixed * ([#????](https://github.com/realm/realm-core/issues/????), since v?.?.?) -* None. +* Fixed a recursive loop which would eventually crash trying to refresh a user app token when it had been revoked by an admin. Now this situation logs the user out and reports an error. ([#4745](https://github.com/realm/realm-core/issues/4745), since v10.0.0). ### Breaking changes * None. diff --git a/src/realm/object-store/sync/sync_session.cpp b/src/realm/object-store/sync/sync_session.cpp index 568686cf20a..8a8a4c9257c 100644 --- a/src/realm/object-store/sync/sync_session.cpp +++ b/src/realm/object-store/sync/sync_session.cpp @@ -381,10 +381,27 @@ std::function)> SyncSession::handle_refresh(s } } else if (error) { - // 10 seconds is arbitrary, but it is to not swamp the server - std::this_thread::sleep_for(milliseconds(10000)); - if (session_user) { - session_user->refresh_custom_data(handle_refresh(session)); + if (error->http_status_code && (*error->http_status_code == 401 || *error->http_status_code == 403)) { + // A 401 response on a refresh request means that the token cannot be refreshed and we should not + // retry. This can be because an admin has revoked this user's sessions or the user has been disabled. + // TODO: ideally this would write to the logs as well in case users didn't set up their error handler. + std::unique_lock lock(session->m_state_mutex); + session->cancel_pending_waits(lock, error->error_code); + if (session_user && session_user->is_logged_in()) { + session_user->log_out(); + } + if (session->m_config.error_handler) { + auto user_facing_error = SyncError(realm::sync::ProtocolError::permission_denied, + "Unable to refresh the user access token.", true); + session->m_config.error_handler(session, user_facing_error); + } + } + else { + // 10 seconds is arbitrary, but it is to not swamp the server + std::this_thread::sleep_for(milliseconds(10000)); + if (session_user) { + session_user->refresh_custom_data(handle_refresh(session)); + } } } else { @@ -592,9 +609,10 @@ void SyncSession::handle_error(SyncError error) } } else { - // The server replies with '401: unauthorized' iff the access token is invalid or expired. - // If the access token is valid but was not authorized by the server a '403: forbidden' is sent. - // This means that if we did get the 401, the next step is always to request a new access token. + // The server replies with '401: unauthorized' if the access token is invalid, expired, revoked, or the user + // is disabled. In this scenario we attempt an automatic token refresh and if that succeeds continue as + // normal. If the refresh request also fails with 401 then we need to stop retrying and pass along the error; + // see handle_refresh(). if (error_code == util::websocket::make_error_code(util::websocket::Error::bad_response_401_unauthorized)) { if (auto u = user()) { u->refresh_custom_data(handle_refresh(shared_from_this())); diff --git a/test/object-store/sync/app.cpp b/test/object-store/sync/app.cpp index dfd5691e741..0efbbe7c2d5 100644 --- a/test/object-store/sync/app.cpp +++ b/test/object-store/sync/app.cpp @@ -93,6 +93,45 @@ std::string get_base_url() } #endif +struct AutoVerifiedEmailCredentials { + AutoVerifiedEmailCredentials() + { + // emails with this prefix will pass through the baas app due to the register function + email = util::format("realm_tests_do_autoverify%1@%2.com", random_string(10), random_string(10)); + password = random_string(10); + } + std::string email; + std::string password; +}; + +void timed_wait_for(std::function condition, + std::chrono::milliseconds max_ms = std::chrono::milliseconds(2000)) +{ + const auto wait_start = std::chrono::steady_clock::now(); + util::EventLoop::main().run_until([&] { + if (std::chrono::steady_clock::now() - wait_start > max_ms) { + throw std::runtime_error(util::format("timed_wait_for exceeded %1 ms", max_ms.count())); + } + return condition(); + }); +} + +AutoVerifiedEmailCredentials create_user_and_login(SharedApp app) +{ + REQUIRE(app); + AutoVerifiedEmailCredentials creds; + app->provider_client().register_email(creds.email, creds.password, + [&](Optional error) { + CHECK(!error); + }); + app->log_in_with_credentials(realm::app::AppCredentials::username_password(creds.email, creds.password), + [&](std::shared_ptr user, Optional error) { + REQUIRE(user); + CHECK(!error); + }); + return creds; +} + } // namespace // MARK: - Login with Credentials Tests @@ -163,9 +202,9 @@ TEST_CASE("app: login_with_credentials integration", "[sync][app]") { // MARK: - UsernamePasswordProviderClient Tests TEST_CASE("app: UsernamePasswordProviderClient integration", "[sync][app]") { - auto email = util::format("realm_tests_do_autoverify%1@%2.com", random_string(10), random_string(10)); - - auto password = random_string(10); + AutoVerifiedEmailCredentials creds; + auto email = creds.email; + auto password = creds.password; std::unique_ptr (*factory)() = [] { return std::unique_ptr(new IntTestTransport); @@ -405,35 +444,11 @@ TEST_CASE("app: UserAPIKeyProviderClient integration", "[sync][app]") { auto app = sync_manager.app(); bool processed = false; - - auto register_and_log_in_user = [&]() -> std::shared_ptr { - auto email = util::format("realm_tests_do_autoverify%1@%2.com", random_string(10), random_string(10)); - auto password = util::format("%1", random_string(15)); - app->provider_client().register_email( - email, password, [&](Optional error) { - CHECK(!error); // first registration should succeed - if (error) { - std::cout << "register failed for email: " << email << " pw: " << password - << " message: " << error->error_code.message() << "+" << error->message << std::endl; - } - }); - std::shared_ptr logged_in_user; - app->log_in_with_credentials(realm::app::AppCredentials::username_password(email, password), - [&](std::shared_ptr user, Optional error) { - REQUIRE(user); - CHECK(!error); - logged_in_user = user; - processed = true; - }); - CHECK(processed); - processed = false; - return logged_in_user; - }; - App::UserAPIKey api_key; SECTION("api-key") { - std::shared_ptr logged_in_user = register_and_log_in_user(); + create_user_and_login(app); + std::shared_ptr logged_in_user = app->current_user(); auto api_key_name = util::format("%1", random_string(15)); app->provider_client().create_api_key( api_key_name, logged_in_user, [&](App::UserAPIKey user_api_key, Optional error) { @@ -576,8 +591,11 @@ TEST_CASE("app: UserAPIKeyProviderClient integration", "[sync][app]") { } SECTION("api-key against the wrong user") { - std::shared_ptr first_user = register_and_log_in_user(); - std::shared_ptr second_user = register_and_log_in_user(); + create_user_and_login(app); + std::shared_ptr first_user = app->current_user(); + create_user_and_login(app); + std::shared_ptr second_user = app->current_user(); + REQUIRE(first_user != second_user); auto api_key_name = util::format("%1", random_string(15)); App::UserAPIKey api_key; App::UserAPIKeyProviderClient provider = app->provider_client(); @@ -752,11 +770,7 @@ TEST_CASE("app: auth providers function integration", "[sync][app]") { TEST_CASE("app: link_user integration", "[sync][app]") { SECTION("link_user intergration") { - - auto email = util::format("realm_tests_do_autoverify%1@%2.com", random_string(10), random_string(10)); - - auto password = random_string(10); - + AutoVerifiedEmailCredentials creds; std::unique_ptr (*factory)() = [] { return std::unique_ptr(new IntTestTransport); }; @@ -780,13 +794,13 @@ TEST_CASE("app: link_user integration", "[sync][app]") { std::shared_ptr sync_user; - auto email_pass_credentials = realm::app::AppCredentials::username_password(email, password); + auto email_pass_credentials = realm::app::AppCredentials::username_password(creds.email, creds.password); app->provider_client().register_email( - email, password, [&](Optional error) { + creds.email, creds.password, [&](Optional error) { CHECK(!error); // first registration success if (error) { - std::cout << "register failed for email: " << email << " pw: " << password + std::cout << "register failed for email: " << creds.email << " pw: " << creds.password << " message: " << error->error_code.message() << "+" << error->message << std::endl; } }); @@ -835,19 +849,7 @@ TEST_CASE("app: call function", "[sync][app]") { TestSyncManager tsm(TestSyncManager::Config(config), {}); auto app = tsm.app(); - auto email = util::format("realm_tests_do_autoverify%1@%2.com", random_string(10), random_string(10)); - auto password = random_string(10); - - app->provider_client().register_email(email, password, - [&](Optional error) { - CHECK(!error); - }); - - app->log_in_with_credentials(realm::app::AppCredentials::username_password(email, password), - [&](std::shared_ptr user, Optional error) { - REQUIRE(user); - CHECK(!error); - }); + create_user_and_login(app); bson::BsonArray toSum(5); std::iota(toSum.begin(), toSum.end(), static_cast(1)); @@ -882,18 +884,7 @@ TEST_CASE("app: remote mongo client", "[sync][app]") { TestSyncManager sync_manager(TestSyncManager::Config(config), {}); auto app = sync_manager.app(); - auto email = util::format("realm_tests_do_autoverify%1@%2.com", random_string(10), random_string(10)); - auto password = random_string(10); - app->provider_client().register_email(email, password, - [&](Optional error) { - CHECK(!error); - }); - - app->log_in_with_credentials(realm::app::AppCredentials::username_password(email, password), - [&](std::shared_ptr user, Optional error) { - REQUIRE(user); - CHECK(!error); - }); + create_user_and_login(app); auto remote_client = app->current_user()->mongo_client("BackingDB"); auto db = remote_client.db(app_session.config.mongo_dbname); @@ -1554,22 +1545,8 @@ TEST_CASE("app: push notifications", "[sync][app]") { TestSyncManager sync_manager(TestSyncManager::Config(config), {}); auto app = sync_manager.app(); - auto email = util::format("realm_tests_do_autoverify%1@%2.com", random_string(10), random_string(10)); - auto password = random_string(10); - - app->provider_client().register_email(email, password, - [&](Optional error) { - CHECK(!error); - }); - - std::shared_ptr sync_user; - - app->log_in_with_credentials(realm::app::AppCredentials::username_password(email, password), - [&](std::shared_ptr user, Optional error) { - REQUIRE(user); - CHECK(!error); - sync_user = user; - }); + create_user_and_login(app); + std::shared_ptr sync_user = app->current_user(); SECTION("register") { bool processed; @@ -1670,23 +1647,9 @@ TEST_CASE("app: token refresh", "[sync][app][token]") { TestSyncManager sync_manager(TestSyncManager::Config(config), {}); auto app = sync_manager.app(); - auto email = util::format("realm_tests_do_autoverify%1@%2.com", random_string(10), random_string(10)); - auto password = random_string(10); - - app->provider_client().register_email(email, password, - [&](Optional error) { - CHECK(!error); - }); - - std::shared_ptr sync_user; - - app->log_in_with_credentials(realm::app::AppCredentials::username_password(email, password), - [&](std::shared_ptr user, Optional error) { - REQUIRE(user); - CHECK(!error); - sync_user = user; - sync_user->update_access_token(ENCODE_FAKE_JWT("fake_access_token")); - }); + create_user_and_login(app); + std::shared_ptr sync_user = app->current_user(); + sync_user->update_access_token(ENCODE_FAKE_JWT("fake_access_token")); auto remote_client = app->current_user()->mongo_client("BackingDB"); auto db = remote_client.db(app_session.config.mongo_dbname); @@ -1857,7 +1820,8 @@ TEST_CASE("app: sync integration", "[sync][app]") { std::string base_url = get_base_url(); const std::string valid_pk_name = "_id"; REQUIRE(!base_url.empty()); - auto app_session = get_runtime_app_session(base_url); + auto app_session = create_app(default_app_config(base_url)); + auto app_config = App::Config{app_session.client_app_id, factory, base_url, @@ -1871,26 +1835,7 @@ TEST_CASE("app: sync integration", "[sync][app]") { auto base_path = util::make_temp_dir() + app_config.app_id; util::try_remove_dir_recursive(base_path); util::try_make_dir(base_path); - // Heap allocate to control lifecycle. - // This is required so that we can reset the sync manager - // through deallocation without worrying about it being popped - // off the stack at the end of test case. - - auto get_app_and_login = [&](SharedApp app) -> std::shared_ptr { - auto email = util::format("realm_tests_do_autoverify%1@%2.com", random_string(10), random_string(10)); - auto password = random_string(10); - app->provider_client().register_email( - email, password, [&](Optional error) { - CHECK(!error); - }); - app->log_in_with_credentials(realm::app::AppCredentials::username_password(email, password), - [&](std::shared_ptr user, Optional error) { - REQUIRE(user); - CHECK(!error); - }); - return app; - }; auto setup_and_get_config = [&base_path, &valid_pk_name](std::shared_ptr app) -> realm::Realm::Config { realm::Realm::Config config; config.sync_config = std::make_shared(app->current_user(), bson::Bson("foo")); @@ -1921,26 +1866,39 @@ TEST_CASE("app: sync integration", "[sync][app]") { REQUIRE(err == std::error_code{}); called.store(true); }); - util::EventLoop::main().run_until([&] { + REQUIRE_NOTHROW(timed_wait_for([&] { return called.load(); - }); + })); REQUIRE(called); called.store(false); session->wait_for_download_completion([&](std::error_code err) { REQUIRE(err == std::error_code{}); called.store(true); }); - util::EventLoop::main().run_until([&] { + REQUIRE_NOTHROW(timed_wait_for([&] { return called.load(); - }); + })); return realm::Results(r, r->read_group().get_table("class_Dog")); }; + auto create_one_dog = [&](realm::SharedRealm r) { + r->begin_transaction(); + CppContext c; + Object::create(c, r, "Dog", + util::Any(realm::AnyDict{{valid_pk_name, util::Any(ObjectId::gen())}, + {"breed", std::string("bulldog")}, + {"name", std::string("fido")}, + {"realm_id", std::string("foo")}}), + CreatePolicy::ForceCreate); + r->commit_transaction(); + }; + // MARK: Add Objects - SECTION("Add Objects") { { TestSyncManager sync_manager(TestSyncManager::Config(app_config), {}); - auto app = get_app_and_login(sync_manager.app()); + auto app = sync_manager.app(); + create_user_and_login(sync_manager.app()); auto config = setup_and_get_config(app); auto r = realm::Realm::get_shared_realm(config); auto session = app->current_user()->session_for_on_disk_path(r->config().path); @@ -1954,16 +1912,7 @@ TEST_CASE("app: sync integration", "[sync][app]") { } REQUIRE(get_dogs(r, session).size() == 0); - r->begin_transaction(); - CppContext c; - Object::create(c, r, "Dog", - util::Any(realm::AnyDict{{valid_pk_name, util::Any(ObjectId::gen())}, - {"breed", std::string("bulldog")}, - {"name", std::string("fido")}, - {"realm_id", std::string("foo")}}), - CreatePolicy::ForceCreate); - r->commit_transaction(); - + create_one_dog(r); REQUIRE(get_dogs(r, session).size() == 1); } @@ -1972,10 +1921,11 @@ TEST_CASE("app: sync integration", "[sync][app]") { util::try_make_dir(base_path); { TestSyncManager reinit(TestSyncManager::Config(app_config), {}); - auto app = get_app_and_login(reinit.app()); - auto config = setup_and_get_config(app); + create_user_and_login(reinit.app()); + + auto config = setup_and_get_config(reinit.app()); auto r = realm::Realm::get_shared_realm(config); - auto session = app->current_user()->session_for_on_disk_path(r->config().path); + auto session = reinit.app()->current_user()->session_for_on_disk_path(r->config().path); Results dogs = get_dogs(r, session); REQUIRE(dogs.size() == 1); REQUIRE(dogs.get(0).get("breed") == "bulldog"); @@ -1988,7 +1938,8 @@ TEST_CASE("app: sync integration", "[sync][app]") { SECTION("Invalid Access Token is Refreshed") { { TestSyncManager sync_manager(TestSyncManager::Config(app_config), {}); - auto app = get_app_and_login(sync_manager.app()); + auto app = sync_manager.app(); + create_user_and_login(sync_manager.app()); auto config = setup_and_get_config(app); auto r = realm::Realm::get_shared_realm(config); auto session = app->current_user()->session_for_on_disk_path(r->config().path); @@ -2002,29 +1953,21 @@ TEST_CASE("app: sync integration", "[sync][app]") { } REQUIRE(get_dogs(r, session).size() == 0); - r->begin_transaction(); - CppContext c; - Object::create(c, r, "Dog", - util::Any(realm::AnyDict{{valid_pk_name, util::Any(ObjectId::gen())}, - {"breed", std::string("bulldog")}, - {"name", std::string("fido")}, - {"realm_id", std::string("foo")}}), - CreatePolicy::ForceCreate); - r->commit_transaction(); - + create_one_dog(r); REQUIRE(get_dogs(r, session).size() == 1); } - util::try_remove_dir_recursive(base_path); - util::try_make_dir(base_path); + REQUIRE(util::try_remove_dir_recursive(base_path)); + REQUIRE(util::try_make_dir(base_path)); { TestSyncManager reinit(TestSyncManager::Config(app_config), {}); - auto app = get_app_and_login(reinit.app()); + create_user_and_login(reinit.app()); + auto user = reinit.app()->current_user(); // set a bad access token. this will trigger a refresh when the sync session opens - app->current_user()->update_access_token(encode_fake_jwt("fake_access_token")); + user->update_access_token(encode_fake_jwt("fake_access_token")); - auto config = setup_and_get_config(app); + auto config = setup_and_get_config(reinit.app()); auto r = realm::Realm::get_shared_realm(config); - auto session = app->current_user()->session_for_on_disk_path(r->config().path); + auto session = user->session_for_on_disk_path(r->config().path); Results dogs = get_dogs(r, session); REQUIRE(dogs.size() == 1); REQUIRE(dogs.get(0).get("breed") == "bulldog"); @@ -2037,7 +1980,8 @@ TEST_CASE("app: sync integration", "[sync][app]") { realm::sync::AccessToken token; { TestSyncManager sync_manager(TestSyncManager::Config(app_config), {}); - auto app = get_app_and_login(sync_manager.app()); + auto app = sync_manager.app(); + auto creds = create_user_and_login(sync_manager.app()); auto config = setup_and_get_config(app); auto r = realm::Realm::get_shared_realm(config); auto session = app->current_user()->session_for_on_disk_path(r->config().path); @@ -2051,15 +1995,7 @@ TEST_CASE("app: sync integration", "[sync][app]") { } REQUIRE(get_dogs(r, session).size() == 0); - r->begin_transaction(); - CppContext c; - Object::create(c, r, "Dog", - util::Any(realm::AnyDict{{valid_pk_name, util::Any(ObjectId::gen())}, - {"breed", std::string("bulldog")}, - {"name", std::string("fido")}, - {"realm_id", std::string("foo")}}), - CreatePolicy::ForceCreate); - r->commit_transaction(); + create_one_dog(r); REQUIRE(get_dogs(r, session).size() == 1); realm::sync::AccessToken::ParseError error_state = realm::sync::AccessToken::ParseError::none; @@ -2078,7 +2014,6 @@ TEST_CASE("app: sync integration", "[sync][app]") { util::try_make_dir(base_path); { std::function hook; - std::vector session_states_seen; std::function()> hooked_factory = [&hook] { if (hook) { hook(); @@ -2088,7 +2023,8 @@ TEST_CASE("app: sync integration", "[sync][app]") { app_config.transport_generator = hooked_factory; TestSyncManager reinit(TestSyncManager::Config(app_config), {}); - auto app = get_app_and_login(reinit.app()); + auto app = reinit.app(); + create_user_and_login(app); REQUIRE(!app->current_user()->access_token_refresh_required()); // Set a bad access token, with an expired time. This will trigger a refresh initiated by the client. app->current_user()->update_access_token( @@ -2118,9 +2054,146 @@ TEST_CASE("app: sync integration", "[sync][app]") { } } + SECTION("Invalid refresh token") { + auto verify_error_on_sync_with_invalid_refresh_token = [&](std::shared_ptr user, + realm::Realm::Config config) { + REQUIRE(user); + REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id)); + + // requesting a new access token fails because the refresh token used for this request is revoked + user->refresh_custom_data([&](util::Optional error) { + REQUIRE(error); + REQUIRE(error->http_status_code == 401); + REQUIRE(error->error_code == + realm::app::make_error_code(realm::app::ServiceErrorCode::invalid_session)); + }); + + // Set a bad access token. This will force a request for a new access token when the sync session opens + // this is only necessary because the server doesn't actually revoke previously issued access tokens + // instead allowing their session to time out as normal. So this simulates the access token expiring. + // see: + // https://github.com/10gen/baas/blob/05837cc3753218dfaf89229c6930277ef1616402/api/common/auth.go#L1380-L1386 + user->update_access_token(encode_fake_jwt("fake_access_token")); + REQUIRE(!app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id)); + + std::atomic sync_error_handler_called{false}; + config.sync_config->error_handler = [&](std::shared_ptr, SyncError error) { + sync_error_handler_called.store(true); + REQUIRE(error.error_code == + realm::sync::make_error_code(realm::sync::ProtocolError::permission_denied)); + REQUIRE(error.message == "Unable to refresh the user access token."); + }; + + auto r = realm::Realm::get_shared_realm(config); + auto session = user->session_for_on_disk_path(r->config().path); + REQUIRE(user->is_logged_in()); + REQUIRE(!sync_error_handler_called.load()); + { + std::atomic called{false}; + session->wait_for_upload_completion([&](std::error_code err) { + called.store(true); + REQUIRE(err == realm::app::make_error_code(realm::app::ServiceErrorCode::invalid_session)); + }); + REQUIRE_NOTHROW(timed_wait_for([&] { + return called.load(); + })); + REQUIRE(called); + } + REQUIRE_NOTHROW(timed_wait_for([&] { + return sync_error_handler_called.load(); + })); + + // the failed refresh logs out the user + REQUIRE(!user->is_logged_in()); + }; + + SECTION("Disabled user results in a sync error") { + TestSyncManager sync_manager(TestSyncManager::Config(app_config), {}); + auto app = sync_manager.app(); + auto creds = create_user_and_login(sync_manager.app()); + auto config = setup_and_get_config(app); + auto user = app->current_user(); + REQUIRE(user); + REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id)); + app_session.admin_api.disable_user_sessions(app->current_user()->identity(), app_session.server_app_id); + + verify_error_on_sync_with_invalid_refresh_token(user, config); + + // logging in again doesn't fix things while the account is disabled + app->log_in_with_credentials( + realm::app::AppCredentials::username_password(creds.email, creds.password), + [&](std::shared_ptr user, Optional error) { + REQUIRE(!user); + REQUIRE(error); + REQUIRE(error->error_code == + realm::app::make_error_code(realm::app::ServiceErrorCode::user_disabled)); + }); + + // admin enables user sessions again which should allow the session to continue + app_session.admin_api.enable_user_sessions(user->identity(), app_session.server_app_id); + + // logging in now works properly + app->log_in_with_credentials(realm::app::AppCredentials::username_password(creds.email, creds.password), + [&](std::shared_ptr user, Optional error) { + REQUIRE(user); + REQUIRE(!error); + }); + // still referencing the same user + REQUIRE(user == app->current_user()); + REQUIRE(user->is_logged_in()); + + { + // check that there are no errors initiating a session now by making sure upload/download succeeds + auto r = realm::Realm::get_shared_realm(config); + auto session = user->session_for_on_disk_path(r->config().path); + Results dogs = get_dogs(r, session); + } + } + + SECTION("Revoked refresh token results in a sync error") { + TestSyncManager sync_manager(TestSyncManager::Config(app_config), {}); + auto app = sync_manager.app(); + auto creds = create_user_and_login(sync_manager.app()); + auto config = setup_and_get_config(app); + auto user = app->current_user(); + REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id)); + app_session.admin_api.revoke_user_sessions(user->identity(), app_session.server_app_id); + // revoking a user session only affects the refresh token, so the access token should still continue to + // work. + REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id)); + + verify_error_on_sync_with_invalid_refresh_token(user, config); + + // logging in again succeeds and generates a new and valid refresh token + app->log_in_with_credentials(realm::app::AppCredentials::username_password(creds.email, creds.password), + [&](std::shared_ptr user, Optional error) { + REQUIRE(!error); + REQUIRE(user); + }); + + // still referencing the same user and now the user is logged in + REQUIRE(user == app->current_user()); + REQUIRE(user->is_logged_in()); + + // new requests for an access token succeed again + user->refresh_custom_data([&](util::Optional error) { + REQUIRE(!error); + }); + + { + // check that there are no errors initiating a new sync session by making sure upload/download + // succeeds + auto r = realm::Realm::get_shared_realm(config); + auto session = user->session_for_on_disk_path(r->config().path); + Results dogs = get_dogs(r, session); + } + } + } + SECTION("invalid partition error handling") { TestSyncManager sync_manager(TestSyncManager::Config(app_config), {}); - auto app = get_app_and_login(sync_manager.app()); + auto app = sync_manager.app(); + auto creds = create_user_and_login(sync_manager.app()); auto config = setup_and_get_config(app); config.sync_config->partition_value = "not a bson serialized string"; std::atomic error_did_occur = false; @@ -2132,16 +2205,17 @@ TEST_CASE("app: sync integration", "[sync][app]") { }; auto r = realm::Realm::get_shared_realm(config); auto session = app->current_user()->session_for_on_disk_path(r->config().path); - util::EventLoop::main().run_until([&] { + REQUIRE_NOTHROW(timed_wait_for([&] { return error_did_occur.load(); - }); + })); REQUIRE(error_did_occur.load()); } SECTION("invalid pk schema error handling") { const std::string invalid_pk_name = "my_primary_key"; TestSyncManager sync_manager(TestSyncManager::Config(app_config), {}); - auto app = get_app_and_login(sync_manager.app()); + auto app = sync_manager.app(); + auto creds = create_user_and_login(sync_manager.app()); auto config = setup_and_get_config(app); auto it = config.schema->find("Dog"); REQUIRE(it != config.schema->end()); @@ -2158,7 +2232,8 @@ TEST_CASE("app: sync integration", "[sync][app]") { SECTION("missing pk schema error handling") { TestSyncManager sync_manager(TestSyncManager::Config(app_config), {}); - auto app = get_app_and_login(sync_manager.app()); + auto app = sync_manager.app(); + auto creds = create_user_and_login(sync_manager.app()); auto config = setup_and_get_config(app); auto it = config.schema->find("Dog"); REQUIRE(it != config.schema->end()); @@ -2174,12 +2249,12 @@ TEST_CASE("app: sync integration", "[sync][app]") { SECTION("too large sync message error handling") { TestSyncManager::Config test_config(app_config); - // Too much log output seems to create problems on Evergreen CI test_config.verbose_sync_client_logging = false; - TestSyncManager sync_manager(test_config, {}); - auto app = get_app_and_login(sync_manager.app()); + TestSyncManager sync_manager(TestSyncManager::Config(app_config), {}); + auto app = sync_manager.app(); + auto creds = create_user_and_login(sync_manager.app()); auto config = setup_and_get_config(app); std::mutex sync_error_mutex; @@ -2205,19 +2280,17 @@ TEST_CASE("app: sync integration", "[sync][app]") { } r->commit_transaction(); - const auto wait_start = std::chrono::steady_clock::now(); auto pred = [](const SyncError& error) { return error.error_code.category() == util::websocket::websocket_close_status_category(); }; - util::EventLoop::main().run_until([&]() -> bool { - std::lock_guard lk(sync_error_mutex); - // If we haven't gotten an error in more than 2 minutes, then something has gone wrong - // and we should fail the test. - if (std::chrono::steady_clock::now() - wait_start > std::chrono::minutes(2)) { - return false; - } - return std::any_of(sync_errors.begin(), sync_errors.end(), pred); - }); + // If we haven't gotten an error in more than 2 minutes, then something has gone wrong + // and we should fail the test. + REQUIRE_NOTHROW(timed_wait_for( + [&] { + std::lock_guard lk(sync_error_mutex); + return std::any_of(sync_errors.begin(), sync_errors.end(), pred); + }, + std::chrono::minutes(2))); auto captured_error = [&] { std::lock_guard lk(sync_error_mutex); diff --git a/test/object-store/util/baas_admin_api.cpp b/test/object-store/util/baas_admin_api.cpp index 9cd905aae7e..0f84fc102b8 100644 --- a/test/object-store/util/baas_admin_api.cpp +++ b/test/object-store/util/baas_admin_api.cpp @@ -436,6 +436,59 @@ AdminAPISession AdminAPISession::login(const std::string& base_url, const std::s return AdminAPISession(std::move(base_url), std::move(access_token), std::move(group_id)); } +void AdminAPISession::revoke_user_sessions(const std::string& user_id, const std::string app_id) +{ + auto endpoint = AdminAPIEndpoint( + util::format("%1/api/admin/v3.0/groups/%2/apps/%3/users/%4/logout", m_base_url, m_group_id, app_id, user_id), + m_access_token); + auto response = endpoint.put(""); + REALM_ASSERT(response.http_status_code == 204); +} + +void AdminAPISession::disable_user_sessions(const std::string& user_id, const std::string app_id) +{ + auto endpoint = AdminAPIEndpoint( + util::format("%1/api/admin/v3.0/groups/%2/apps/%3/users/%4/disable", m_base_url, m_group_id, app_id, user_id), + m_access_token); + auto response = endpoint.put(""); + REALM_ASSERT(response.http_status_code == 204); +} + +void AdminAPISession::enable_user_sessions(const std::string& user_id, const std::string app_id) +{ + auto endpoint = AdminAPIEndpoint( + util::format("%1/api/admin/v3.0/groups/%2/apps/%3/users/%4/enable", m_base_url, m_group_id, app_id, user_id), + m_access_token); + auto response = endpoint.put(""); + REALM_ASSERT(response.http_status_code == 204); +} + +// returns false for an invalid/expired access token +bool AdminAPISession::verify_access_token(const std::string& access_token, const std::string app_id) +{ + auto endpoint = AdminAPIEndpoint( + util::format("%1/api/admin/v3.0/groups/%2/apps/%3/users/verify_token", m_base_url, m_group_id, app_id), + m_access_token); + nlohmann::json request_body{ + {"token", access_token}, + }; + auto response = endpoint.post(request_body.dump()); + if (response.http_status_code == 200) { + auto resp_json = nlohmann::json::parse(response.body.empty() ? "{}" : response.body); + try { + // if these fields are found, then the token is valid according to the server. + // if it is invalid or expired then an error response is sent. + int64_t issued_at = resp_json["iat"]; + int64_t expires_at = resp_json["exp"]; + return issued_at != 0 && expires_at != 0; + } + catch (...) { + return false; + } + } + return false; +} + AdminAPIEndpoint AdminAPISession::apps() const { return AdminAPIEndpoint(util::format("%1/api/admin/v3.0/groups/%2/apps", m_base_url, m_group_id), m_access_token); diff --git a/test/object-store/util/baas_admin_api.hpp b/test/object-store/util/baas_admin_api.hpp index dfb4f6f6399..7973b6052f7 100644 --- a/test/object-store/util/baas_admin_api.hpp +++ b/test/object-store/util/baas_admin_api.hpp @@ -63,6 +63,10 @@ class AdminAPISession { const std::string& password); AdminAPIEndpoint apps() const; + void revoke_user_sessions(const std::string& user_id, const std::string app_id); + void disable_user_sessions(const std::string& user_id, const std::string app_id); + void enable_user_sessions(const std::string& user_id, const std::string app_id); + bool verify_access_token(const std::string& access_token, const std::string app_id); private: AdminAPISession(std::string base_url, std::string access_token, std::string group_id)