From 62255d7284d960bf8e2a647a9bbe30eff249ce76 Mon Sep 17 00:00:00 2001 From: AustinBenoit Date: Thu, 5 Mar 2026 11:00:59 -0500 Subject: [PATCH 1/4] Feat: add in limited use tokens to functions --- app/src/function_registry.h | 1 + app_check/src/common/common.h | 1 + app_check/src/desktop/app_check_desktop.cc | 47 +++++++++++ app_check/src/desktop/app_check_desktop.h | 7 ++ functions/src/android/functions_android.cc | 79 ++++++++++++++++++- functions/src/android/functions_android.h | 6 ++ functions/src/common/functions.cc | 12 +++ .../src/desktop/callable_reference_desktop.cc | 20 +++-- .../src/desktop/callable_reference_desktop.h | 6 +- functions/src/desktop/functions_desktop.h | 6 ++ functions/src/include/firebase/functions.h | 10 +++ .../firebase/functions/callable_reference.h | 8 ++ functions/src/ios/functions_ios.h | 4 + functions/src/ios/functions_ios.mm | 24 +++++- 14 files changed, 218 insertions(+), 13 deletions(-) diff --git a/app/src/function_registry.h b/app/src/function_registry.h index 2a1fafad2a..dbdbcccf5b 100644 --- a/app/src/function_registry.h +++ b/app/src/function_registry.h @@ -38,6 +38,7 @@ enum FunctionId { FnAppCheckGetTokenAsync, FnAppCheckAddListener, FnAppCheckRemoveListener, + FnAppCheckGetLimitedUseTokenAsync, }; // Class for providing a generic way for firebase libraries to expose their diff --git a/app_check/src/common/common.h b/app_check/src/common/common.h index 0303b12a75..8c58efb0f4 100644 --- a/app_check/src/common/common.h +++ b/app_check/src/common/common.h @@ -24,6 +24,7 @@ enum AppCheckFn { kAppCheckFnGetAppCheckToken = 0, kAppCheckFnGetAppCheckStringInternal, kAppCheckFnGetLimitedUseAppCheckToken, + kAppCheckFnGetLimitedUseAppCheckStringInternal, kAppCheckFnCount, }; diff --git a/app_check/src/desktop/app_check_desktop.cc b/app_check/src/desktop/app_check_desktop.cc index dce3b3a426..fa867ac543 100644 --- a/app_check/src/desktop/app_check_desktop.cc +++ b/app_check/src/desktop/app_check_desktop.cc @@ -186,6 +186,30 @@ Future AppCheckInternal::GetAppCheckTokenStringInternal() { return MakeFuture(future(), handle); } +Future AppCheckInternal::GetLimitedUseAppCheckTokenStringInternal() { + auto handle = + future()->SafeAlloc(kAppCheckFnGetLimitedUseAppCheckStringInternal); + + AppCheckProvider* provider = GetProvider(); + if (provider != nullptr) { + auto token_callback{ + [this, handle](firebase::app_check::AppCheckToken token, + int error_code, const std::string& error_message) { + if (error_code == firebase::app_check::kAppCheckErrorNone) { + future()->CompleteWithResult(handle, 0, token.token); + } else { + future()->Complete(handle, error_code, error_message.c_str()); + } + }}; + provider->GetLimitedUseToken(token_callback); + } else { + future()->Complete( + handle, firebase::app_check::kAppCheckErrorInvalidConfiguration, + "No AppCheckProvider installed."); + } + return MakeFuture(future(), handle); +} + void AppCheckInternal::AddAppCheckListener(AppCheckListener* listener) { if (listener) { token_listeners_.push_back(listener); @@ -211,6 +235,9 @@ void AppCheckInternal::InitRegistryCalls() { app_->function_registry()->RegisterFunction( ::firebase::internal::FnAppCheckGetTokenAsync, AppCheckInternal::GetAppCheckTokenAsyncForRegistry); + app_->function_registry()->RegisterFunction( + ::firebase::internal::FnAppCheckGetLimitedUseTokenAsync, + AppCheckInternal::GetLimitedUseAppCheckTokenAsyncForRegistry); app_->function_registry()->RegisterFunction( ::firebase::internal::FnAppCheckAddListener, AppCheckInternal::AddAppCheckListenerForRegistry); @@ -226,6 +253,8 @@ void AppCheckInternal::CleanupRegistryCalls() { if (g_app_check_registry_count == 0) { app_->function_registry()->UnregisterFunction( ::firebase::internal::FnAppCheckGetTokenAsync); + app_->function_registry()->UnregisterFunction( + ::firebase::internal::FnAppCheckGetLimitedUseTokenAsync); app_->function_registry()->UnregisterFunction( ::firebase::internal::FnAppCheckAddListener); app_->function_registry()->UnregisterFunction( @@ -250,6 +279,24 @@ bool AppCheckInternal::GetAppCheckTokenAsyncForRegistry(App* app, return false; } +// static +bool AppCheckInternal::GetLimitedUseAppCheckTokenAsyncForRegistry(App* app, + void* /*unused*/, + void* out) { + Future* out_future = static_cast*>(out); + if (!app || !out_future) { + return false; + } + + AppCheck* app_check = AppCheck::GetInstance(app); + if (app_check && app_check->internal_) { + *out_future = + app_check->internal_->GetLimitedUseAppCheckTokenStringInternal(); + return true; + } + return false; +} + void FunctionRegistryAppCheckListener::AddListener( FunctionRegistryCallback callback, void* context) { callbacks_.emplace_back(callback, context); diff --git a/app_check/src/desktop/app_check_desktop.h b/app_check/src/desktop/app_check_desktop.h index cdd3d21358..59b606e935 100644 --- a/app_check/src/desktop/app_check_desktop.h +++ b/app_check/src/desktop/app_check_desktop.h @@ -70,6 +70,10 @@ class AppCheckInternal { // internal methods to not conflict with the publicly returned future. Future GetAppCheckTokenStringInternal(); + // Gets the limited-use App Check token as just the string, to be used by + // internal methods to not conflict with the publicly returned future. + Future GetLimitedUseAppCheckTokenStringInternal(); + void AddAppCheckListener(AppCheckListener* listener); void RemoveAppCheckListener(AppCheckListener* listener); @@ -100,6 +104,9 @@ class AppCheckInternal { static bool GetAppCheckTokenAsyncForRegistry(App* app, void* /*unused*/, void* out_future); + static bool GetLimitedUseAppCheckTokenAsyncForRegistry(App* app, void* /*unused*/, + void* out_future); + static bool AddAppCheckListenerForRegistry(App* app, void* callback, void* context); diff --git a/functions/src/android/functions_android.cc b/functions/src/android/functions_android.cc index 619cd7d02d..82431abaf5 100644 --- a/functions/src/android/functions_android.cc +++ b/functions/src/android/functions_android.cc @@ -35,10 +35,18 @@ namespace internal { "(Ljava/lang/String;)" \ "Lcom/google/firebase/functions/HttpsCallableReference;", \ util::kMethodTypeInstance), \ + X(GetHttpsCallableWithOptions, "getHttpsCallable", \ + "(Ljava/lang/String;Lcom/google/firebase/functions/HttpsCallableOptions;)" \ + "Lcom/google/firebase/functions/HttpsCallableReference;", \ + util::kMethodTypeInstance), \ X(GetHttpsCallableFromURL, "getHttpsCallableFromUrl", \ "(Ljava/net/URL;)" \ "Lcom/google/firebase/functions/HttpsCallableReference;", \ util::kMethodTypeInstance), \ + X(GetHttpsCallableFromURLWithOptions, "getHttpsCallableFromUrl", \ + "(Ljava/net/URL;Lcom/google/firebase/functions/HttpsCallableOptions;)" \ + "Lcom/google/firebase/functions/HttpsCallableReference;", \ + util::kMethodTypeInstance), \ X(UseFunctionsEmulator, "useFunctionsEmulator", \ "(Ljava/lang/String;)V", \ util::kMethodTypeInstance) @@ -50,6 +58,21 @@ METHOD_LOOKUP_DEFINITION(firebase_functions, "com/google/firebase/functions/FirebaseFunctions", FIREBASE_FUNCTIONS_METHODS) +// clang-format off +#define CALLABLE_OPTIONS_METHODS(X) \ + X(BuilderConstructor, "", "()V"), \ + X(SetLimitedUseAppCheckTokens, "setLimitedUseAppCheckTokens", \ + "(Z)Lcom/google/firebase/functions/HttpsCallableOptions$Builder;"), \ + X(Build, "build", "()Lcom/google/firebase/functions/HttpsCallableOptions;") +// clang-format on + +METHOD_LOOKUP_DECLARATION(callable_options_builder, CALLABLE_OPTIONS_METHODS) +METHOD_LOOKUP_DEFINITION( + callable_options_builder, + PROGUARD_KEEP_CLASS + "com/google/firebase/functions/HttpsCallableOptions$Builder", + CALLABLE_OPTIONS_METHODS) + // clang-format off #define FUNCTIONS_EXCEPTION_METHODS(X) \ X(GetMessage, "getMessage", "()Ljava/lang/String;"), \ @@ -125,6 +148,7 @@ bool FunctionsInternal::Initialize(App* app) { functions_exception::CacheMethodIds(env, activity) && functions_exception_code::CacheMethodIds(env, activity) && functions_exception_code::CacheFieldIds(env, activity) && + callable_options_builder::CacheMethodIds(env, activity) && // Call Initialize on all other Functions internal classes. HttpsCallableReferenceInternal::Initialize(app))) { return false; @@ -144,6 +168,7 @@ void FunctionsInternal::Terminate(App* app) { firebase_functions::ReleaseClass(env); functions_exception::ReleaseClass(env); functions_exception_code::ReleaseClass(env); + callable_options_builder::ReleaseClass(env); // Call Terminate on all other Functions internal classes. HttpsCallableReferenceInternal::Terminate(app); @@ -186,14 +211,37 @@ Error FunctionsInternal::ErrorFromJavaFunctionsException( HttpsCallableReferenceInternal* FunctionsInternal::GetHttpsCallable( const char* name) const { + return GetHttpsCallable(name, HttpsCallableOptions()); +} + +HttpsCallableReferenceInternal* FunctionsInternal::GetHttpsCallable( + const char* name, const HttpsCallableOptions& options) const { FIREBASE_ASSERT_RETURN(nullptr, name != nullptr); JNIEnv* env = app_->GetJNIEnv(); + + // Create HttpsCallableOptions + jobject builder = env->NewObject( + callable_options_builder::GetClass(), + callable_options_builder::GetMethodId(callable_options_builder::kBuilderConstructor)); + jobject builder2 = env->CallObjectMethod( + builder, + callable_options_builder::GetMethodId(callable_options_builder::kSetLimitedUseAppCheckTokens), + options.limited_use_app_check_token); + env->DeleteLocalRef(builder); + builder = builder2; + jobject java_options = env->CallObjectMethod( + builder, + callable_options_builder::GetMethodId(callable_options_builder::kBuild)); + env->DeleteLocalRef(builder); + jobject name_string = env->NewStringUTF(name); jobject callable_reference_obj = env->CallObjectMethod( obj_, - firebase_functions::GetMethodId(firebase_functions::kGetHttpsCallable), - name_string); + firebase_functions::GetMethodId(firebase_functions::kGetHttpsCallableWithOptions), + name_string, java_options); env->DeleteLocalRef(name_string); + env->DeleteLocalRef(java_options); + if (util::LogException(env, kLogLevelError, "Functions::GetHttpsCallable() (name = %s) failed", name)) { @@ -208,15 +256,38 @@ HttpsCallableReferenceInternal* FunctionsInternal::GetHttpsCallable( HttpsCallableReferenceInternal* FunctionsInternal::GetHttpsCallableFromURL( const char* url) const { + return GetHttpsCallableFromURL(url, HttpsCallableOptions()); +} + +HttpsCallableReferenceInternal* FunctionsInternal::GetHttpsCallableFromURL( + const char* url, const HttpsCallableOptions& options) const { FIREBASE_ASSERT_RETURN(nullptr, url != nullptr); JNIEnv* env = app_->GetJNIEnv(); + + // Create HttpsCallableOptions + jobject builder = env->NewObject( + callable_options_builder::GetClass(), + callable_options_builder::GetMethodId(callable_options_builder::kBuilderConstructor)); + jobject builder2 = env->CallObjectMethod( + builder, + callable_options_builder::GetMethodId(callable_options_builder::kSetLimitedUseAppCheckTokens), + options.limited_use_app_check_token); + env->DeleteLocalRef(builder); + builder = builder2; + jobject java_options = env->CallObjectMethod( + builder, + callable_options_builder::GetMethodId(callable_options_builder::kBuild)); + env->DeleteLocalRef(builder); + jobject url_object = util::CharsToURL(env, url); jobject callable_reference_obj = env->CallObjectMethod(obj_, firebase_functions::GetMethodId( - firebase_functions::kGetHttpsCallableFromURL), - url_object); + firebase_functions::kGetHttpsCallableFromURLWithOptions), + url_object, java_options); env->DeleteLocalRef(url_object); + env->DeleteLocalRef(java_options); + if (util::LogException( env, kLogLevelError, "Functions::GetHttpsCallableFromURL() (url = %s) failed", url)) { diff --git a/functions/src/android/functions_android.h b/functions/src/android/functions_android.h index 3c57248c02..21a9f234e2 100644 --- a/functions/src/android/functions_android.h +++ b/functions/src/android/functions_android.h @@ -49,10 +49,16 @@ class FunctionsInternal { HttpsCallableReferenceInternal* GetHttpsCallable(const char* name) const; + HttpsCallableReferenceInternal* GetHttpsCallable( + const char* name, const HttpsCallableOptions& options) const; + // Get a FunctionsReference for the specified URL. HttpsCallableReferenceInternal* GetHttpsCallableFromURL( const char* url) const; + HttpsCallableReferenceInternal* GetHttpsCallableFromURL( + const char* url, const HttpsCallableOptions& options) const; + void UseFunctionsEmulator(const char* origin); // Convert an error code obtained from a Java FunctionsException into a C++ diff --git a/functions/src/common/functions.cc b/functions/src/common/functions.cc index 4ea9f0c810..d51061e25b 100644 --- a/functions/src/common/functions.cc +++ b/functions/src/common/functions.cc @@ -147,12 +147,24 @@ HttpsCallableReference Functions::GetHttpsCallable(const char* name) const { return HttpsCallableReference(internal_->GetHttpsCallable(name)); } +HttpsCallableReference Functions::GetHttpsCallable( + const char* name, const HttpsCallableOptions& options) const { + if (!internal_) return HttpsCallableReference(); + return HttpsCallableReference(internal_->GetHttpsCallable(name, options)); +} + HttpsCallableReference Functions::GetHttpsCallableFromURL( const char* url) const { if (!internal_) return HttpsCallableReference(); return HttpsCallableReference(internal_->GetHttpsCallableFromURL(url)); } +HttpsCallableReference Functions::GetHttpsCallableFromURL( + const char* url, const HttpsCallableOptions& options) const { + if (!internal_) return HttpsCallableReference(); + return HttpsCallableReference(internal_->GetHttpsCallableFromURL(url, options)); +} + void Functions::UseFunctionsEmulator(const char* origin) { if (!internal_) return; internal_->UseFunctionsEmulator(origin); diff --git a/functions/src/desktop/callable_reference_desktop.cc b/functions/src/desktop/callable_reference_desktop.cc index 6e5cd204c0..c99a1a44d5 100644 --- a/functions/src/desktop/callable_reference_desktop.cc +++ b/functions/src/desktop/callable_reference_desktop.cc @@ -34,8 +34,9 @@ enum CallableReferenceFn { }; HttpsCallableReferenceInternal::HttpsCallableReferenceInternal( - FunctionsInternal* functions, const char* url) - : functions_(functions), url_(url) { + FunctionsInternal* functions, const char* url, + const HttpsCallableOptions& options) + : functions_(functions), url_(url), options_(options) { functions_->future_manager().AllocFutureApi(this, kCallableReferenceFnCount); rest::InitTransportCurl(); transport_.set_is_async(true); @@ -48,7 +49,7 @@ HttpsCallableReferenceInternal::~HttpsCallableReferenceInternal() { HttpsCallableReferenceInternal::HttpsCallableReferenceInternal( const HttpsCallableReferenceInternal& other) - : functions_(other.functions_), url_(other.url_) { + : functions_(other.functions_), url_(other.url_), options_(other.options_) { functions_->future_manager().AllocFutureApi(this, kCallableReferenceFnCount); rest::InitTransportCurl(); transport_.set_is_async(true); @@ -58,13 +59,16 @@ HttpsCallableReferenceInternal& HttpsCallableReferenceInternal::operator=( const HttpsCallableReferenceInternal& other) { functions_ = other.functions_; url_ = other.url_; + options_ = other.options_; return *this; } #if defined(FIREBASE_USE_MOVE_OPERATORS) || defined(DOXYGEN) HttpsCallableReferenceInternal::HttpsCallableReferenceInternal( HttpsCallableReferenceInternal&& other) - : functions_(other.functions_), url_(std::move(other.url_)) { + : functions_(other.functions_), + url_(std::move(other.url_)), + options_(other.options_) { other.functions_ = nullptr; functions_->future_manager().MoveFutureApi(&other, this); rest::InitTransportCurl(); @@ -76,6 +80,7 @@ HttpsCallableReferenceInternal& HttpsCallableReferenceInternal::operator=( functions_ = other.functions_; other.functions_ = nullptr; url_ = std::move(other.url_); + options_ = other.options_; functions_->future_manager().MoveFutureApi(&other, this); return *this; } @@ -333,9 +338,12 @@ Future HttpsCallableReferenceInternal::Call( // Check for App Check token function Future app_check_future; + ::firebase::internal::FunctionId token_function_id = + options_.limited_use_app_check_token + ? ::firebase::internal::FnAppCheckGetLimitedUseTokenAsync + : ::firebase::internal::FnAppCheckGetTokenAsync; bool succeeded = functions_->app()->function_registry()->CallFunction( - ::firebase::internal::FnAppCheckGetTokenAsync, functions_->app(), nullptr, - &app_check_future); + token_function_id, functions_->app(), nullptr, &app_check_future); if (succeeded && app_check_future.status() != kFutureStatusInvalid) { // Perform the transform request on a completion app_check_future.OnCompletion([&](const Future& future_token) { diff --git a/functions/src/desktop/callable_reference_desktop.h b/functions/src/desktop/callable_reference_desktop.h index 03e89bf393..d8654aeb87 100644 --- a/functions/src/desktop/callable_reference_desktop.h +++ b/functions/src/desktop/callable_reference_desktop.h @@ -54,7 +54,8 @@ class HttpsCallableRequest : public rest::Request { class HttpsCallableReferenceInternal { public: - HttpsCallableReferenceInternal(FunctionsInternal* functions, const char* url); + HttpsCallableReferenceInternal(FunctionsInternal* functions, const char* url, + const HttpsCallableOptions& options); ~HttpsCallableReferenceInternal(); // Copy constructor. It's totally okay (and efficient) to copy @@ -112,6 +113,9 @@ class HttpsCallableReferenceInternal { // The URL of the endpoint this reference points to. std::string url_; + // Options for the callable reference. + HttpsCallableOptions options_; + rest::TransportCurl transport_; // For now, we only allow one request per reference at a time in C++. // TODO(klimt): Figure out the lifetime issues enough to allow multiple diff --git a/functions/src/desktop/functions_desktop.h b/functions/src/desktop/functions_desktop.h index a6224a99a5..a1467fe659 100644 --- a/functions/src/desktop/functions_desktop.h +++ b/functions/src/desktop/functions_desktop.h @@ -39,10 +39,16 @@ class FunctionsInternal { // Get a FunctionsReference for the specified path. HttpsCallableReferenceInternal* GetHttpsCallable(const char* name) const; + HttpsCallableReferenceInternal* GetHttpsCallable( + const char* name, const HttpsCallableOptions& options) const; + // Get a FunctionsReference for the specified URL. HttpsCallableReferenceInternal* GetHttpsCallableFromURL( const char* url) const; + HttpsCallableReferenceInternal* GetHttpsCallableFromURL( + const char* url, const HttpsCallableOptions& options) const; + void UseFunctionsEmulator(const char* origin); // Returns the URL for the endpoint with the given name. diff --git a/functions/src/include/firebase/functions.h b/functions/src/include/firebase/functions.h index 500a05cc7a..2302229664 100644 --- a/functions/src/include/firebase/functions.h +++ b/functions/src/include/firebase/functions.h @@ -90,9 +90,19 @@ class Functions { /// @brief Get a FunctionsReference for the specified path. HttpsCallableReference GetHttpsCallable(const char* name) const; + /// @brief Get a FunctionsReference for the specified path with the given + /// options. + HttpsCallableReference GetHttpsCallable(const char* name, + const HttpsCallableOptions& options) const; + /// @brief Get a FunctionsReference for the specified URL. HttpsCallableReference GetHttpsCallableFromURL(const char* url) const; + /// @brief Get a FunctionsReference for the specified URL with the given + /// options. + HttpsCallableReference GetHttpsCallableFromURL( + const char* url, const HttpsCallableOptions& options) const; + /// @brief Sets an origin for a Cloud Functions emulator to use. void UseFunctionsEmulator(const char* origin); diff --git a/functions/src/include/firebase/functions/callable_reference.h b/functions/src/include/firebase/functions/callable_reference.h index 9641c23564..f1b8773ba9 100644 --- a/functions/src/include/firebase/functions/callable_reference.h +++ b/functions/src/include/firebase/functions/callable_reference.h @@ -34,6 +34,14 @@ class HttpsCallableReferenceInternal; } // namespace internal /// @endcond +struct HttpsCallableOptions { + // Whether to request a limited-use App Check token for this request. + // This is useful for replay protection. + bool limited_use_app_check_token; + + HttpsCallableOptions() : limited_use_app_check_token(false) {} +}; + #ifndef SWIG /// Represents a reference to a Cloud Functions object. /// Developers can call HTTPS Callable Functions. diff --git a/functions/src/ios/functions_ios.h b/functions/src/ios/functions_ios.h index aa8fd9a9ff..b1bdb58974 100644 --- a/functions/src/ios/functions_ios.h +++ b/functions/src/ios/functions_ios.h @@ -55,9 +55,13 @@ class FunctionsInternal { // Get a FunctionsReference for the specified path. HttpsCallableReferenceInternal* GetHttpsCallable(const char* name) const; + HttpsCallableReferenceInternal* GetHttpsCallable( + const char* name, const HttpsCallableOptions& options) const; // Get a FunctionsReference for the specified URL. HttpsCallableReferenceInternal* GetHttpsCallableFromURL(const char* url) const; + HttpsCallableReferenceInternal* GetHttpsCallableFromURL( + const char* url, const HttpsCallableOptions& options) const; void UseFunctionsEmulator(const char* origin); diff --git a/functions/src/ios/functions_ios.mm b/functions/src/ios/functions_ios.mm index b09951e72b..050df318c2 100644 --- a/functions/src/ios/functions_ios.mm +++ b/functions/src/ios/functions_ios.mm @@ -42,18 +42,38 @@ const char* FunctionsInternal::region() const { return region_.c_str(); } HttpsCallableReferenceInternal* FunctionsInternal::GetHttpsCallable(const char* name) const { + return GetHttpsCallable(name, HttpsCallableOptions()); +} + +HttpsCallableReferenceInternal* FunctionsInternal::GetHttpsCallable( + const char* name, const HttpsCallableOptions& options) const { + FIRHTTPSCallableOptions *firOptions = [[FIRHTTPSCallableOptions alloc] + initWithRequireLimitedUseAppCheckTokens:options.limited_use_app_check_token]; + // HttpsCallableReferenceInternal handles deleting the wrapper pointer. return new HttpsCallableReferenceInternal( const_cast(this), - std::make_unique([impl_.get()->get() HTTPSCallableWithName:@(name)])); + std::make_unique([impl_.get()->get() + HTTPSCallableWithName:@(name) + options:firOptions])); } HttpsCallableReferenceInternal* FunctionsInternal::GetHttpsCallableFromURL(const char* url) const { + return GetHttpsCallableFromURL(url, HttpsCallableOptions()); +} + +HttpsCallableReferenceInternal* FunctionsInternal::GetHttpsCallableFromURL( + const char* url, const HttpsCallableOptions& options) const { + FIRHTTPSCallableOptions *firOptions = [[FIRHTTPSCallableOptions alloc] + initWithRequireLimitedUseAppCheckTokens:options.limited_use_app_check_token]; + // HttpsCallableReferenceInternal handles deleting the wrapper pointer. NSURL *nsurl = [NSURL URLWithString:@(url)]; return new HttpsCallableReferenceInternal( const_cast(this), - std::make_unique([impl_.get()->get() HTTPSCallableWithURL:nsurl])); + std::make_unique([impl_.get()->get() + HTTPSCallableWithURL:nsurl + options:firOptions])); } void FunctionsInternal::UseFunctionsEmulator(const char* origin) { From e9a09448b371e9a6a42bd0385799d5179affaafa Mon Sep 17 00:00:00 2001 From: AustinBenoit Date: Thu, 5 Mar 2026 11:08:01 -0500 Subject: [PATCH 2/4] Update release notes --- release_build_files/readme.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/release_build_files/readme.md b/release_build_files/readme.md index fe35f65852..5b684addec 100644 --- a/release_build_files/readme.md +++ b/release_build_files/readme.md @@ -613,6 +613,10 @@ workflow use only during the development of your app, not for publicly shipping code. ## Release Notes +### Upcoming +- Changes + - Functions (general): Add in support for Limited Use Tokens. + ### 13.5.0 - Changes - General (Android): Update to Firebase Android BoM version 34.10.0. From de71bc15ab9ef330a2c53b7d36eaa01828932ec5 Mon Sep 17 00:00:00 2001 From: AustinBenoit Date: Fri, 6 Mar 2026 11:05:11 -0500 Subject: [PATCH 3/4] Add test and callable reference internal for options --- .../integration_test/src/integration_test.cc | 50 +++++++++++++++++++ functions/src/desktop/functions_desktop.cc | 14 +++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/functions/integration_test/src/integration_test.cc b/functions/integration_test/src/integration_test.cc index a61865b2dd..709c674b41 100644 --- a/functions/integration_test/src/integration_test.cc +++ b/functions/integration_test/src/integration_test.cc @@ -380,4 +380,54 @@ TEST_F(FirebaseFunctionsTest, TestFunctionFromURL) { EXPECT_EQ(result.map()["operationResult"], 6); } +TEST_F(FirebaseFunctionsTest, TestFunctionWithLimitedUseAppCheckToken) { + SignIn(); + + // addNumbers(5, 7) = 12 + firebase::Variant data(firebase::Variant::EmptyMap()); + data.map()["firstNumber"] = 5; + data.map()["secondNumber"] = 7; + + firebase::functions::HttpsCallableOptions options; + options.limited_use_app_check_token = true; + + LogDebug("Calling addNumbers with Limited Use App Check Token"); + firebase::functions::HttpsCallableReference ref = + functions_->GetHttpsCallable("addNumbers", options); + + firebase::Variant result = + TestFunctionHelper("addNumbers", ref, &data, firebase::Variant::Null()) + .result() + ->data(); + EXPECT_TRUE(result.is_map()); + EXPECT_EQ(result.map()["operationResult"], 12); +} + +TEST_F(FirebaseFunctionsTest, TestFunctionFromURLWithLimitedUseAppCheckToken) { + SignIn(); + + // addNumbers(4, 2) = 6 + firebase::Variant data(firebase::Variant::EmptyMap()); + data.map()["firstNumber"] = 4; + data.map()["secondNumber"] = 2; + + std::string proj = app_->options().project_id(); + std::string url = + "https://us-central1-" + proj + ".cloudfunctions.net/addNumbers"; + + firebase::functions::HttpsCallableOptions options; + options.limited_use_app_check_token = true; + + LogDebug("Calling by URL %s with Limited Use App Check Token", url.c_str()); + firebase::functions::HttpsCallableReference ref = + functions_->GetHttpsCallableFromURL(url.c_str(), options); + + firebase::Variant result = + TestFunctionHelper(url.c_str(), ref, &data, firebase::Variant::Null()) + .result() + ->data(); + EXPECT_TRUE(result.is_map()); + EXPECT_EQ(result.map()["operationResult"], 6); +} + } // namespace firebase_testapp_automated diff --git a/functions/src/desktop/functions_desktop.cc b/functions/src/desktop/functions_desktop.cc index 9390daa338..4a056a7d5e 100644 --- a/functions/src/desktop/functions_desktop.cc +++ b/functions/src/desktop/functions_desktop.cc @@ -34,14 +34,24 @@ const char* FunctionsInternal::region() const { return region_.c_str(); } HttpsCallableReferenceInternal* FunctionsInternal::GetHttpsCallable( const char* name) const { + return GetHttpsCallable(name, HttpsCallableOptions()); +} + +HttpsCallableReferenceInternal* FunctionsInternal::GetHttpsCallable( + const char* name, const HttpsCallableOptions& options) const { return new HttpsCallableReferenceInternal( - const_cast(this), GetUrl(name).c_str()); + const_cast(this), GetUrl(name).c_str(), options); } HttpsCallableReferenceInternal* FunctionsInternal::GetHttpsCallableFromURL( const char* url) const { + return GetHttpsCallableFromURL(url, HttpsCallableOptions()); +} + +HttpsCallableReferenceInternal* FunctionsInternal::GetHttpsCallableFromURL( + const char* url, const HttpsCallableOptions& options) const { return new HttpsCallableReferenceInternal( - const_cast(this), url); + const_cast(this), url, options); } std::string FunctionsInternal::GetUrl(const std::string& name) const { From 0a63d015a4e686b92bc6557049d84185416c279b Mon Sep 17 00:00:00 2001 From: AustinBenoit Date: Fri, 6 Mar 2026 11:27:21 -0500 Subject: [PATCH 4/4] Fix docs and formating --- app_check/src/desktop/app_check_desktop.cc | 36 +++++++++---------- app_check/src/desktop/app_check_desktop.h | 3 +- functions/src/android/functions_android.cc | 33 +++++++++-------- functions/src/common/functions.cc | 3 +- functions/src/include/firebase/functions.h | 4 +-- .../firebase/functions/callable_reference.h | 7 ++-- 6 files changed, 48 insertions(+), 38 deletions(-) diff --git a/app_check/src/desktop/app_check_desktop.cc b/app_check/src/desktop/app_check_desktop.cc index fa867ac543..4d43f56fd9 100644 --- a/app_check/src/desktop/app_check_desktop.cc +++ b/app_check/src/desktop/app_check_desktop.cc @@ -186,26 +186,27 @@ Future AppCheckInternal::GetAppCheckTokenStringInternal() { return MakeFuture(future(), handle); } -Future AppCheckInternal::GetLimitedUseAppCheckTokenStringInternal() { - auto handle = - future()->SafeAlloc(kAppCheckFnGetLimitedUseAppCheckStringInternal); +Future +AppCheckInternal::GetLimitedUseAppCheckTokenStringInternal() { + auto handle = future()->SafeAlloc( + kAppCheckFnGetLimitedUseAppCheckStringInternal); AppCheckProvider* provider = GetProvider(); if (provider != nullptr) { - auto token_callback{ - [this, handle](firebase::app_check::AppCheckToken token, - int error_code, const std::string& error_message) { - if (error_code == firebase::app_check::kAppCheckErrorNone) { - future()->CompleteWithResult(handle, 0, token.token); - } else { - future()->Complete(handle, error_code, error_message.c_str()); - } - }}; + auto token_callback{[this, handle](firebase::app_check::AppCheckToken token, + int error_code, + const std::string& error_message) { + if (error_code == firebase::app_check::kAppCheckErrorNone) { + future()->CompleteWithResult(handle, 0, token.token); + } else { + future()->Complete(handle, error_code, error_message.c_str()); + } + }}; provider->GetLimitedUseToken(token_callback); } else { - future()->Complete( - handle, firebase::app_check::kAppCheckErrorInvalidConfiguration, - "No AppCheckProvider installed."); + future()->Complete(handle, + firebase::app_check::kAppCheckErrorInvalidConfiguration, + "No AppCheckProvider installed."); } return MakeFuture(future(), handle); } @@ -280,9 +281,8 @@ bool AppCheckInternal::GetAppCheckTokenAsyncForRegistry(App* app, } // static -bool AppCheckInternal::GetLimitedUseAppCheckTokenAsyncForRegistry(App* app, - void* /*unused*/, - void* out) { +bool AppCheckInternal::GetLimitedUseAppCheckTokenAsyncForRegistry( + App* app, void* /*unused*/, void* out) { Future* out_future = static_cast*>(out); if (!app || !out_future) { return false; diff --git a/app_check/src/desktop/app_check_desktop.h b/app_check/src/desktop/app_check_desktop.h index 59b606e935..513221f2ec 100644 --- a/app_check/src/desktop/app_check_desktop.h +++ b/app_check/src/desktop/app_check_desktop.h @@ -104,7 +104,8 @@ class AppCheckInternal { static bool GetAppCheckTokenAsyncForRegistry(App* app, void* /*unused*/, void* out_future); - static bool GetLimitedUseAppCheckTokenAsyncForRegistry(App* app, void* /*unused*/, + static bool GetLimitedUseAppCheckTokenAsyncForRegistry(App* app, + void* /*unused*/, void* out_future); static bool AddAppCheckListenerForRegistry(App* app, void* callback, diff --git a/functions/src/android/functions_android.cc b/functions/src/android/functions_android.cc index 82431abaf5..2f8591e584 100644 --- a/functions/src/android/functions_android.cc +++ b/functions/src/android/functions_android.cc @@ -220,12 +220,14 @@ HttpsCallableReferenceInternal* FunctionsInternal::GetHttpsCallable( JNIEnv* env = app_->GetJNIEnv(); // Create HttpsCallableOptions - jobject builder = env->NewObject( - callable_options_builder::GetClass(), - callable_options_builder::GetMethodId(callable_options_builder::kBuilderConstructor)); + jobject builder = + env->NewObject(callable_options_builder::GetClass(), + callable_options_builder::GetMethodId( + callable_options_builder::kBuilderConstructor)); jobject builder2 = env->CallObjectMethod( builder, - callable_options_builder::GetMethodId(callable_options_builder::kSetLimitedUseAppCheckTokens), + callable_options_builder::GetMethodId( + callable_options_builder::kSetLimitedUseAppCheckTokens), options.limited_use_app_check_token); env->DeleteLocalRef(builder); builder = builder2; @@ -237,7 +239,8 @@ HttpsCallableReferenceInternal* FunctionsInternal::GetHttpsCallable( jobject name_string = env->NewStringUTF(name); jobject callable_reference_obj = env->CallObjectMethod( obj_, - firebase_functions::GetMethodId(firebase_functions::kGetHttpsCallableWithOptions), + firebase_functions::GetMethodId( + firebase_functions::kGetHttpsCallableWithOptions), name_string, java_options); env->DeleteLocalRef(name_string); env->DeleteLocalRef(java_options); @@ -265,12 +268,14 @@ HttpsCallableReferenceInternal* FunctionsInternal::GetHttpsCallableFromURL( JNIEnv* env = app_->GetJNIEnv(); // Create HttpsCallableOptions - jobject builder = env->NewObject( - callable_options_builder::GetClass(), - callable_options_builder::GetMethodId(callable_options_builder::kBuilderConstructor)); + jobject builder = + env->NewObject(callable_options_builder::GetClass(), + callable_options_builder::GetMethodId( + callable_options_builder::kBuilderConstructor)); jobject builder2 = env->CallObjectMethod( builder, - callable_options_builder::GetMethodId(callable_options_builder::kSetLimitedUseAppCheckTokens), + callable_options_builder::GetMethodId( + callable_options_builder::kSetLimitedUseAppCheckTokens), options.limited_use_app_check_token); env->DeleteLocalRef(builder); builder = builder2; @@ -280,11 +285,11 @@ HttpsCallableReferenceInternal* FunctionsInternal::GetHttpsCallableFromURL( env->DeleteLocalRef(builder); jobject url_object = util::CharsToURL(env, url); - jobject callable_reference_obj = - env->CallObjectMethod(obj_, - firebase_functions::GetMethodId( - firebase_functions::kGetHttpsCallableFromURLWithOptions), - url_object, java_options); + jobject callable_reference_obj = env->CallObjectMethod( + obj_, + firebase_functions::GetMethodId( + firebase_functions::kGetHttpsCallableFromURLWithOptions), + url_object, java_options); env->DeleteLocalRef(url_object); env->DeleteLocalRef(java_options); diff --git a/functions/src/common/functions.cc b/functions/src/common/functions.cc index d51061e25b..2d4ae2ddf7 100644 --- a/functions/src/common/functions.cc +++ b/functions/src/common/functions.cc @@ -162,7 +162,8 @@ HttpsCallableReference Functions::GetHttpsCallableFromURL( HttpsCallableReference Functions::GetHttpsCallableFromURL( const char* url, const HttpsCallableOptions& options) const { if (!internal_) return HttpsCallableReference(); - return HttpsCallableReference(internal_->GetHttpsCallableFromURL(url, options)); + return HttpsCallableReference( + internal_->GetHttpsCallableFromURL(url, options)); } void Functions::UseFunctionsEmulator(const char* origin) { diff --git a/functions/src/include/firebase/functions.h b/functions/src/include/firebase/functions.h index 2302229664..669c43f189 100644 --- a/functions/src/include/firebase/functions.h +++ b/functions/src/include/firebase/functions.h @@ -92,8 +92,8 @@ class Functions { /// @brief Get a FunctionsReference for the specified path with the given /// options. - HttpsCallableReference GetHttpsCallable(const char* name, - const HttpsCallableOptions& options) const; + HttpsCallableReference GetHttpsCallable( + const char* name, const HttpsCallableOptions& options) const; /// @brief Get a FunctionsReference for the specified URL. HttpsCallableReference GetHttpsCallableFromURL(const char* url) const; diff --git a/functions/src/include/firebase/functions/callable_reference.h b/functions/src/include/firebase/functions/callable_reference.h index f1b8773ba9..58329cab13 100644 --- a/functions/src/include/firebase/functions/callable_reference.h +++ b/functions/src/include/firebase/functions/callable_reference.h @@ -34,9 +34,12 @@ class HttpsCallableReferenceInternal; } // namespace internal /// @endcond +/// @brief Options for configuring the callable function. +/// These properties are immutable once a callable function reference is +/// instantiated. struct HttpsCallableOptions { - // Whether to request a limited-use App Check token for this request. - // This is useful for replay protection. + /// @brief Whether to request a limited-use App Check token for this request. + /// This is useful for replay protection. bool limited_use_app_check_token; HttpsCallableOptions() : limited_use_app_check_token(false) {}