Skip to content

Commit 5a3de3a

Browse files
authored
Implement limited-use App Check tokens. (#4876)
* Implement `getLimitedUseAppCheckToken`. * Add unit tests. * Update `api.txt` and changelog. * Update test application. * Additional javadoc update. * Address review comment. * Update link to public docs.
1 parent fa9eb02 commit 5a3de3a

File tree

8 files changed

+110
-6
lines changed

8 files changed

+110
-6
lines changed

appcheck/firebase-appcheck/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# Unreleased
2+
* [feature] Added `getLimtedUseAppCheckToken()` for obtaining limited-use tokens
3+
for protecting non-Firebase backends.
24

35
# 16.1.2
46
* [unchanged] Updated to keep [app_check] SDK versions aligned.

appcheck/firebase-appcheck/api.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ package com.google.firebase.appcheck {
2121
method @NonNull public abstract com.google.android.gms.tasks.Task<com.google.firebase.appcheck.AppCheckToken> getAppCheckToken(boolean);
2222
method @NonNull public static com.google.firebase.appcheck.FirebaseAppCheck getInstance();
2323
method @NonNull public static com.google.firebase.appcheck.FirebaseAppCheck getInstance(@NonNull com.google.firebase.FirebaseApp);
24+
method @NonNull public abstract com.google.android.gms.tasks.Task<com.google.firebase.appcheck.AppCheckToken> getLimitedUseAppCheckToken();
2425
method public abstract void installAppCheckProviderFactory(@NonNull com.google.firebase.appcheck.AppCheckProviderFactory);
2526
method public abstract void installAppCheckProviderFactory(@NonNull com.google.firebase.appcheck.AppCheckProviderFactory, boolean);
2627
method public abstract void removeAppCheckListener(@NonNull com.google.firebase.appcheck.FirebaseAppCheck.AppCheckListener);

appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/FirebaseAppCheck.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,29 @@ public abstract void installAppCheckProviderFactory(
7575
* Requests a Firebase App Check token. This method should be used ONLY if you need to authorize
7676
* requests to a non-Firebase backend. Requests to Firebase backends are authorized automatically
7777
* if configured.
78+
*
79+
* <p>If your non-Firebase backend exposes sensitive or expensive endpoints that has low traffic
80+
* volume, consider protecting it with <a
81+
* href=https://firebase.google.com/docs/app-check/custom-resource-backend#replay-protection>Replay
82+
* Protection</a>. In this case, use the #getLimitedUseAppCheckToken() instead to obtain a
83+
* limited-use token.
7884
*/
7985
@NonNull
8086
public abstract Task<AppCheckToken> getAppCheckToken(boolean forceRefresh);
8187

88+
/**
89+
* Requests a Firebase App Check token. This method should be used ONLY if you need to authorize
90+
* requests to a non-Firebase backend.
91+
*
92+
* <p>Returns limited-use tokens that are intended for use with your non-Firebase backend
93+
* endpoints that are protected with <a
94+
* href=https://firebase.google.com/docs/app-check/custom-resource-backend#replay-protection>Replay
95+
* Protection</a>. This method does not affect the token generation behavior of the
96+
* #getAppCheckToken() method.
97+
*/
98+
@NonNull
99+
public abstract Task<AppCheckToken> getLimitedUseAppCheckToken();
100+
82101
/**
83102
* Registers an {@link AppCheckListener} to changes in the token state. This method should be used
84103
* ONLY if you need to authorize requests to a non-Firebase backend. Requests to Firebase backends

appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultFirebaseAppCheck.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,19 @@ public Task<AppCheckToken> getAppCheckToken(boolean forceRefresh) {
229229
});
230230
}
231231

232+
@NonNull
233+
@Override
234+
public Task<AppCheckToken> getLimitedUseAppCheckToken() {
235+
if (appCheckProvider == null) {
236+
return Tasks.forException(new FirebaseException("No AppCheckProvider installed."));
237+
}
238+
239+
// We explicitly do not call the fetchTokenFromProvider helper method, as that method includes
240+
// side effects such as notifying listeners, updating the cached token, and scheduling token
241+
// refresh.
242+
return appCheckProvider.getToken();
243+
}
244+
232245
/** Fetches an {@link AppCheckToken} via the installed {@link AppCheckProvider}. */
233246
Task<AppCheckToken> fetchTokenFromProvider() {
234247
return appCheckProvider

appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/DefaultFirebaseAppCheckTest.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,13 @@ public void testGetAppCheckToken_noFactoryInstalled_taskFails() throws Exception
228228
assertThat(tokenTask.isSuccessful()).isFalse();
229229
}
230230

231+
@Test
232+
public void testGetLimitedUseAppCheckToken_noFactoryInstalled_taskFails() throws Exception {
233+
Task<AppCheckToken> tokenTask = defaultFirebaseAppCheck.getLimitedUseAppCheckToken();
234+
assertThat(tokenTask.isComplete()).isTrue();
235+
assertThat(tokenTask.isSuccessful()).isFalse();
236+
}
237+
231238
@Test
232239
public void testGetToken_factoryInstalled_proxiesToAppCheckFactory() {
233240
defaultFirebaseAppCheck.installAppCheckProviderFactory(mockAppCheckProviderFactory);
@@ -396,4 +403,23 @@ public void testGetAppCheckToken_existingInvalidToken_requestsNewToken() {
396403

397404
verify(mockAppCheckProvider).getToken();
398405
}
406+
407+
@Test
408+
public void testGetLimitedUseAppCheckToken_noExistingToken_requestsNewToken() {
409+
defaultFirebaseAppCheck.installAppCheckProviderFactory(mockAppCheckProviderFactory);
410+
411+
defaultFirebaseAppCheck.getLimitedUseAppCheckToken();
412+
413+
verify(mockAppCheckProvider).getToken();
414+
}
415+
416+
@Test
417+
public void testGetLimitedUseAppCheckToken_existingToken_requestsNewToken() {
418+
defaultFirebaseAppCheck.setCachedToken(validDefaultAppCheckToken);
419+
defaultFirebaseAppCheck.installAppCheckProviderFactory(mockAppCheckProviderFactory);
420+
421+
defaultFirebaseAppCheck.getLimitedUseAppCheckToken();
422+
423+
verify(mockAppCheckProvider).getToken();
424+
}
399425
}

appcheck/firebase-appcheck/test-app/src/main/java/com/googletest/firebase/appcheck/MainActivity.java

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public class MainActivity extends AppCompatActivity {
4747
private Button installSafetyNetButton;
4848
private Button installDebugButton;
4949
private Button getAppCheckTokenButton;
50+
private Button getLimitedUseTokenButton;
5051
private Button listStorageFilesButton;
5152

5253
@Override
@@ -74,7 +75,7 @@ private void initFirebase() {
7475
new AppCheckListener() {
7576
@Override
7677
public void onAppCheckTokenChanged(@NonNull AppCheckToken token) {
77-
Log.d(TAG, "onAppCheckTokenChanged");
78+
Log.d(TAG, "onAppCheckTokenChanged: " + token.getToken());
7879
}
7980
};
8081

@@ -86,6 +87,7 @@ private void initViews() {
8687
installSafetyNetButton = findViewById(R.id.install_safety_net_app_check_button);
8788
installDebugButton = findViewById(R.id.install_debug_app_check_button);
8889
getAppCheckTokenButton = findViewById(R.id.exchange_app_check_button);
90+
getLimitedUseTokenButton = findViewById(R.id.limited_use_app_check_button);
8991
listStorageFilesButton = findViewById(R.id.storage_list_files_button);
9092

9193
setOnClickListeners();
@@ -134,20 +136,55 @@ public void onClick(View v) {
134136
new OnSuccessListener<AppCheckToken>() {
135137
@Override
136138
public void onSuccess(AppCheckToken appCheckToken) {
137-
Log.d(TAG, "Successfully retrieved AppCheck token.");
138-
showToast("Successfully retrieved AppCheck token.");
139+
// Note: Logging App Check tokens is bad practice and should NEVER be done in a
140+
// production application. We log the token here in our unpublished test
141+
// application for easier debugging.
142+
Log.d(
143+
TAG, "Successfully retrieved App Check token: " + appCheckToken.getToken());
144+
showToast("Successfully retrieved App Check token.");
139145
}
140146
});
141147
task.addOnFailureListener(
142148
new OnFailureListener() {
143149
@Override
144150
public void onFailure(@NonNull Exception e) {
145-
Log.d(TAG, "AppCheck token exchange failed with error: " + e.getMessage());
146-
showToast("AppCheck token exchange failed.");
151+
Log.d(TAG, "App Check token exchange failed with error: " + e.getMessage());
152+
showToast("App Check token exchange failed.");
147153
}
148154
});
149155
}
150156
});
157+
158+
getLimitedUseTokenButton.setOnClickListener(
159+
new OnClickListener() {
160+
@Override
161+
public void onClick(View v) {
162+
Task<AppCheckToken> task = firebaseAppCheck.getLimitedUseAppCheckToken();
163+
task.addOnSuccessListener(
164+
new OnSuccessListener<AppCheckToken>() {
165+
@Override
166+
public void onSuccess(AppCheckToken appCheckToken) {
167+
// Note: Logging App Check tokens is bad practice and should NEVER be done in a
168+
// production application. We log the token here in our unpublished test
169+
// application for easier debugging.
170+
Log.d(
171+
TAG,
172+
"Successfully retrieved limited-use App Check token: "
173+
+ appCheckToken.getToken());
174+
showToast("Successfully retrieved limited-use App Check token.");
175+
}
176+
});
177+
task.addOnFailureListener(
178+
new OnFailureListener() {
179+
@Override
180+
public void onFailure(@NonNull Exception e) {
181+
Log.d(TAG, "App Check token exchange failed with error: " + e.getMessage());
182+
showToast("App Check token exchange failed.");
183+
}
184+
});
185+
}
186+
});
187+
151188
listStorageFilesButton.setOnClickListener(
152189
new OnClickListener() {
153190
@Override

appcheck/firebase-appcheck/test-app/src/main/res/layout/activity_main.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
android:layout_width="wrap_content"
2727
android:layout_height="wrap_content"
2828
android:text="@string/exchange_app_check_button_text"/>
29+
<Button
30+
android:id="@+id/limited_use_app_check_button"
31+
android:layout_width="wrap_content"
32+
android:layout_height="wrap_content"
33+
android:text="@string/limited_use_app_check_button_text"/>
2934
<Button
3035
android:id="@+id/storage_list_files_button"
3136
android:layout_width="wrap_content"

appcheck/firebase-appcheck/test-app/src/main/res/values/strings.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<string name="install_play_integrity_app_check_button_text">Install PlayIntegrityAppCheckProvider</string>
44
<string name="install_safety_net_app_check_button_text">Install SafetyNetAppCheckProvider</string>
55
<string name="install_debug_app_check_button_text">Install DebugAppCheckProvider</string>
6-
<string name="exchange_app_check_button_text">Exchange attestation for App Check token</string>
6+
<string name="exchange_app_check_button_text">Get App Check token</string>
7+
<string name="limited_use_app_check_button_text">Get limited-use App Check token</string>
78
<string name="storage_list_files_button_text">List Cloud Storage files with App Check token</string>
89
</resources>

0 commit comments

Comments
 (0)