Skip to content

Commit ecb876d

Browse files
authored
Implement GetAppCheckToken API (#2757)
* Implement 3P GetAppCheckToken API. * Fix DefaultTokenRefresher. * Update test app. * Update tests. * Formatting. * Address review comment. * Inline fetchTokenResultFromProvider.
1 parent f65c333 commit ecb876d

File tree

7 files changed

+292
-64
lines changed

7 files changed

+292
-64
lines changed

appcheck/firebase-appcheck-debug-testing/src/androidTest/java/com/google/firebase/appcheck/debug/testing/FirebaseAppCheckTest.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.google.android.gms.tasks.Task;
2323
import com.google.android.gms.tasks.Tasks;
2424
import com.google.firebase.FirebaseApp;
25+
import com.google.firebase.appcheck.AppCheckToken;
2526
import com.google.firebase.appcheck.AppCheckTokenResult;
2627
import com.google.firebase.appcheck.FirebaseAppCheck;
2728
import com.google.firebase.storage.FirebaseStorage;
@@ -52,7 +53,7 @@ public void tearDown() {
5253
}
5354

5455
@Test
55-
public void exchangeDebugSecretForAppCheckToken() throws Exception {
56+
public void exchangeDebugSecretForAppCheckToken_interopApi() throws Exception {
5657
debugAppCheckTestHelper.withDebugProvider(
5758
() -> {
5859
Task<AppCheckTokenResult> tokenResultTask = firebaseAppCheck.getToken(true);
@@ -63,6 +64,17 @@ public void exchangeDebugSecretForAppCheckToken() throws Exception {
6364
});
6465
}
6566

67+
@Test
68+
public void exchangeDebugSecretForAppCheckToken_publicApi() throws Exception {
69+
debugAppCheckTestHelper.withDebugProvider(
70+
() -> {
71+
Task<AppCheckToken> tokenTask = firebaseAppCheck.getAppCheckToken(true);
72+
Tasks.await(tokenTask);
73+
AppCheckToken result = tokenTask.getResult();
74+
assertThat(result.getToken()).isNotEmpty();
75+
});
76+
}
77+
6678
@Test
6779
@Ignore("TODO: Enable once we have a project with enforcement enabled in CI.")
6880
public void firebaseStorageListFiles_withValidAppCheckToken_success() throws Exception {

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import android.annotation.SuppressLint;
1818
import androidx.annotation.NonNull;
19+
import com.google.android.gms.tasks.Task;
1920
import com.google.firebase.FirebaseApp;
2021
import com.google.firebase.appcheck.interop.AppCheckTokenListener;
2122
import com.google.firebase.appcheck.interop.InternalAppCheckTokenProvider;
@@ -69,4 +70,30 @@ public abstract void installAppCheckProviderFactory(
6970

7071
/** Sets the {@code isTokenAutoRefreshEnabled} flag. */
7172
public abstract void setTokenAutoRefreshEnabled(boolean isTokenAutoRefreshEnabled);
73+
74+
/**
75+
* Requests a Firebase App Check token. This method should be used ONLY if you need to authorize
76+
* requests to a non-Firebase backend. Requests to Firebase backends are authorized automatically
77+
* if configured.
78+
*/
79+
@NonNull
80+
public abstract Task<AppCheckToken> getAppCheckToken(boolean forceRefresh);
81+
82+
/**
83+
* Registers an {@link AppCheckListener} to changes in the token state. This method should be used
84+
* ONLY if you need to authorize requests to a non-Firebase backend. Requests to Firebase backends
85+
* are authorized automatically if configured.
86+
*/
87+
public abstract void addAppCheckListener(@NonNull AppCheckListener listener);
88+
89+
/** Unregisters an {@link AppCheckListener} to changes in the token state. */
90+
public abstract void removeAppCheckListener(@NonNull AppCheckListener listener);
91+
92+
public interface AppCheckListener {
93+
/**
94+
* This method gets invoked on the UI thread on changes to the token state. Does not trigger on
95+
* token expiry.
96+
*/
97+
void onAppCheckTokenChanged(@NonNull AppCheckToken token);
98+
}
7299
}

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

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public class DefaultFirebaseAppCheck extends FirebaseAppCheck {
4646
private final Provider<UserAgentPublisher> userAgentPublisherProvider;
4747
private final Provider<HeartBeatInfo> heartBeatInfoProvider;
4848
private final List<AppCheckTokenListener> appCheckTokenListenerList;
49+
private final List<AppCheckListener> appCheckListenerList;
4950
private final StorageHelper storageHelper;
5051
private final TokenRefreshManager tokenRefreshManager;
5152
private final Clock clock;
@@ -65,6 +66,7 @@ public DefaultFirebaseAppCheck(
6566
this.userAgentPublisherProvider = userAgentPublisherProvider;
6667
this.heartBeatInfoProvider = heartBeatInfoProvider;
6768
this.appCheckTokenListenerList = new ArrayList<>();
69+
this.appCheckListenerList = new ArrayList<>();
6870
this.storageHelper =
6971
new StorageHelper(firebaseApp.getApplicationContext(), firebaseApp.getPersistenceKey());
7072
this.tokenRefreshManager =
@@ -110,7 +112,8 @@ public void resetAppCheckState() {
110112
public void addAppCheckTokenListener(@NonNull AppCheckTokenListener listener) {
111113
checkNotNull(listener);
112114
appCheckTokenListenerList.add(listener);
113-
tokenRefreshManager.onListenerCountChanged(appCheckTokenListenerList.size());
115+
tokenRefreshManager.onListenerCountChanged(
116+
appCheckTokenListenerList.size() + appCheckListenerList.size());
114117
// If there is a token available, trigger the listener with the current token.
115118
if (hasValidToken()) {
116119
listener.onAppCheckTokenChanged(
@@ -122,7 +125,28 @@ public void addAppCheckTokenListener(@NonNull AppCheckTokenListener listener) {
122125
public void removeAppCheckTokenListener(@NonNull AppCheckTokenListener listener) {
123126
checkNotNull(listener);
124127
appCheckTokenListenerList.remove(listener);
125-
tokenRefreshManager.onListenerCountChanged(appCheckTokenListenerList.size());
128+
tokenRefreshManager.onListenerCountChanged(
129+
appCheckTokenListenerList.size() + appCheckListenerList.size());
130+
}
131+
132+
@Override
133+
public void addAppCheckListener(@NonNull AppCheckListener listener) {
134+
checkNotNull(listener);
135+
appCheckListenerList.add(listener);
136+
tokenRefreshManager.onListenerCountChanged(
137+
appCheckTokenListenerList.size() + appCheckListenerList.size());
138+
// If there is a token available, trigger the listener with the current token.
139+
if (hasValidToken()) {
140+
listener.onAppCheckTokenChanged(cachedToken);
141+
}
142+
}
143+
144+
@Override
145+
public void removeAppCheckListener(@NonNull AppCheckListener listener) {
146+
checkNotNull(listener);
147+
appCheckListenerList.remove(listener);
148+
tokenRefreshManager.onListenerCountChanged(
149+
appCheckTokenListenerList.size() + appCheckListenerList.size());
126150
}
127151

128152
@NonNull
@@ -137,33 +161,58 @@ public Task<AppCheckTokenResult> getToken(boolean forceRefresh) {
137161
new FirebaseException("No AppCheckProvider installed.")));
138162
}
139163
// TODO: Cache the in-flight task.
164+
return fetchTokenFromProvider()
165+
.continueWithTask(
166+
new Continuation<AppCheckToken, Task<AppCheckTokenResult>>() {
167+
@Override
168+
public Task<AppCheckTokenResult> then(@NonNull Task<AppCheckToken> task) {
169+
if (task.isSuccessful()) {
170+
return Tasks.forResult(
171+
DefaultAppCheckTokenResult.constructFromAppCheckToken(task.getResult()));
172+
}
173+
// If the token exchange failed, return a dummy token for integrators to attach in
174+
// their headers.
175+
return Tasks.forResult(
176+
DefaultAppCheckTokenResult.constructFromError(
177+
new FirebaseException(
178+
task.getException().getMessage(), task.getException())));
179+
}
180+
});
181+
}
182+
183+
@NonNull
184+
@Override
185+
public Task<AppCheckToken> getAppCheckToken(boolean forceRefresh) {
186+
if (!forceRefresh && hasValidToken()) {
187+
return Tasks.forResult(cachedToken);
188+
}
189+
if (appCheckProvider == null) {
190+
return Tasks.forException(new FirebaseException("No AppCheckProvider installed."));
191+
}
140192
return fetchTokenFromProvider();
141193
}
142194

143-
/** Fetches an {@link AppCheckTokenResult} via the installed {@link AppCheckProvider}. */
144-
Task<AppCheckTokenResult> fetchTokenFromProvider() {
195+
/** Fetches an {@link AppCheckToken} via the installed {@link AppCheckProvider}. */
196+
Task<AppCheckToken> fetchTokenFromProvider() {
145197
return appCheckProvider
146198
.getToken()
147199
.continueWithTask(
148-
new Continuation<AppCheckToken, Task<AppCheckTokenResult>>() {
200+
new Continuation<AppCheckToken, Task<AppCheckToken>>() {
149201
@Override
150-
public Task<AppCheckTokenResult> then(@NonNull Task<AppCheckToken> task) {
202+
public Task<AppCheckToken> then(@NonNull Task<AppCheckToken> task) {
151203
if (task.isSuccessful()) {
152204
AppCheckToken token = task.getResult();
153205
updateStoredToken(token);
206+
for (AppCheckListener listener : appCheckListenerList) {
207+
listener.onAppCheckTokenChanged(token);
208+
}
154209
AppCheckTokenResult tokenResult =
155210
DefaultAppCheckTokenResult.constructFromAppCheckToken(token);
156211
for (AppCheckTokenListener listener : appCheckTokenListenerList) {
157212
listener.onAppCheckTokenChanged(tokenResult);
158213
}
159-
return Tasks.forResult(tokenResult);
160214
}
161-
// If the token exchange failed, return a dummy token for integrators to attach in
162-
// their headers.
163-
return Tasks.forResult(
164-
DefaultAppCheckTokenResult.constructFromError(
165-
new FirebaseException(
166-
task.getException().getMessage(), task.getException())));
215+
return task;
167216
}
168217
});
169218
}

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

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,7 @@
2020

2121
import androidx.annotation.NonNull;
2222
import androidx.annotation.VisibleForTesting;
23-
import com.google.android.gms.tasks.OnCompleteListener;
24-
import com.google.android.gms.tasks.Task;
25-
import com.google.firebase.appcheck.AppCheckTokenResult;
26-
import com.google.firebase.appcheck.internal.util.Logger;
23+
import com.google.android.gms.tasks.OnFailureListener;
2724
import java.util.concurrent.Executors;
2825
import java.util.concurrent.ScheduledExecutorService;
2926
import java.util.concurrent.ScheduledFuture;
@@ -94,22 +91,15 @@ private long getNextRefreshMillis() {
9491
}
9592

9693
private void onRefresh() {
97-
Task<AppCheckTokenResult> task = firebaseAppCheck.fetchTokenFromProvider();
98-
task.addOnCompleteListener(
99-
new OnCompleteListener<AppCheckTokenResult>() {
100-
@Override
101-
public void onComplete(@NonNull Task<AppCheckTokenResult> task) {
102-
if (task.isSuccessful()) {
103-
AppCheckTokenResult tokenResult = task.getResult();
104-
if (tokenResult.getError() != null) {
94+
firebaseAppCheck
95+
.fetchTokenFromProvider()
96+
.addOnFailureListener(
97+
new OnFailureListener() {
98+
@Override
99+
public void onFailure(@NonNull Exception e) {
105100
scheduleRefreshAfterFailure();
106101
}
107-
} else {
108-
// Task was not successful; this should not happen.
109-
Logger.getLogger().e("Unexpected failure while fetching token.");
110-
}
111-
}
112-
});
102+
});
113103
}
114104

115105
/** Cancels the in-flight scheduled refresh. */

0 commit comments

Comments
 (0)