From c96933d5e3b947c3a5d7c6d6361eb2d50a94400b Mon Sep 17 00:00:00 2001 From: Pierre Proulx Date: Wed, 30 Sep 2026 23:46:24 -0400 Subject: [PATCH 1/4] BCloud-14842: Add client attestation support: setAppCheckToken method. Also added missing authenticateEpicGames, attachEpicGamesIdentity, mergeEpicGamesIdentity and detachEpicGamesIdentity. --- include/braincloud/AuthenticationType.h | 1 + include/braincloud/BrainCloudAuthentication.h | 19 +++++++++++++++++++ include/braincloud/BrainCloudIdentity.h | 17 +++++++++++++++++ include/braincloud/OperationParam.h | 1 + src/AuthenticationType.cpp | 5 +++++ src/BrainCloudAuthentication.cpp | 15 +++++++++++++++ src/BrainCloudIdentity.cpp | 15 +++++++++++++++ src/OperationParam.cpp | 1 + 8 files changed, 74 insertions(+) diff --git a/include/braincloud/AuthenticationType.h b/include/braincloud/AuthenticationType.h index 49475c1f..2adbc588 100644 --- a/include/braincloud/AuthenticationType.h +++ b/include/braincloud/AuthenticationType.h @@ -25,6 +25,7 @@ namespace BrainCloud static const AuthenticationType & Oculus; static const AuthenticationType & GameCenter; static const AuthenticationType & Steam; + static const AuthenticationType & EpicGames; static const AuthenticationType & Apple; static const AuthenticationType & Google; static const AuthenticationType & GoogleOpenId; diff --git a/include/braincloud/BrainCloudAuthentication.h b/include/braincloud/BrainCloudAuthentication.h index 49e481e1..b1a72cab 100644 --- a/include/braincloud/BrainCloudAuthentication.h +++ b/include/braincloud/BrainCloudAuthentication.h @@ -29,6 +29,16 @@ namespace BrainCloud // internal use only void setClientLib(const char* lib); + /** + * Sets the caller-supplied Firebase App Check token for subsequent authenticate requests. + * The token is copied and retained in memory until replaced or cleared with an empty string. + * The host app is responsible for obtaining and refreshing it, including before reconnects. + * Call on the same thread as authentication; already queued requests are not updated. + * No token validation or Firebase integration is performed by this SDK. + * @param token Opaque App Check token, or an empty string to omit it from requests. + */ + void setAppCheckToken(const std::string& token); + /** * Initialize - initializes the identity service with a saved * anonymous installation id and most recently used profile id @@ -207,6 +217,13 @@ namespace BrainCloud */ void authenticateApple(const char * appleUserId, const char * identityToken, bool forceCreate, IServerCallback * callback = NULL); + /** Authenticate with an Epic account ID and authentication ID token. + * @param forceCreate Create a profile if this identity does not exist. + * @param callback Callback invoked with the server response. + */ + void authenticateEpicGames(const char * epicAccountId, const char * authIdToken, bool forceCreate, IServerCallback * callback = NULL); + + /** * Authenticate the user using a google userid(email address) and google authentication token. * @@ -462,6 +479,8 @@ namespace BrainCloud void authenticate(const char * externalId, const char * authenticationToken, AuthenticationType authenticationType, const char * externalAuthName, bool forceCreate, const std::string &extraJson, IServerCallback * callback); private: + std::string _appCheckToken; + struct PreviousAuthParams { std::string externalId; diff --git a/include/braincloud/BrainCloudIdentity.h b/include/braincloud/BrainCloudIdentity.h index 93b728b4..bf43fa56 100644 --- a/include/braincloud/BrainCloudIdentity.h +++ b/include/braincloud/BrainCloudIdentity.h @@ -294,6 +294,23 @@ namespace BrainCloud */ void detachAppleIdentity(const char * appleId, bool continueAnon, IServerCallback * callback = NULL); + /** Attach Epic Games credentials to the current profile (identity ATTACH). + * @param epicAccountId Epic account ID. + * @param authIdToken Epic authentication ID token. + * @param callback Callback invoked with the server response. + */ + void attachEpicGamesIdentity(const char * epicAccountId, const char * authIdToken, IServerCallback * callback = NULL); + + /** Merge the profile associated with Epic Games credentials (identity MERGE). */ + void mergeEpicGamesIdentity(const char * epicAccountId, const char * authIdToken, IServerCallback * callback = NULL); + + /** Detach Epic Games credentials (identity DETACH). + * @param epicAccountId Epic account ID. + * @param continueAnon Allow the profile to revert to anonymous. + * @param callback Callback invoked with the server response. + */ + void detachEpicGamesIdentity(const char * epicAccountId, bool continueAnon, IServerCallback * callback = NULL); + /** * Attach the user's Ultra credentials to the current profile. * diff --git a/include/braincloud/OperationParam.h b/include/braincloud/OperationParam.h index 6a7fece7..ae7aa46f 100644 --- a/include/braincloud/OperationParam.h +++ b/include/braincloud/OperationParam.h @@ -44,6 +44,7 @@ namespace BrainCloud { // Authenticate Service - Authenticate Params static const OperationParam AuthenticateServiceAuthenticateAuthenticationType; static const OperationParam AuthenticateServiceAuthenticateAuthenticationToken; + static const OperationParam AuthenticateServiceAuthenticateAppCheckToken; static const OperationParam AuthenticateServiceAuthenticateExternalId; static const OperationParam AuthenticateServiceAuthenticateUniversalId; static const OperationParam AuthenticateServiceAuthenticateEmailAddress; diff --git a/src/AuthenticationType.cpp b/src/AuthenticationType.cpp index c4d89771..e023ca51 100644 --- a/src/AuthenticationType.cpp +++ b/src/AuthenticationType.cpp @@ -11,6 +11,7 @@ namespace BrainCloud const AuthenticationType & AuthenticationType::Oculus = AuthenticationType("Oculus"); const AuthenticationType & AuthenticationType::GameCenter = AuthenticationType("GameCenter"); const AuthenticationType & AuthenticationType::Steam = AuthenticationType("Steam"); + const AuthenticationType & AuthenticationType::EpicGames = AuthenticationType("EpicGames"); const AuthenticationType & AuthenticationType::Apple = AuthenticationType("Apple"); const AuthenticationType & AuthenticationType::Google = AuthenticationType("Google"); const AuthenticationType & AuthenticationType::GoogleOpenId = AuthenticationType("GoogleOpenId"); @@ -63,6 +64,10 @@ namespace BrainCloud { return AuthenticationType::Steam; } + if (AuthenticationType == EpicGames) + { + return AuthenticationType::EpicGames; + } if (AuthenticationType == Apple) { return AuthenticationType::Apple; diff --git a/src/BrainCloudAuthentication.cpp b/src/BrainCloudAuthentication.cpp index 4a4c5908..c4975ce7 100644 --- a/src/BrainCloudAuthentication.cpp +++ b/src/BrainCloudAuthentication.cpp @@ -29,6 +29,11 @@ namespace BrainCloud { _clientLib = lib; } + void BrainCloudAuthentication::setAppCheckToken(const std::string& token) + { + _appCheckToken = token; + } + void BrainCloudAuthentication::initialize(const char * profileId, const char * anonymousId) { _profileId = profileId; @@ -143,6 +148,11 @@ namespace BrainCloud { authenticate(userid, sessionticket, AuthenticationType::Steam, NULL, forceCreate, "", callback); } + void BrainCloudAuthentication::authenticateEpicGames(const char * epicAccountId, const char * authIdToken, bool forceCreate, IServerCallback * callback) + { + authenticate(epicAccountId, authIdToken, AuthenticationType::EpicGames, NULL, forceCreate, "", callback); + } + void BrainCloudAuthentication::authenticateApple(const char * appleUserId, const char * identityToken, bool forceCreate, IServerCallback * callback) { authenticate(appleUserId, identityToken, AuthenticationType::Apple, NULL, forceCreate, "", callback); @@ -334,6 +344,11 @@ namespace BrainCloud { message[OperationParam::AuthenticateServiceAuthenticateExtraJson.getValue()] = JsonUtil::jsonStringToValue(extraJson); } + if (!_appCheckToken.empty()) + { + message[OperationParam::AuthenticateServiceAuthenticateAppCheckToken.getValue()] = _appCheckToken; + } + ServerCall * sc = new ServerCall(ServiceName::AuthenticateV2, ServiceOperation::Authenticate, message, callback); m_client->sendRequest(sc); } diff --git a/src/BrainCloudIdentity.cpp b/src/BrainCloudIdentity.cpp index 6635a597..41489ea3 100644 --- a/src/BrainCloudIdentity.cpp +++ b/src/BrainCloudIdentity.cpp @@ -120,6 +120,21 @@ namespace BrainCloud m_client->sendRequest(sc); } + void BrainCloudIdentity::attachEpicGamesIdentity(const char * epicAccountId, const char * authIdToken, IServerCallback * callback) + { + attachIdentity(epicAccountId, authIdToken, AuthenticationType::EpicGames, callback); + } + + void BrainCloudIdentity::mergeEpicGamesIdentity(const char * epicAccountId, const char * authIdToken, IServerCallback * callback) + { + mergeIdentity(epicAccountId, authIdToken, AuthenticationType::EpicGames, callback); + } + + void BrainCloudIdentity::detachEpicGamesIdentity(const char * epicAccountId, bool continueAnon, IServerCallback * callback) + { + detachIdentity(epicAccountId, AuthenticationType::EpicGames, continueAnon, callback); + } + void BrainCloudIdentity::attachAppleIdentity(const char * appleId, const char * authenticationToken, IServerCallback * callback) { attachIdentity(appleId, authenticationToken, AuthenticationType::Apple, callback); diff --git a/src/OperationParam.cpp b/src/OperationParam.cpp index 259f7234..092bc174 100644 --- a/src/OperationParam.cpp +++ b/src/OperationParam.cpp @@ -38,6 +38,7 @@ namespace BrainCloud // Authenticate Service - Authenticate Params const OperationParam OperationParam::AuthenticateServiceAuthenticateAuthenticationType = OperationParam("authenticationType"); const OperationParam OperationParam::AuthenticateServiceAuthenticateAuthenticationToken = OperationParam("authenticationToken"); + const OperationParam OperationParam::AuthenticateServiceAuthenticateAppCheckToken = OperationParam("appCheckToken"); const OperationParam OperationParam::AuthenticateServiceAuthenticateExternalId = OperationParam("externalId"); const OperationParam OperationParam::AuthenticateServiceAuthenticateUniversalId = OperationParam("universalId"); const OperationParam OperationParam::AuthenticateServiceAuthenticateEmailAddress = OperationParam("emailAddress"); From 1f60688642be9174b9d4094e7ca0c4b081e2f331 Mon Sep 17 00:00:00 2001 From: Pierre Proulx Date: Fri, 2 Oct 2026 23:27:11 -0400 Subject: [PATCH 2/4] Added an AppCheckTokenProvider to refresh the firebase token. --- README.md | 26 ++++++ include/braincloud/AuthenticationType.h | 2 +- include/braincloud/BrainCloudAuthentication.h | 25 ++++++ include/braincloud/reason_codes.h | 1 + src/BrainCloudAuthentication.cpp | 89 +++++++++++++++++++ src/BrainCloudClient.cpp | 3 + 6 files changed, 145 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c70429a..30fd806f 100644 --- a/README.md +++ b/README.md @@ -338,3 +338,29 @@ These are included as submodules so can be retrieved using However should not be updated or pulled to another version since the initialized version is known to build and run stable. JsonCpp has recently been made a submodule (as of brainCloud 4.15) so if pulling or changing branches the existing folder lib/jsoncpp-1.0.0/ should be deleted or there will be git errors. + +### Asynchronous App Check token provider + +Register `setAppCheckTokenProvider` on the authentication service to obtain a current +opaque token for every authentication, including retries and automatic reconnects. +The provider takes precedence over `setAppCheckToken`; removing it restores the +stored-token behavior. Neither SDK depends on Firebase. + +The provider is invoked on the authentication thread and may complete on any thread. +Keep calling `runCallbacks` (REST or ALL): it processes results and a 30-second timeout. +An error or empty token fails authentication locally with status 400 and reason 90300 +(`CLIENT_APP_CHECK_TOKEN_ERROR`), without sending the request or using the stored token. +Only the first completion is accepted. Resetting communication or destroying the client +silently discards pending requests; late completions are ignored. Configure providers +on the SDK thread and avoid strong ownership cycles. Provider replacement affects new +requests; requests already waiting retain their original provider. + +```cpp +client.getAuthenticationService()->setAppCheckTokenProvider( + [](BrainCloud::BrainCloudAuthentication::AppCheckTokenCompletion completion) { + // Fetch asynchronously through your app's token service, then: + // completion(token, ""); on success, or completion("", errorMessage). + }); +// Restore the manually supplied token: +client.getAuthenticationService()->setAppCheckTokenProvider(nullptr); +``` diff --git a/include/braincloud/AuthenticationType.h b/include/braincloud/AuthenticationType.h index 2adbc588..cf75e881 100644 --- a/include/braincloud/AuthenticationType.h +++ b/include/braincloud/AuthenticationType.h @@ -25,8 +25,8 @@ namespace BrainCloud static const AuthenticationType & Oculus; static const AuthenticationType & GameCenter; static const AuthenticationType & Steam; - static const AuthenticationType & EpicGames; static const AuthenticationType & Apple; + static const AuthenticationType & EpicGames; static const AuthenticationType & Google; static const AuthenticationType & GoogleOpenId; static const AuthenticationType & Twitter; diff --git a/include/braincloud/BrainCloudAuthentication.h b/include/braincloud/BrainCloudAuthentication.h index b1a72cab..0bbecccb 100644 --- a/include/braincloud/BrainCloudAuthentication.h +++ b/include/braincloud/BrainCloudAuthentication.h @@ -9,6 +9,9 @@ #include #include +#include +#include +#include #include "json/json.h" #include "braincloud/BrainCloudTypes.h" #include "braincloud/AuthenticationType.h" @@ -39,6 +42,25 @@ namespace BrainCloud */ void setAppCheckToken(const std::string& token); + typedef std::function AppCheckTokenCompletion; + typedef std::function AppCheckTokenProvider; + + /** + * Resolves a token for every authentication, including retries and automatic reconnects. + * Takes precedence over the setter; an empty provider restores setter behavior. + * Called on the authentication thread. Completion may run on any thread; only its + * first result is used. An error or empty token fails authentication without sending. + * Results and a 30-second timeout are processed by REST/ALL runCallbacks(). + * Configure on the SDK thread. Reset/destruction discards pending results without callbacks. + * The provider is retained until replaced or the client is destroyed; avoid ownership cycles. + * No Firebase dependency is required. + */ + void setAppCheckTokenProvider(AppCheckTokenProvider provider); + + // Internal: called by BrainCloudClient on its callback thread. + void runAppCheckCallbacks(); + void cancelPendingAppCheckRequests(); + /** * Initialize - initializes the identity service with a saved * anonymous installation id and most recently used profile id @@ -480,6 +502,9 @@ namespace BrainCloud private: std::string _appCheckToken; + struct PendingAppCheck; + AppCheckTokenProvider _appCheckTokenProvider; + std::vector > _pendingAppCheck; struct PreviousAuthParams { diff --git a/include/braincloud/reason_codes.h b/include/braincloud/reason_codes.h index e5dcc69f..828aa942 100644 --- a/include/braincloud/reason_codes.h +++ b/include/braincloud/reason_codes.h @@ -733,6 +733,7 @@ static const int CLIENT_UPLOAD_FILE_CANCELLED = 90100; static const int CLIENT_UPLOAD_FILE_TIMED_OUT = 90101; static const int CLIENT_UPLOAD_FILE_UNKNOWN = 90102; static const int CLIENT_DISABLED = 90200; +static const int CLIENT_APP_CHECK_TOKEN_ERROR = 90300; static const int ITUNES_PURCHASE_ALREADY_CLAIMED = APPLE_TRANS_ID_ALREADY_CLAIMED; static const int CHILD_USER_MISSING = CHILD_PLAYER_MISSING; static const int DISABLED_APP = DISABLED_GAME; diff --git a/src/BrainCloudAuthentication.cpp b/src/BrainCloudAuthentication.cpp index c4975ce7..0159359d 100644 --- a/src/BrainCloudAuthentication.cpp +++ b/src/BrainCloudAuthentication.cpp @@ -1,6 +1,9 @@ // Copyright 2026 bitHeads, Inc. All Rights Reserved. #include "braincloud/BrainCloudAuthentication.h" +#include +#include +#include "braincloud/reason_codes.h" #include "braincloud/BrainCloudClient.h" #include "braincloud/IServerCallback.h" @@ -34,6 +37,71 @@ namespace BrainCloud { _appCheckToken = token; } + struct BrainCloudAuthentication::PendingAppCheck + { + std::mutex mutex; + bool completed = false; + std::string token, error; + Json::Value message; + IServerCallback* callback = nullptr; + std::chrono::steady_clock::time_point started = std::chrono::steady_clock::now(); + }; + + void BrainCloudAuthentication::setAppCheckTokenProvider(AppCheckTokenProvider provider) + { + _appCheckTokenProvider = provider; + } + + void BrainCloudAuthentication::cancelPendingAppCheckRequests() + { + _pendingAppCheck.clear(); + } + + void BrainCloudAuthentication::runAppCheckCallbacks() + { + // Remove each result before invoking application code, which can reset the client. + for (size_t i = 0; i < _pendingAppCheck.size();) + { + auto pending = _pendingAppCheck[i]; + std::string token, error; + { + std::lock_guard lock(pending->mutex); + if (!pending->completed) + { + if (std::chrono::steady_clock::now() - pending->started < std::chrono::seconds(30)) + { + ++i; + continue; + } + pending->completed = true; + pending->error = "App Check token provider timed out"; + } + token = pending->token; + error = pending->error; + } + _pendingAppCheck.erase(_pendingAppCheck.begin() + i); + if (!error.empty() || token.empty()) + { + if (pending->callback) + { + Json::Value response; + response["status"] = 400; + response["reason_code"] = CLIENT_APP_CHECK_TOKEN_ERROR; + response["status_message"] = error.empty() ? "App Check token provider returned an empty token" : error; + pending->callback->serverError(ServiceName::AuthenticateV2, ServiceOperation::Authenticate, + 400, CLIENT_APP_CHECK_TOKEN_ERROR, Json::FastWriter().write(response)); + } + } + else + { + pending->message[OperationParam::AuthenticateServiceAuthenticateAppCheckToken.getValue()] = token; + m_client->sendRequest(new ServerCall(ServiceName::AuthenticateV2, + ServiceOperation::Authenticate, pending->message, pending->callback)); + } + i = 0; + } + } + void BrainCloudAuthentication::initialize(const char * profileId, const char * anonymousId) { _profileId = profileId; @@ -344,6 +412,27 @@ namespace BrainCloud { message[OperationParam::AuthenticateServiceAuthenticateExtraJson.getValue()] = JsonUtil::jsonStringToValue(extraJson); } + if (_appCheckTokenProvider) + { + auto pending = std::make_shared(); + pending->message = message; + pending->callback = callback; + _pendingAppCheck.push_back(pending); + std::weak_ptr weakPending = pending; + // Capture no client pointer: late completions after reset/destruction are harmless. + auto provider = _appCheckTokenProvider; + provider([weakPending](const std::string& token, const std::string& error) { + auto result = weakPending.lock(); + if (!result) return; + std::lock_guard lock(result->mutex); + if (result->completed) return; + result->token = token; + result->error = error; + result->completed = true; + }); + return; + } + if (!_appCheckToken.empty()) { message[OperationParam::AuthenticateServiceAuthenticateAppCheckToken.getValue()] = _appCheckToken; diff --git a/src/BrainCloudClient.cpp b/src/BrainCloudClient.cpp index 446ef828..d5b5fd1d 100644 --- a/src/BrainCloudClient.cpp +++ b/src/BrainCloudClient.cpp @@ -271,6 +271,7 @@ namespace BrainCloud switch (updateType) { case eBrainCloudUpdateType::REST: + _authenticationService->runAppCheckCallbacks(); _brainCloudComms->runCallbacks(); break; case eBrainCloudUpdateType::PING: @@ -283,6 +284,7 @@ namespace BrainCloud _relayComms->runCallbacks(); break; case eBrainCloudUpdateType::ALL: + _authenticationService->runAppCheckCallbacks(); _brainCloudComms->runCallbacks(); _lobbyService->runPingCallbacks(); _rttComms->runCallbacks(); @@ -380,6 +382,7 @@ namespace BrainCloud void BrainCloudClient::resetCommunication() { + _authenticationService->cancelPendingAppCheckRequests(); _relayComms->resetCommunication(); _rttComms->resetCommunication(); _brainCloudComms->resetCommunication(); From 3632a823045d9cba474f55e152bb0b49bb5e8307 Mon Sep 17 00:00:00 2001 From: Pierre Proulx Date: Fri, 2 Oct 2026 23:29:04 -0400 Subject: [PATCH 3/4] Added unit test code. --- tests/src/TestBCAppCheck.cpp | 232 +++++++++++++++++++++++++++++++++++ tests/src/TestBCAppCheck.h | 29 +++++ 2 files changed, 261 insertions(+) create mode 100644 tests/src/TestBCAppCheck.cpp create mode 100644 tests/src/TestBCAppCheck.h diff --git a/tests/src/TestBCAppCheck.cpp b/tests/src/TestBCAppCheck.cpp new file mode 100644 index 00000000..fbcf868b --- /dev/null +++ b/tests/src/TestBCAppCheck.cpp @@ -0,0 +1,232 @@ +// Copyright 2026 bitHeads, Inc. All Rights Reserved. + +#include "gtest/gtest.h" +#include +#include "braincloud/reason_codes.h" +#include "braincloud/BrainCloudClient.h" +#include "braincloud/AuthenticationIds.h" +#include "braincloud/internal/DefaultBrainCloudComms.h" + +namespace +{ + using namespace BrainCloud; + + // Capture the actual ServerCall at the queue boundary. No network or test account is needed. + class AppCheckCaptureComms : public DefaultBrainCloudComms + { + public: + explicit AppCheckCaptureComms(BrainCloudClient* client) : DefaultBrainCloudComms(client) + { + _appId = "test-app"; + } + + void addToQueue(ServerCall* call) override + { + payload = *call->getPayload(); + delete call; + } + + Json::Value payload; + }; + + class AppCheckClient : public BrainCloudClient + { + public: + AppCheckClient() + { + delete _brainCloudComms; + _brainCloudComms = new AppCheckCaptureComms(this); + _releasePlatform = "IOS"; + _appVersion = "1.2.3"; + _countryCode = "CA"; + _languageCode = "en"; + _timezoneOffset = 0.0; + getAuthenticationService()->initialize("profile", "anonymous"); + } + + const Json::Value& payload() const + { + return static_cast(_brainCloudComms)->payload; + } + }; + + class TestBCAppCheck : public testing::Test + { + protected: + AppCheckClient client; + + std::string serializedPayload() + { + Json::FastWriter writer; + return writer.write(client.payload()); + } + + // Legacy authenticate ServerCall bytes for fixed inputs, including field order and newline. + std::string legacyPayload() + { + return std::string("{\"data\":{\"anonymousId\":\"anonymous\",\"authenticationToken\":\"password\"," + "\"authenticationType\":\"Universal\",\"clientLib\":\"cpp\",\"clientLibVersion\":\"") + + client.getBrainCloudClientVersion() + + "\",\"compressResponses\":true,\"countryCode\":\"CA\",\"externalId\":\"user\"," + "\"forceCreate\":true,\"gameId\":\"test-app\",\"gameVersion\":\"1.2.3\"," + "\"languageCode\":\"en\",\"profileId\":\"profile\",\"releasePlatform\":\"IOS\"," + "\"timeZoneOffset\":0.0},\"operation\":\"AUTHENTICATE\",\"service\":\"authenticationV2\"}\n"; + } + }; + + TEST_F(TestBCAppCheck, OmittedTokenPreservesLegacyBytes) + { + client.getAuthenticationService()->authenticateUniversal("user", "password", true); + EXPECT_FALSE(client.payload()["data"].isMember("appCheckToken")); + EXPECT_EQ(legacyPayload(), serializedPayload()); + } + + TEST_F(TestBCAppCheck, SuppliedTokenIsCopiedAndExistingFieldsAreUnchanged) + { + std::string token = "opaque.token.with-\"quotes\"\\and\nnewline"; + const std::string original = token; + client.getAuthenticationService()->setAppCheckToken(token); + token = "changed-by-caller"; + client.getAuthenticationService()->authenticateUniversal("user", "password", true); + EXPECT_EQ(original, client.payload()["data"]["appCheckToken"].asString()); + + Json::Value withoutToken = client.payload(); + withoutToken["data"].removeMember("appCheckToken"); + Json::FastWriter writer; + EXPECT_EQ(legacyPayload(), writer.write(withoutToken)); + } + + TEST_F(TestBCAppCheck, ClearingTokenRestoresLegacyBytes) + { + auto auth = client.getAuthenticationService(); + auth->setAppCheckToken("token"); + auth->authenticateUniversal("user", "password", true); + auth->setAppCheckToken(""); + auth->authenticateUniversal("user", "password", true); + EXPECT_EQ(legacyPayload(), serializedPayload()); + } + + TEST_F(TestBCAppCheck, RetryUsesLatestTokenAndClearingIsRespected) + { + auto auth = client.getAuthenticationService(); + auth->setAppCheckToken("first"); + auth->authenticateUniversal("user", "password", true); + auth->setAppCheckToken("refreshed"); + EXPECT_EQ("first", client.payload()["data"]["appCheckToken"].asString()); + auth->retryPreviousAuthenticate(NULL); + EXPECT_EQ("refreshed", client.payload()["data"]["appCheckToken"].asString()); + auth->setAppCheckToken(""); + auth->retryPreviousAuthenticate(NULL); + EXPECT_EQ(legacyPayload(), serializedPayload()); + } + + TEST_F(TestBCAppCheck, TokenPersistsAcrossAuthenticationMethods) + { + auto auth = client.getAuthenticationService(); + auth->setAppCheckToken("token"); + auth->authenticateAnonymous(true); + EXPECT_EQ("token", client.payload()["data"]["appCheckToken"].asString()); + AuthenticationIds ids = { "user", "password", "provider" }; + auth->authenticateAdvanced(AuthenticationType::External, ids, false, "{\"custom\":42}"); + EXPECT_EQ("token", client.payload()["data"]["appCheckToken"].asString()); + EXPECT_EQ("provider", client.payload()["data"]["externalAuthName"].asString()); + EXPECT_EQ(42, client.payload()["data"]["extraJson"]["custom"].asInt()); + } + + TEST_F(TestBCAppCheck, TokenIsNotSentWithOtherOperationsOrSharedBetweenClients) + { + client.getAuthenticationService()->setAppCheckToken("token"); + client.getAuthenticationService()->resetEmailPassword("user@example.com"); + EXPECT_FALSE(client.payload()["data"].isMember("appCheckToken")); + client.getAuthenticationService()->getServerVersion(); + EXPECT_FALSE(client.payload()["data"].isMember("appCheckToken")); + + AppCheckClient other; + other.getAuthenticationService()->authenticateAnonymous(true); + EXPECT_FALSE(other.payload()["data"].isMember("appCheckToken")); + } + + TEST_F(TestBCAppCheck, ProviderRefreshesEachAuthenticationAndCanBeRemoved) + { + auto auth = client.getAuthenticationService(); + auth->setAppCheckToken("manual"); + int calls = 0; + auth->setAppCheckTokenProvider([&](BrainCloudAuthentication::AppCheckTokenCompletion done) { + done(++calls == 1 ? "first" : "fresh", ""); + }); + auth->authenticateAnonymous(false); + EXPECT_TRUE(client.payload().isNull()); + client.runCallbacks(eBrainCloudUpdateType::REST); + EXPECT_EQ("first", client.payload()["data"]["appCheckToken"].asString()); + auth->retryPreviousAuthenticate(nullptr); + client.runCallbacks(eBrainCloudUpdateType::REST); + EXPECT_EQ("fresh", client.payload()["data"]["appCheckToken"].asString()); + EXPECT_EQ(2, calls); + auth->setAppCheckTokenProvider(nullptr); + auth->authenticateAnonymous(false); + EXPECT_EQ("manual", client.payload()["data"]["appCheckToken"].asString()); + } + + TEST_F(TestBCAppCheck, DelayedWorkerCompletionIsMarshalledAndOnlyUsedOnce) + { + auto auth = client.getAuthenticationService(); + BrainCloudAuthentication::AppCheckTokenCompletion done; + auth->setAppCheckTokenProvider([&](BrainCloudAuthentication::AppCheckTokenCompletion completion) { done = completion; }); + auth->authenticateUniversal("user", "password", true); + client.runCallbacks(eBrainCloudUpdateType::REST); + EXPECT_TRUE(client.payload().isNull()); + std::thread worker([&] { done("fresh", ""); done("duplicate", ""); }); + worker.join(); + EXPECT_TRUE(client.payload().isNull()); + client.runCallbacks(eBrainCloudUpdateType::REST); + EXPECT_EQ("fresh", client.payload()["data"]["appCheckToken"].asString()); + EXPECT_EQ("user", client.payload()["data"]["externalId"].asString()); + } + + class ProviderErrorCallback : public IServerCallback + { + public: + int errors = 0; + void serverCallback(ServiceName, ServiceOperation, const std::string&) override { ADD_FAILURE(); } + void serverError(ServiceName, ServiceOperation, int status, int reason, const std::string&) override + { + ++errors; + EXPECT_EQ(400, status); + EXPECT_EQ(CLIENT_APP_CHECK_TOKEN_ERROR, reason); + } + }; + + TEST_F(TestBCAppCheck, ProviderFailureAndEmptyTokenNeverFallBackToStoredToken) + { + auto auth = client.getAuthenticationService(); + ProviderErrorCallback callback; + auth->setAppCheckToken("stale"); + auth->setAppCheckTokenProvider([](BrainCloudAuthentication::AppCheckTokenCompletion done) { done("ignored", "failed"); }); + auth->authenticateAnonymous(false, &callback); + client.runCallbacks(eBrainCloudUpdateType::REST); + auth->setAppCheckTokenProvider([](BrainCloudAuthentication::AppCheckTokenCompletion done) { done("", ""); }); + auth->authenticateAnonymous(false, &callback); + client.runCallbacks(eBrainCloudUpdateType::REST); + EXPECT_EQ(2, callback.errors); + EXPECT_TRUE(client.payload().isNull()); + } + + TEST_F(TestBCAppCheck, ResetAndDestructionIgnoreLateCompletions) + { + BrainCloudAuthentication::AppCheckTokenCompletion done; + auto auth = client.getAuthenticationService(); + auth->setAppCheckTokenProvider([&](BrainCloudAuthentication::AppCheckTokenCompletion completion) { done = completion; }); + auth->authenticateAnonymous(false); + client.resetCommunication(); + done("late", ""); + client.runCallbacks(eBrainCloudUpdateType::REST); + EXPECT_TRUE(client.payload().isNull()); + { + AppCheckClient temporary; + temporary.getAuthenticationService()->setAppCheckTokenProvider([&](BrainCloudAuthentication::AppCheckTokenCompletion completion) { done = completion; }); + temporary.getAuthenticationService()->authenticateAnonymous(false); + } + done("after destruction", ""); + } + +} diff --git a/tests/src/TestBCAppCheck.h b/tests/src/TestBCAppCheck.h new file mode 100644 index 00000000..86ef63bb --- /dev/null +++ b/tests/src/TestBCAppCheck.h @@ -0,0 +1,29 @@ +// Copyright 2026 bitHeads, Inc. All Rights Reserved. + +#ifndef TESTBCAPPCHECK_H +#define TESTBCAPPCHECK_H + +#include "gtest/gtest.h" +#include "braincloud/BrainCloudClient.h" + +namespace AppCheckTests +{ + class AppCheckClient : public BrainCloud::BrainCloudClient + { + public: + AppCheckClient(); + const Json::Value& payload() const; + }; + + // Offline fixture: captures requests without authenticating against a server. + class TestBCAppCheck : public testing::Test + { + protected: + AppCheckClient client; + + std::string serializedPayload(); + std::string legacyPayload(); + }; +} + +#endif From c5c9928fcf2fe7f92cc40676c63b785fa64c1911 Mon Sep 17 00:00:00 2001 From: Pierre Proulx Date: Wed, 21 Oct 2026 14:37:32 -0400 Subject: [PATCH 4/4] Temp commit. --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index 30fd806f..fb032ae0 100644 --- a/README.md +++ b/README.md @@ -364,3 +364,28 @@ client.getAuthenticationService()->setAppCheckTokenProvider( // Restore the manually supplied token: client.getAuthenticationService()->setAppCheckTokenProvider(nullptr); ``` + +//// +Use `setAppCheckTokenProvider` to fetch a fresh token for each authentication attempt, +including retries and reconnects. It overrides `setAppCheckToken` until removed. +Firebase is not required. +The provider runs on the authentication thread and can complete on any thread. +Keep calling runCallbacks (`REST` or `ALL`) to process results and the 30-second timeout. +Errors, empty tokens, and timeouts fail locally with +(`ReasonCodes.CLIENT_APP_CHECK_TOKEN_ERROR`); the stored token is never used as a fallback. + +```cpp +client.getAuthenticationService()->setAppCheckTokenProvider( + [](BrainCloud::BrainCloudAuthentication::AppCheckTokenCompletion completion) { + // Fetch a token asynchronously, then call: + // completion(token, ""); // success + // completion("", errorMessage); // failure + }); + +// Switch back to the manually supplied token: +client.getAuthenticationService()->setAppCheckTokenProvider(nullptr); +``` + +Configure providers on the SDK thread and avoid strong ownership cycles. +Changes apply to new requests. Only the first completion counts; completions +after a reset or client destruction are ignored. \ No newline at end of file