Skip to content

Commit 8e5c0a2

Browse files
authored
Limited Use Tokens in Functions (#1840)
* Feat: add in limited use tokens to functions * Update release notes * Add test and callable reference internal for options * Fix docs and formating * Fix builder naming * Add in the app enabled test for th e * Add copyright * Formating fix * Fix typo in functions_ios.mm selector for limited use tokens * Fix token spelling * Fix typo in functions_ios.mm and add app_check dependency to functions in build_testapps.json * Rotate tokend and remove unneeded files * fix secret
1 parent 8882fce commit 8e5c0a2

25 files changed

Lines changed: 425 additions & 21 deletions

File tree

app/src/function_registry.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ enum FunctionId {
3838
FnAppCheckGetTokenAsync,
3939
FnAppCheckAddListener,
4040
FnAppCheckRemoveListener,
41+
FnAppCheckGetLimitedUseTokenAsync,
4142
};
4243

4344
// Class for providing a generic way for firebase libraries to expose their

app_check/src/common/common.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ enum AppCheckFn {
2424
kAppCheckFnGetAppCheckToken = 0,
2525
kAppCheckFnGetAppCheckStringInternal,
2626
kAppCheckFnGetLimitedUseAppCheckToken,
27+
kAppCheckFnGetLimitedUseAppCheckStringInternal,
2728
kAppCheckFnCount,
2829
};
2930

app_check/src/desktop/app_check_desktop.cc

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,31 @@ Future<std::string> AppCheckInternal::GetAppCheckTokenStringInternal() {
186186
return MakeFuture(future(), handle);
187187
}
188188

189+
Future<std::string>
190+
AppCheckInternal::GetLimitedUseAppCheckTokenStringInternal() {
191+
auto handle = future()->SafeAlloc<std::string>(
192+
kAppCheckFnGetLimitedUseAppCheckStringInternal);
193+
194+
AppCheckProvider* provider = GetProvider();
195+
if (provider != nullptr) {
196+
auto token_callback{[this, handle](firebase::app_check::AppCheckToken token,
197+
int error_code,
198+
const std::string& error_message) {
199+
if (error_code == firebase::app_check::kAppCheckErrorNone) {
200+
future()->CompleteWithResult(handle, 0, token.token);
201+
} else {
202+
future()->Complete(handle, error_code, error_message.c_str());
203+
}
204+
}};
205+
provider->GetLimitedUseToken(token_callback);
206+
} else {
207+
future()->Complete(handle,
208+
firebase::app_check::kAppCheckErrorInvalidConfiguration,
209+
"No AppCheckProvider installed.");
210+
}
211+
return MakeFuture(future(), handle);
212+
}
213+
189214
void AppCheckInternal::AddAppCheckListener(AppCheckListener* listener) {
190215
if (listener) {
191216
token_listeners_.push_back(listener);
@@ -211,6 +236,9 @@ void AppCheckInternal::InitRegistryCalls() {
211236
app_->function_registry()->RegisterFunction(
212237
::firebase::internal::FnAppCheckGetTokenAsync,
213238
AppCheckInternal::GetAppCheckTokenAsyncForRegistry);
239+
app_->function_registry()->RegisterFunction(
240+
::firebase::internal::FnAppCheckGetLimitedUseTokenAsync,
241+
AppCheckInternal::GetLimitedUseAppCheckTokenAsyncForRegistry);
214242
app_->function_registry()->RegisterFunction(
215243
::firebase::internal::FnAppCheckAddListener,
216244
AppCheckInternal::AddAppCheckListenerForRegistry);
@@ -226,6 +254,8 @@ void AppCheckInternal::CleanupRegistryCalls() {
226254
if (g_app_check_registry_count == 0) {
227255
app_->function_registry()->UnregisterFunction(
228256
::firebase::internal::FnAppCheckGetTokenAsync);
257+
app_->function_registry()->UnregisterFunction(
258+
::firebase::internal::FnAppCheckGetLimitedUseTokenAsync);
229259
app_->function_registry()->UnregisterFunction(
230260
::firebase::internal::FnAppCheckAddListener);
231261
app_->function_registry()->UnregisterFunction(
@@ -250,6 +280,23 @@ bool AppCheckInternal::GetAppCheckTokenAsyncForRegistry(App* app,
250280
return false;
251281
}
252282

283+
// static
284+
bool AppCheckInternal::GetLimitedUseAppCheckTokenAsyncForRegistry(
285+
App* app, void* /*unused*/, void* out) {
286+
Future<std::string>* out_future = static_cast<Future<std::string>*>(out);
287+
if (!app || !out_future) {
288+
return false;
289+
}
290+
291+
AppCheck* app_check = AppCheck::GetInstance(app);
292+
if (app_check && app_check->internal_) {
293+
*out_future =
294+
app_check->internal_->GetLimitedUseAppCheckTokenStringInternal();
295+
return true;
296+
}
297+
return false;
298+
}
299+
253300
void FunctionRegistryAppCheckListener::AddListener(
254301
FunctionRegistryCallback callback, void* context) {
255302
callbacks_.emplace_back(callback, context);

app_check/src/desktop/app_check_desktop.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ class AppCheckInternal {
7070
// internal methods to not conflict with the publicly returned future.
7171
Future<std::string> GetAppCheckTokenStringInternal();
7272

73+
// Gets the limited-use App Check token as just the string, to be used by
74+
// internal methods to not conflict with the publicly returned future.
75+
Future<std::string> GetLimitedUseAppCheckTokenStringInternal();
76+
7377
void AddAppCheckListener(AppCheckListener* listener);
7478

7579
void RemoveAppCheckListener(AppCheckListener* listener);
@@ -100,6 +104,10 @@ class AppCheckInternal {
100104
static bool GetAppCheckTokenAsyncForRegistry(App* app, void* /*unused*/,
101105
void* out_future);
102106

107+
static bool GetLimitedUseAppCheckTokenAsyncForRegistry(App* app,
108+
void* /*unused*/,
109+
void* out_future);
110+
103111
static bool AddAppCheckListenerForRegistry(App* app, void* callback,
104112
void* context);
105113

functions/integration_test/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ endif()
236236
# Add the Firebase libraries to the target using the function from the SDK.
237237
add_subdirectory(${FIREBASE_CPP_SDK_DIR} bin/ EXCLUDE_FROM_ALL)
238238
# Note that firebase_app needs to be last in the list.
239-
set(firebase_libs firebase_functions firebase_auth firebase_app)
239+
set(firebase_libs firebase_app_check firebase_functions firebase_auth firebase_app)
240240
set(gtest_libs gtest gmock)
241241
target_link_libraries(${integration_test_target_name} ${firebase_libs}
242242
${gtest_libs} ${ADDITIONAL_LIBS})

functions/integration_test/Podfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ use_frameworks! :linkage => :static
44

55
target 'integration_test' do
66
platform :ios, '15.0'
7+
pod 'Firebase/AppCheck', '12.10.0'
78
pod 'Firebase/Functions', '12.10.0'
89
pod 'Firebase/Auth', '12.10.0'
910
end
1011

1112
target 'integration_test_tvos' do
1213
platform :tvos, '15.0'
14+
pod 'Firebase/AppCheck', '12.10.0'
1315
pod 'Firebase/Functions', '12.10.0'
1416
pod 'Firebase/Auth', '12.10.0'
1517
end

functions/integration_test/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ android {
8484

8585
apply from: "$gradle.firebase_cpp_sdk_dir/Android/firebase_dependencies.gradle"
8686
firebaseCpp.dependencies {
87+
appCheck
8788
auth
8889
functions
8990
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2026 Google Inc. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
16+
/**
17+
* Import function triggers from their respective submodules:
18+
*
19+
* const {onCall} = require("firebase-functions/v1/https"); // wait, V1 doesn't have /v1/https
20+
*
21+
* See a full list of supported triggers at https://firebase.google.com/docs/functions
22+
*/
23+
24+
const functions = require("firebase-functions");
25+
26+
// Creates a function that consumes limited-use App Check tokens
27+
exports.addtwowithlimiteduse = functions.runWith({
28+
enforceAppCheck: true,
29+
consumeAppCheckToken: true,
30+
maxInstances: 10 // Setting maxInstances the V1 way
31+
}).https.onCall((data, context) => {
32+
// context.app will be defined if a valid App Check token was provided
33+
if (context.app === undefined) {
34+
throw new functions.https.HttpsError(
35+
'failed-precondition',
36+
'The function must be called from an App Check verified app.');
37+
}
38+
39+
const firstNumber = data.firstNumber;
40+
const secondNumber = data.secondNumber;
41+
42+
if (firstNumber === undefined || secondNumber === undefined) {
43+
throw new functions.https.HttpsError('invalid-argument', 'The function must be called with "firstNumber" and "secondNumber".');
44+
}
45+
46+
return {
47+
firstNumber: firstNumber,
48+
secondNumber: secondNumber,
49+
operator: '+',
50+
operationResult: Number(firstNumber) + Number(secondNumber),
51+
};
52+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "functions",
3+
"description": "Cloud Functions for Firebase",
4+
"engines": {
5+
"node": "20"
6+
},
7+
"main": "index.js",
8+
"dependencies": {
9+
"firebase-admin": "^11.5.0",
10+
"firebase-functions": "^4.2.1"
11+
},
12+
"private": true
13+
}

functions/integration_test/src/integration_test.cc

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
#include "app_framework.h" // NOLINT
2424
#include "firebase/app.h"
25+
#include "firebase/app_check.h"
26+
#include "firebase/app_check/debug_provider.h"
2527
#include "firebase/auth.h"
2628
#include "firebase/functions.h"
2729
#include "firebase/util.h"
@@ -49,6 +51,10 @@ using firebase_test_framework::FirebaseTest;
4951

5052
const char kIntegrationTestRootPath[] = "integration_test_data";
5153

54+
// Your Firebase project's Debug token goes here.
55+
// You can get this from Firebase Console, in the App Check settings.
56+
const char kAppCheckDebugToken[] = "REPLACE_WITH_APP_CHECK_TOKEN";
57+
5258
class FirebaseFunctionsTest : public FirebaseTest {
5359
public:
5460
FirebaseFunctionsTest();
@@ -119,8 +125,17 @@ void FirebaseFunctionsTest::TearDown() {
119125
void FirebaseFunctionsTest::Initialize() {
120126
if (initialized_) return;
121127

128+
LogDebug("Initializing Firebase App Check with Debug Provider");
129+
firebase::app_check::DebugAppCheckProviderFactory::GetInstance()
130+
->SetDebugToken(kAppCheckDebugToken);
131+
firebase::app_check::AppCheck::SetAppCheckProviderFactory(
132+
firebase::app_check::DebugAppCheckProviderFactory::GetInstance());
133+
122134
InitializeApp();
123135

136+
// Create the AppCheck instance so it's available for the tests.
137+
::firebase::app_check::AppCheck::GetInstance(app_);
138+
124139
LogDebug("Initializing Firebase Auth and Firebase Functions.");
125140

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

193+
if (app_) {
194+
::firebase::app_check::AppCheck* app_check =
195+
::firebase::app_check::AppCheck::GetInstance(app_);
196+
if (app_check) {
197+
LogDebug("Shutdown App Check.");
198+
delete app_check;
199+
}
200+
}
201+
202+
firebase::app_check::AppCheck::SetAppCheckProviderFactory(nullptr);
203+
178204
TerminateApp();
179205

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

409+
TEST_F(FirebaseFunctionsTest, TestFunctionWithLimitedUseAppCheckToken) {
410+
SignIn();
411+
412+
// addNumbers(5, 7) = 12
413+
firebase::Variant data(firebase::Variant::EmptyMap());
414+
data.map()["firstNumber"] = 5;
415+
data.map()["secondNumber"] = 7;
416+
417+
firebase::functions::HttpsCallableOptions options;
418+
options.limited_use_app_check_token = true;
419+
420+
LogDebug("Calling addNumbers with Limited Use App Check Token");
421+
firebase::functions::HttpsCallableReference ref =
422+
functions_->GetHttpsCallable("addNumbers", options);
423+
424+
firebase::Variant result =
425+
TestFunctionHelper("addNumbers", ref, &data, firebase::Variant::Null())
426+
.result()
427+
->data();
428+
EXPECT_TRUE(result.is_map());
429+
EXPECT_EQ(result.map()["operationResult"], 12);
430+
}
431+
432+
TEST_F(FirebaseFunctionsTest, TestFunctionFromURLWithLimitedUseAppCheckToken) {
433+
SignIn();
434+
435+
// addNumbers(4, 2) = 6
436+
firebase::Variant data(firebase::Variant::EmptyMap());
437+
data.map()["firstNumber"] = 4;
438+
data.map()["secondNumber"] = 2;
439+
440+
std::string proj = app_->options().project_id();
441+
// V2 functions can still be addressed via the V1 URL schema which handles
442+
// internal routing
443+
std::string url =
444+
"https://us-central1-" + proj + ".cloudfunctions.net/addNumbers";
445+
446+
firebase::functions::HttpsCallableOptions options;
447+
options.limited_use_app_check_token = true;
448+
449+
LogDebug("Calling by URL %s with Limited Use App Check Token", url.c_str());
450+
firebase::functions::HttpsCallableReference ref =
451+
functions_->GetHttpsCallableFromURL(url.c_str(), options);
452+
453+
firebase::Variant result =
454+
TestFunctionHelper(url.c_str(), ref, &data, firebase::Variant::Null())
455+
.result()
456+
->data();
457+
EXPECT_TRUE(result.is_map());
458+
EXPECT_EQ(result.map()["operationResult"], 6);
459+
}
460+
461+
TEST_F(FirebaseFunctionsTest, TestV1FunctionWithLimitedUseAppCheckToken) {
462+
SignIn();
463+
464+
// addNumbers(5, 7) = 12
465+
firebase::Variant data(firebase::Variant::EmptyMap());
466+
data.map()["firstNumber"] = 5;
467+
data.map()["secondNumber"] = 7;
468+
469+
firebase::functions::HttpsCallableOptions options;
470+
options.limited_use_app_check_token = true;
471+
472+
LogDebug("Calling addNumbers with Limited Use App Check Token");
473+
firebase::functions::HttpsCallableReference ref =
474+
functions_->GetHttpsCallable("addNumbers", options);
475+
476+
firebase::Variant result =
477+
TestFunctionHelper("addNumbers", ref, &data, firebase::Variant::Null())
478+
.result()
479+
->data();
480+
EXPECT_TRUE(result.is_map());
481+
EXPECT_EQ(result.map()["operationResult"], 12);
482+
}
483+
383484
} // namespace firebase_testapp_automated

0 commit comments

Comments
 (0)