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
1 change: 1 addition & 0 deletions app/src/function_registry.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ enum FunctionId {
FnAppCheckGetTokenAsync,
FnAppCheckAddListener,
FnAppCheckRemoveListener,
FnAppCheckGetLimitedUseTokenAsync,
};

// Class for providing a generic way for firebase libraries to expose their
Expand Down
1 change: 1 addition & 0 deletions app_check/src/common/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ enum AppCheckFn {
kAppCheckFnGetAppCheckToken = 0,
kAppCheckFnGetAppCheckStringInternal,
kAppCheckFnGetLimitedUseAppCheckToken,
kAppCheckFnGetLimitedUseAppCheckStringInternal,
kAppCheckFnCount,
};

Expand Down
47 changes: 47 additions & 0 deletions app_check/src/desktop/app_check_desktop.cc
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,31 @@ Future<std::string> AppCheckInternal::GetAppCheckTokenStringInternal() {
return MakeFuture(future(), handle);
}

Future<std::string>
AppCheckInternal::GetLimitedUseAppCheckTokenStringInternal() {
auto handle = future()->SafeAlloc<std::string>(
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);
Expand All @@ -211,6 +236,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);
Expand All @@ -226,6 +254,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(
Expand All @@ -250,6 +280,23 @@ bool AppCheckInternal::GetAppCheckTokenAsyncForRegistry(App* app,
return false;
}

// static
bool AppCheckInternal::GetLimitedUseAppCheckTokenAsyncForRegistry(
App* app, void* /*unused*/, void* out) {
Future<std::string>* out_future = static_cast<Future<std::string>*>(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);
Expand Down
8 changes: 8 additions & 0 deletions app_check/src/desktop/app_check_desktop.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ class AppCheckInternal {
// internal methods to not conflict with the publicly returned future.
Future<std::string> 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<std::string> GetLimitedUseAppCheckTokenStringInternal();

void AddAppCheckListener(AppCheckListener* listener);

void RemoveAppCheckListener(AppCheckListener* listener);
Expand Down Expand Up @@ -100,6 +104,10 @@ 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);

Expand Down
2 changes: 1 addition & 1 deletion functions/integration_test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ endif()
# Add the Firebase libraries to the target using the function from the SDK.
add_subdirectory(${FIREBASE_CPP_SDK_DIR} bin/ EXCLUDE_FROM_ALL)
# Note that firebase_app needs to be last in the list.
set(firebase_libs firebase_functions firebase_auth firebase_app)
set(firebase_libs firebase_app_check firebase_functions firebase_auth firebase_app)
set(gtest_libs gtest gmock)
target_link_libraries(${integration_test_target_name} ${firebase_libs}
${gtest_libs} ${ADDITIONAL_LIBS})
2 changes: 2 additions & 0 deletions functions/integration_test/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ use_frameworks! :linkage => :static

target 'integration_test' do
platform :ios, '15.0'
pod 'Firebase/AppCheck', '12.10.0'
pod 'Firebase/Functions', '12.10.0'
pod 'Firebase/Auth', '12.10.0'
end

target 'integration_test_tvos' do
platform :tvos, '15.0'
pod 'Firebase/AppCheck', '12.10.0'
pod 'Firebase/Functions', '12.10.0'
pod 'Firebase/Auth', '12.10.0'
end
Expand Down
1 change: 1 addition & 0 deletions functions/integration_test/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ android {

apply from: "$gradle.firebase_cpp_sdk_dir/Android/firebase_dependencies.gradle"
firebaseCpp.dependencies {
appCheck
auth
functions
}
Expand Down
52 changes: 52 additions & 0 deletions functions/integration_test/functions/functions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2026 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


/**
* Import function triggers from their respective submodules:
*
* const {onCall} = require("firebase-functions/v1/https"); // wait, V1 doesn't have /v1/https
*
* See a full list of supported triggers at https://firebase.google.com/docs/functions
*/

const functions = require("firebase-functions");

// Creates a function that consumes limited-use App Check tokens
exports.addtwowithlimiteduse = functions.runWith({
enforceAppCheck: true,
consumeAppCheckToken: true,
maxInstances: 10 // Setting maxInstances the V1 way
}).https.onCall((data, context) => {
// context.app will be defined if a valid App Check token was provided
if (context.app === undefined) {
throw new functions.https.HttpsError(
'failed-precondition',
'The function must be called from an App Check verified app.');
}

const firstNumber = data.firstNumber;
const secondNumber = data.secondNumber;

if (firstNumber === undefined || secondNumber === undefined) {
throw new functions.https.HttpsError('invalid-argument', 'The function must be called with "firstNumber" and "secondNumber".');
}

return {
firstNumber: firstNumber,
secondNumber: secondNumber,
operator: '+',
operationResult: Number(firstNumber) + Number(secondNumber),
};
});
13 changes: 13 additions & 0 deletions functions/integration_test/functions/functions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "functions",
"description": "Cloud Functions for Firebase",
"engines": {
"node": "20"
},
"main": "index.js",
"dependencies": {
"firebase-admin": "^11.5.0",
"firebase-functions": "^4.2.1"
},
"private": true
}
101 changes: 101 additions & 0 deletions functions/integration_test/src/integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

#include "app_framework.h" // NOLINT
#include "firebase/app.h"
#include "firebase/app_check.h"
#include "firebase/app_check/debug_provider.h"
#include "firebase/auth.h"
#include "firebase/functions.h"
#include "firebase/util.h"
Expand Down Expand Up @@ -49,6 +51,10 @@ using firebase_test_framework::FirebaseTest;

const char kIntegrationTestRootPath[] = "integration_test_data";

// Your Firebase project's Debug token goes here.
// You can get this from Firebase Console, in the App Check settings.
const char kAppCheckDebugToken[] = "REPLACE_WITH_APP_CHECK_TOKEN";

class FirebaseFunctionsTest : public FirebaseTest {
public:
FirebaseFunctionsTest();
Expand Down Expand Up @@ -119,8 +125,17 @@ void FirebaseFunctionsTest::TearDown() {
void FirebaseFunctionsTest::Initialize() {
if (initialized_) return;

LogDebug("Initializing Firebase App Check with Debug Provider");
firebase::app_check::DebugAppCheckProviderFactory::GetInstance()
->SetDebugToken(kAppCheckDebugToken);
firebase::app_check::AppCheck::SetAppCheckProviderFactory(
firebase::app_check::DebugAppCheckProviderFactory::GetInstance());

InitializeApp();

// Create the AppCheck instance so it's available for the tests.
::firebase::app_check::AppCheck::GetInstance(app_);

LogDebug("Initializing Firebase Auth and Firebase Functions.");

// 0th element has a reference to this object, the rest have the initializer
Expand Down Expand Up @@ -175,6 +190,17 @@ void FirebaseFunctionsTest::Terminate() {
auth_ = nullptr;
}

if (app_) {
::firebase::app_check::AppCheck* app_check =
::firebase::app_check::AppCheck::GetInstance(app_);
if (app_check) {
LogDebug("Shutdown App Check.");
delete app_check;
}
}

firebase::app_check::AppCheck::SetAppCheckProviderFactory(nullptr);

TerminateApp();

initialized_ = false;
Expand Down Expand Up @@ -380,4 +406,79 @@ TEST_F(FirebaseFunctionsTest, TestFunctionFromURL) {
EXPECT_EQ(result.map()["operationResult"], 6);
}

TEST_F(FirebaseFunctionsTest, TestFunctionWithLimitedUseAppCheckToken) {
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.

So, this test will be a bit weird, but I'm not sure what the best approach is. Currently, App Check isn't linked in with the testapp when it is built, so it would should check the registry, get nothing, and proceed without it. Beyond that, the Function itself isn't expecting an App Check token.

If we wanted a test that actually verified the app check token, we would likely want to make a different function that actually required the App Check token to run.

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.

Ahhh I have added in app check!

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();
// V2 functions can still be addressed via the V1 URL schema which handles
// internal routing
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);
}

TEST_F(FirebaseFunctionsTest, TestV1FunctionWithLimitedUseAppCheckToken) {
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);
}

} // namespace firebase_testapp_automated
Loading
Loading