Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,3 +338,54 @@ 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have the same verbosity concern here too.


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);
```

////
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.
37 changes: 37 additions & 0 deletions include/braincloud/BrainCloudAuthentication.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

#include <string>
#include <map>
#include <functional>

@michaelc-bitheads michaelc-bitheads Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we not use the existing callback objects?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Authentication still uses the existing IServerCallback. These new callbacks handle fetching the App Check token before authentication is sent, so their inputs differ from a server response. We can follow the existing callback-object pattern with dedicated App Check interfaces instead of std::function.

#include <memory>
Comment thread
michaelc-bitheads marked this conversation as resolved.
#include <vector>
#include "json/json.h"
#include "braincloud/BrainCloudTypes.h"
#include "braincloud/AuthenticationType.h"
Expand All @@ -29,6 +32,35 @@ 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);

typedef std::function<void(const std::string& token, const std::string& error)> AppCheckTokenCompletion;
typedef std::function<void(AppCheckTokenCompletion)> 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
Expand Down Expand Up @@ -475,6 +507,11 @@ 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 PendingAppCheck;
AppCheckTokenProvider _appCheckTokenProvider;
std::vector<std::shared_ptr<PendingAppCheck> > _pendingAppCheck;

struct PreviousAuthParams
{
std::string externalId;
Expand Down
1 change: 1 addition & 0 deletions include/braincloud/OperationParam.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ namespace BrainCloud {
// Authenticate Service - Authenticate Params
static const OperationParam AuthenticateServiceAuthenticateAuthenticationType;
static const OperationParam AuthenticateServiceAuthenticateAuthenticationToken;
static const OperationParam AuthenticateServiceAuthenticateAppCheckToken;
Comment thread
michaelc-bitheads marked this conversation as resolved.
static const OperationParam AuthenticateServiceAuthenticateExternalId;
static const OperationParam AuthenticateServiceAuthenticateUniversalId;
static const OperationParam AuthenticateServiceAuthenticateEmailAddress;
Expand Down
1 change: 1 addition & 0 deletions include/braincloud/reason_codes.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
99 changes: 99 additions & 0 deletions src/BrainCloudAuthentication.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Copyright 2026 bitHeads, Inc. All Rights Reserved.

#include "braincloud/BrainCloudAuthentication.h"
#include <mutex>
#include <chrono>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Surely we can track the time more simply than having to spin up a new thread?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No new thread is created here. We store the start time with std::chrono::steady_clock and check elapsed time in the existing callback loop. The mutex protects the result if the token provider completes on another thread. We could use an existing SDK timing utility for consistency, but the timeout already works by polling.

#include "braincloud/reason_codes.h"

#include "braincloud/BrainCloudClient.h"
#include "braincloud/IServerCallback.h"
Expand Down Expand Up @@ -29,6 +32,76 @@ namespace BrainCloud {
_clientLib = lib;
}

void BrainCloudAuthentication::setAppCheckToken(const std::string& token)
{
_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<std::mutex> 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;
Expand Down Expand Up @@ -339,6 +412,32 @@ namespace BrainCloud {
message[OperationParam::AuthenticateServiceAuthenticateExtraJson.getValue()] = JsonUtil::jsonStringToValue(extraJson);
}

if (_appCheckTokenProvider)
{
auto pending = std::make_shared<PendingAppCheck>();
pending->message = message;
pending->callback = callback;
_pendingAppCheck.push_back(pending);
std::weak_ptr<PendingAppCheck> 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<std::mutex> 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;
}

ServerCall * sc = new ServerCall(ServiceName::AuthenticateV2, ServiceOperation::Authenticate, message, callback);
m_client->sendRequest(sc);
}
Expand Down
3 changes: 3 additions & 0 deletions src/BrainCloudClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ namespace BrainCloud
switch (updateType)
{
case eBrainCloudUpdateType::REST:
_authenticationService->runAppCheckCallbacks();
_brainCloudComms->runCallbacks();
break;
case eBrainCloudUpdateType::PING:
Expand All @@ -283,6 +284,7 @@ namespace BrainCloud
_relayComms->runCallbacks();
break;
case eBrainCloudUpdateType::ALL:
_authenticationService->runAppCheckCallbacks();
_brainCloudComms->runCallbacks();
_lobbyService->runPingCallbacks();
_rttComms->runCallbacks();
Expand Down Expand Up @@ -380,6 +382,7 @@ namespace BrainCloud

void BrainCloudClient::resetCommunication()
{
_authenticationService->cancelPendingAppCheckRequests();
_relayComms->resetCommunication();
_rttComms->resetCommunication();
_brainCloudComms->resetCommunication();
Expand Down
1 change: 1 addition & 0 deletions src/OperationParam.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading