diff --git a/app_check/integration_test/src/integration_test.cc b/app_check/integration_test/src/integration_test.cc index d0cc4ee8cd..5a2d659c0c 100644 --- a/app_check/integration_test/src/integration_test.cc +++ b/app_check/integration_test/src/integration_test.cc @@ -516,6 +516,28 @@ TEST_F(FirebaseAppCheckTest, TestGetTokenLastResult) { future2.result()->expire_time_millis); } +TEST_F(FirebaseAppCheckTest, TestGetLimitedUseAppCheckToken) { + InitializeAppCheckWithDebug(); + InitializeApp(); + ::firebase::app_check::AppCheck* app_check = + ::firebase::app_check::AppCheck::GetInstance(app_); + ASSERT_NE(app_check, nullptr); + + firebase::Future<::firebase::app_check::AppCheckToken> future = + app_check->GetLimitedUseAppCheckToken(); + EXPECT_TRUE(WaitForCompletion(future, "GetLimitedUseAppCheckToken")); + ::firebase::app_check::AppCheckToken token = *future.result(); + EXPECT_NE(token.token, ""); + EXPECT_NE(token.expire_time_millis, 0); + + firebase::Future<::firebase::app_check::AppCheckToken> future2 = + app_check->GetLimitedUseAppCheckTokenLastResult(); + EXPECT_TRUE( + WaitForCompletion(future2, "GetLimitedUseAppCheckTokenLastResult")); + EXPECT_EQ(future.result()->expire_time_millis, + future2.result()->expire_time_millis); +} + TEST_F(FirebaseAppCheckTest, TestAddTokenChangedListener) { InitializeAppCheckWithDebug(); InitializeApp(); @@ -588,6 +610,32 @@ TEST_F(FirebaseAppCheckTest, TestDebugProviderValidToken) { got_token_future.wait_for(kGetTokenTimeout)); } +TEST_F(FirebaseAppCheckTest, TestDebugProviderValidLimitedUseToken) { + firebase::app_check::DebugAppCheckProviderFactory* factory = + firebase::app_check::DebugAppCheckProviderFactory::GetInstance(); + ASSERT_NE(factory, nullptr); + InitializeAppCheckWithDebug(); + InitializeApp(); + + firebase::app_check::AppCheckProvider* provider = + factory->CreateProvider(app_); + ASSERT_NE(provider, nullptr); + auto got_token_promise = std::make_shared>(); + auto token_callback{ + [got_token_promise](firebase::app_check::AppCheckToken token, + int error_code, const std::string& error_message) { + EXPECT_EQ(firebase::app_check::kAppCheckErrorNone, error_code); + EXPECT_EQ("", error_message); + EXPECT_NE(0, token.expire_time_millis); + EXPECT_NE("", token.token); + got_token_promise->set_value(); + }}; + provider->GetLimitedUseToken(token_callback); + auto got_token_future = got_token_promise->get_future(); + ASSERT_EQ(std::future_status::ready, + got_token_future.wait_for(kGetTokenTimeout)); +} + TEST_F(FirebaseAppCheckTest, TestAppAttestProvider) { firebase::app_check::AppAttestProviderFactory* factory = firebase::app_check::AppAttestProviderFactory::GetInstance(); diff --git a/app_check/src/android/app_check_android.cc b/app_check/src/android/app_check_android.cc index f7295d525a..843d13d4d3 100644 --- a/app_check/src/android/app_check_android.cc +++ b/app_check/src/android/app_check_android.cc @@ -47,6 +47,8 @@ namespace internal { "(Z)V"), \ X(GetToken, "getAppCheckToken", \ "(Z)Lcom/google/android/gms/tasks/Task;"), \ + X(GetLimitedUseToken, "getLimitedUseAppCheckToken", \ + "()Lcom/google/android/gms/tasks/Task;"), \ X(AddAppCheckListener, "addAppCheckListener", \ "(Lcom/google/firebase/appcheck/FirebaseAppCheck$AppCheckListener;)V"), \ X(RemoveAppCheckListener, "removeAppCheckListener", \ @@ -112,8 +114,7 @@ JNIEXPORT void JNICALL JniAppCheckProvider_nativeGetToken( static const JNINativeMethod kNativeJniAppCheckProviderMethods[] = { {"nativeGetToken", "(JLcom/google/android/gms/tasks/TaskCompletionSource;)V", - reinterpret_cast(JniAppCheckProvider_nativeGetToken)}, -}; + reinterpret_cast(JniAppCheckProvider_nativeGetToken)}}; // clang-format off #define JNI_APP_CHECK_LISTENER_METHODS(X) \ @@ -452,6 +453,33 @@ Future AppCheckInternal::GetAppCheckTokenLastResult() { future()->LastResult(kAppCheckFnGetAppCheckToken)); } +Future AppCheckInternal::GetLimitedUseAppCheckToken() { + JNIEnv* env = app_->GetJNIEnv(); + auto handle = + future()->SafeAlloc(kAppCheckFnGetLimitedUseAppCheckToken); + jobject j_task = env->CallObjectMethod( + app_check_impl_, app_check::GetMethodId(app_check::kGetLimitedUseToken)); + + std::string error = util::GetAndClearExceptionMessage(env); + if (error.empty()) { + auto data_handle = new FutureDataHandle(future(), handle); + util::RegisterCallbackOnTask(env, j_task, TokenResultCallback, + reinterpret_cast(data_handle), + jni_task_id_.c_str()); + } else { + AppCheckToken empty_token; + future()->CompleteWithResult(handle, kAppCheckErrorUnknown, error.c_str(), + empty_token); + } + env->DeleteLocalRef(j_task); + return MakeFuture(future(), handle); +} + +Future AppCheckInternal::GetLimitedUseAppCheckTokenLastResult() { + return static_cast&>( + future()->LastResult(kAppCheckFnGetLimitedUseAppCheckToken)); +} + void AppCheckInternal::AddAppCheckListener(AppCheckListener* listener) { MutexLock lock(listeners_mutex_); auto it = std::find(listeners_.begin(), listeners_.end(), listener); diff --git a/app_check/src/android/app_check_android.h b/app_check/src/android/app_check_android.h index 22c30259a1..4ca71c4122 100644 --- a/app_check/src/android/app_check_android.h +++ b/app_check/src/android/app_check_android.h @@ -45,6 +45,10 @@ class AppCheckInternal { Future GetAppCheckTokenLastResult(); + Future GetLimitedUseAppCheckToken(); + + Future GetLimitedUseAppCheckTokenLastResult(); + void AddAppCheckListener(AppCheckListener* listener); void RemoveAppCheckListener(AppCheckListener* listener); diff --git a/app_check/src/android/common_android.cc b/app_check/src/android/common_android.cc index 2bae6cf878..095c1f3c03 100644 --- a/app_check/src/android/common_android.cc +++ b/app_check/src/android/common_android.cc @@ -161,6 +161,16 @@ void AndroidAppCheckProvider::GetToken( env->DeleteLocalRef(j_task); } +void AndroidAppCheckProvider::GetLimitedUseToken( + std::function + completion_callback) { + LogWarning( + "GetLimitedUseToken() was called, but the AppCheckProvider interface on " + "Android does not yet support limited-use tokens. Falling back to " + "GetToken()."); + GetToken(completion_callback); +} + } // namespace internal } // namespace app_check } // namespace firebase diff --git a/app_check/src/android/common_android.h b/app_check/src/android/common_android.h index bd8ca8ec4a..89984772e7 100644 --- a/app_check/src/android/common_android.h +++ b/app_check/src/android/common_android.h @@ -59,6 +59,13 @@ class AndroidAppCheckProvider : public AppCheckProvider { void GetToken(std::function completion_callback) override; + /// Fetches an AppCheckToken via a fallback method to the GetToken. The + /// current Android implementation does not support limited use tokens. For + /// custom App Check providers. + void GetLimitedUseToken( + std::function + completion_callback) override; + private: jobject android_provider_; diff --git a/app_check/src/common/app_check.cc b/app_check/src/common/app_check.cc index 057b8985b4..4e3c065488 100644 --- a/app_check/src/common/app_check.cc +++ b/app_check/src/common/app_check.cc @@ -67,6 +67,18 @@ static std::map<::firebase::App*, AppCheck*>* g_app_check_map = nullptr; // Define the destructors for the virtual listener/provider/factory classes. AppCheckListener::~AppCheckListener() {} AppCheckProvider::~AppCheckProvider() {} + +// Default implementation. Can be overridden by App check providers that support +// limited use tokens +void AppCheckProvider::GetLimitedUseToken( + std::function + completion_callback) { + LogWarning( + "A limited-use token was requested, but the custom provider did not " + "implement the GetLimitedUseToken method. The default implementation is " + "triggered as a result, and GetToken has been invoked instead."); + GetToken(completion_callback); +} AppCheckProviderFactory::~AppCheckProviderFactory() {} namespace internal { @@ -149,6 +161,16 @@ Future AppCheck::GetAppCheckTokenLastResult() { : Future(); } +Future AppCheck::GetLimitedUseAppCheckToken() { + return internal_ ? internal_->GetLimitedUseAppCheckToken() + : Future(); +} + +Future AppCheck::GetLimitedUseAppCheckTokenLastResult() { + return internal_ ? internal_->GetLimitedUseAppCheckTokenLastResult() + : Future(); +} + void AppCheck::AddAppCheckListener(AppCheckListener* listener) { if (!internal_) return; internal_->AddAppCheckListener(listener); diff --git a/app_check/src/common/common.h b/app_check/src/common/common.h index d5bc500587..0303b12a75 100644 --- a/app_check/src/common/common.h +++ b/app_check/src/common/common.h @@ -23,6 +23,7 @@ namespace internal { enum AppCheckFn { kAppCheckFnGetAppCheckToken = 0, kAppCheckFnGetAppCheckStringInternal, + kAppCheckFnGetLimitedUseAppCheckToken, kAppCheckFnCount, }; diff --git a/app_check/src/desktop/app_check_desktop.cc b/app_check/src/desktop/app_check_desktop.cc index 9fc7cf48ea..dce3b3a426 100644 --- a/app_check/src/desktop/app_check_desktop.cc +++ b/app_check/src/desktop/app_check_desktop.cc @@ -120,6 +120,36 @@ Future AppCheckInternal::GetAppCheckTokenLastResult() { future()->LastResult(kAppCheckFnGetAppCheckToken)); } +Future AppCheckInternal::GetLimitedUseAppCheckToken() { + auto handle = + future()->SafeAlloc(kAppCheckFnGetLimitedUseAppCheckToken); + // Get a new token, and pass the result into the future. + 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) { + // Note that we do NOT update the cached token for limited-use tokens. + future()->CompleteWithResult(handle, 0, 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); +} + +Future AppCheckInternal::GetLimitedUseAppCheckTokenLastResult() { + return static_cast&>( + future()->LastResult(kAppCheckFnGetLimitedUseAppCheckToken)); +} + Future AppCheckInternal::GetAppCheckTokenStringInternal() { auto handle = future()->SafeAlloc(kAppCheckFnGetAppCheckStringInternal); diff --git a/app_check/src/desktop/app_check_desktop.h b/app_check/src/desktop/app_check_desktop.h index 87f6d813f7..cdd3d21358 100644 --- a/app_check/src/desktop/app_check_desktop.h +++ b/app_check/src/desktop/app_check_desktop.h @@ -62,6 +62,10 @@ class AppCheckInternal { Future GetAppCheckTokenLastResult(); + Future GetLimitedUseAppCheckToken(); + + Future GetLimitedUseAppCheckTokenLastResult(); + // Gets the App Check token as just the string, to be used by // internal methods to not conflict with the publicly returned future. Future GetAppCheckTokenStringInternal(); diff --git a/app_check/src/desktop/debug_provider_desktop.cc b/app_check/src/desktop/debug_provider_desktop.cc index 26754ede26..e0e558b47a 100644 --- a/app_check/src/desktop/debug_provider_desktop.cc +++ b/app_check/src/desktop/debug_provider_desktop.cc @@ -41,7 +41,16 @@ class DebugAppCheckProvider : public AppCheckProvider { void GetToken(std::function completion_callback) override; + void GetLimitedUseToken( + std::function + completion_callback) override; + private: + void GetTokenInternal( + bool limited_use, + std::function + completion_callback); + App* app_; scheduler::Scheduler scheduler_; @@ -92,6 +101,19 @@ void GetTokenAsync(std::shared_ptr request, void DebugAppCheckProvider::GetToken( std::function completion_callback) { + GetTokenInternal(false, completion_callback); +} + +void DebugAppCheckProvider::GetLimitedUseToken( + std::function + completion_callback) { + GetTokenInternal(true, completion_callback); +} + +void DebugAppCheckProvider::GetTokenInternal( + bool limited_use, + std::function + completion_callback) { // Identify the user's debug token const char* debug_token_cstr; if (!debug_token_.empty()) { @@ -109,6 +131,7 @@ void DebugAppCheckProvider::GetToken( // Exchange debug token with the backend to get a proper attestation token. auto request = std::make_shared(app_); request->SetDebugToken(debug_token_cstr); + request->SetLimitedUse(limited_use); // Use an async call, since we don't want to block on the server response. auto async_call = diff --git a/app_check/src/desktop/debug_token_request.fbs b/app_check/src/desktop/debug_token_request.fbs index 65b2413757..8bcaca2dba 100644 --- a/app_check/src/desktop/debug_token_request.fbs +++ b/app_check/src/desktop/debug_token_request.fbs @@ -16,6 +16,7 @@ namespace firebase.app_check.fbs; table DebugTokenRequest { debug_token:string; + limited_use:bool; } root_type DebugTokenRequest; diff --git a/app_check/src/desktop/debug_token_request.h b/app_check/src/desktop/debug_token_request.h index b2ea2b5cf0..6688e90761 100644 --- a/app_check/src/desktop/debug_token_request.h +++ b/app_check/src/desktop/debug_token_request.h @@ -55,6 +55,11 @@ class DebugTokenRequest application_data_->debug_token = std::move(debug_token); UpdatePostFields(); } + + void SetLimitedUse(bool limited_use) { + application_data_->limited_use = limited_use; + UpdatePostFields(); + } }; } // namespace internal diff --git a/app_check/src/include/firebase/app_check.h b/app_check/src/include/firebase/app_check.h index 19167bcf3b..dfd28b126f 100644 --- a/app_check/src/include/firebase/app_check.h +++ b/app_check/src/include/firebase/app_check.h @@ -78,6 +78,16 @@ class AppCheckProvider { virtual void GetToken( std::function completion_callback) = 0; + + /// Fetches an AppCheckToken suitable for consumption in limited-use scenarios + /// and then calls the provided callback function with the token or with an + /// error code and error message. + /// + /// If you do not implement this method, the default implementation invokes + /// the GetToken method whenever a limited-use token is requested. + virtual void GetLimitedUseToken( + std::function + completion_callback); }; /// Interface for a factory that generates {@link AppCheckProvider}s. @@ -143,6 +153,15 @@ class AppCheck { /// Returns the result of the most recent call to GetAppCheckToken(); Future GetAppCheckTokenLastResult(); + /// Requests a limited-use Firebase App Check token. This method should be + /// used ONLY if you need to authorize requests to a non-Firebase backend. + /// Requests to Firebase backends are authorized automatically if configured. + Future GetLimitedUseAppCheckToken(); + + /// Returns the result of the most recent call to + /// GetLimitedUseAppCheckToken(); + Future GetLimitedUseAppCheckTokenLastResult(); + /// Registers an {@link AppCheckListener} to changes in the token state. This /// method should be used ONLY if you need to authorize requests to a /// non-Firebase backend. Requests to Firebase backends are authorized diff --git a/app_check/src/ios/app_attest_provider_ios.mm b/app_check/src/ios/app_attest_provider_ios.mm index 21e8498f4e..222fb01777 100644 --- a/app_check/src/ios/app_attest_provider_ios.mm +++ b/app_check/src/ios/app_attest_provider_ios.mm @@ -37,6 +37,12 @@ virtual void GetToken( std::function completion_callback) override; + /// Fetches an AppCheckToken suitable for consumption in limited-use scenarios + /// and then calls the provided callback function with the token or with an + /// error code and error message. + virtual void GetLimitedUseToken( + std::function completion_callback) override; + private: FIRAppAttestProvider* ios_provider_; }; @@ -55,6 +61,16 @@ virtual void GetToken( }]; } +void AppAttestProvider::GetLimitedUseToken( + std::function completion_callback) { + [ios_provider_ getLimitedUseTokenWithCompletion:^(FIRAppCheckToken* _Nullable token, + NSError* _Nullable error) { + completion_callback(firebase::app_check::internal::AppCheckTokenFromFIRAppCheckToken(token), + firebase::app_check::internal::AppCheckErrorFromNSError(error), + util::NSStringToString(error.localizedDescription).c_str()); + }]; +} + AppAttestProviderFactoryInternal::AppAttestProviderFactoryInternal() : created_providers_() {} AppAttestProviderFactoryInternal::~AppAttestProviderFactoryInternal() { diff --git a/app_check/src/ios/app_check_ios.h b/app_check/src/ios/app_check_ios.h index bc68aea3c1..003ab2ae08 100644 --- a/app_check/src/ios/app_check_ios.h +++ b/app_check/src/ios/app_check_ios.h @@ -72,6 +72,10 @@ class AppCheckInternal { Future GetAppCheckTokenLastResult(); + Future GetLimitedUseAppCheckToken(); + + Future GetLimitedUseAppCheckTokenLastResult(); + void AddAppCheckListener(AppCheckListener* listener); void RemoveAppCheckListener(AppCheckListener* listener); diff --git a/app_check/src/ios/app_check_ios.mm b/app_check/src/ios/app_check_ios.mm index 1b6d37fd11..4778cbf78f 100644 --- a/app_check/src/ios/app_check_ios.mm +++ b/app_check/src/ios/app_check_ios.mm @@ -59,6 +59,19 @@ - (void)getTokenWithCompletion:(nonnull void (^)(FIRAppCheckToken* _Nullable, _cppProvider->GetToken(token_callback); } +- (void)getLimitedUseTokenWithCompletion:(nonnull void (^)(FIRAppCheckToken* _Nullable, + NSError* _Nullable))handler { + auto token_callback{[handler](firebase::app_check::AppCheckToken token, int error_code, + const std::string& error_message) { + NSError* ios_error = firebase::app_check::internal::AppCheckErrorToNSError( + static_cast(error_code), error_message); + FIRAppCheckToken* ios_token = + firebase::app_check::internal::AppCheckTokenToFIRAppCheckToken(token); + handler(ios_token, ios_error); + }}; + _cppProvider->GetLimitedUseToken(token_callback); +} + @end // Defines an iOS AppCheckProviderFactory that wraps a given C++ Factory. @@ -209,6 +222,39 @@ - (void)appCheckTokenDidChangeNotification:(NSNotification*)notification { future()->LastResult(kAppCheckFnGetAppCheckToken)); } +Future AppCheckInternal::GetLimitedUseAppCheckToken() { + __block SafeFutureHandle handle = + future()->SafeAlloc(kAppCheckFnGetLimitedUseAppCheckToken); + + [impl() + limitedUseTokenWithCompletion:^(FIRAppCheckToken* _Nullable token, NSError* _Nullable error) { + AppCheckToken cpp_token = AppCheckTokenFromFIRAppCheckToken(token); + if (error != nil) { + NSLog(@"Unable to retrieve limited-use App Check token: %@", error); + int error_code = firebase::app_check::internal::AppCheckErrorFromNSError(error); + std::string error_message = util::NSStringToString(error.localizedDescription); + + future()->CompleteWithResult(handle, error_code, error_message.c_str(), cpp_token); + return; + } + if (token == nil) { + NSLog(@"App Check token is nil."); + future()->CompleteWithResult(handle, kAppCheckErrorUnknown, + "AppCheck GetLimitedUseToken returned an " + "empty token.", + cpp_token); + return; + } + future()->CompleteWithResult(handle, kAppCheckErrorNone, cpp_token); + }]; + return MakeFuture(future(), handle); +} + +Future AppCheckInternal::GetLimitedUseAppCheckTokenLastResult() { + return static_cast&>( + future()->LastResult(kAppCheckFnGetLimitedUseAppCheckToken)); +} + void AppCheckInternal::AddAppCheckListener(AppCheckListener* listener) { [notification_center_wrapper() addListener:listener]; } diff --git a/app_check/src/ios/debug_provider_ios.mm b/app_check/src/ios/debug_provider_ios.mm index bcbb289cc2..fda3426da5 100644 --- a/app_check/src/ios/debug_provider_ios.mm +++ b/app_check/src/ios/debug_provider_ios.mm @@ -42,6 +42,12 @@ virtual void GetToken( std::function completion_callback) override; + /// Fetches an AppCheckToken suitable for consumption in limited-use scenarios + /// and then calls the provided callback function with the token or with an + /// error code and error message. + virtual void GetLimitedUseToken( + std::function completion_callback) override; + private: FIRAppCheckDebugProvider* ios_provider_; }; @@ -61,6 +67,16 @@ virtual void GetToken( }]; } +void DebugAppCheckProvider::GetLimitedUseToken( + std::function completion_callback) { + [ios_provider_ getLimitedUseTokenWithCompletion:^(FIRAppCheckToken* _Nullable token, + NSError* _Nullable error) { + completion_callback(firebase::app_check::internal::AppCheckTokenFromFIRAppCheckToken(token), + firebase::app_check::internal::AppCheckErrorFromNSError(error), + util::NSStringToString(error.localizedDescription).c_str()); + }]; +} + DebugAppCheckProviderFactoryInternal::DebugAppCheckProviderFactoryInternal() : created_providers_() { ios_provider_factory_ = std::make_unique( diff --git a/app_check/src/ios/device_check_provider_ios.mm b/app_check/src/ios/device_check_provider_ios.mm index b5e0677f9f..3f660fb639 100644 --- a/app_check/src/ios/device_check_provider_ios.mm +++ b/app_check/src/ios/device_check_provider_ios.mm @@ -38,6 +38,12 @@ virtual void GetToken( std::function completion_callback) override; + /// Fetches an AppCheckToken suitable for consumption in limited-use scenarios + /// and then calls the provided callback function with the token or with an + /// error code and error message. + virtual void GetLimitedUseToken( + std::function completion_callback) override; + private: FIRDeviceCheckProvider* ios_provider_; }; @@ -57,6 +63,16 @@ virtual void GetToken( }]; } +void DeviceCheckProvider::GetLimitedUseToken( + std::function completion_callback) { + [ios_provider_ getLimitedUseTokenWithCompletion:^(FIRAppCheckToken* _Nullable token, + NSError* _Nullable error) { + completion_callback(firebase::app_check::internal::AppCheckTokenFromFIRAppCheckToken(token), + firebase::app_check::internal::AppCheckErrorFromNSError(error), + util::NSStringToString(error.localizedDescription).c_str()); + }]; +} + DeviceCheckProviderFactoryInternal::DeviceCheckProviderFactoryInternal() : created_providers_() { ios_provider_factory_ = std::make_unique( [[FIRDeviceCheckProviderFactory alloc] init]); diff --git a/release_build_files/readme.md b/release_build_files/readme.md index 40b2970e84..bebceabca9 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 + - App Check: Add in support for Limited Use Tokens. + ### 13.4.0 - Changes - General (Android): Update to Firebase Android BoM version 34.8.0.