Skip to content

Commit 6cd3540

Browse files
authored
Implementing breaking API changes for FirebaseCredential API (firebase#21)
* Implementing breaking API changes for FirebaseCredential API; Returning a GoogleOAuthAccessToken instance which contains expirty info in addition to the token string. Implementing all token caching at the FirebaseApp level. * Updating javadoc; Removing some redundant assertions * Moved isExpired() functionality to FirebaseApp
1 parent aa39832 commit 6cd3540

8 files changed

Lines changed: 260 additions & 202 deletions

File tree

src/main/java/com/google/firebase/FirebaseApp.java

Lines changed: 62 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import com.google.common.base.Strings;
2828
import com.google.common.collect.ImmutableList;
2929
import com.google.common.io.BaseEncoding;
30+
import com.google.firebase.auth.GoogleOAuthAccessToken;
3031
import com.google.firebase.internal.AuthStateListener;
3132
import com.google.firebase.internal.FirebaseAppStore;
3233
import com.google.firebase.internal.FirebaseExecutors;
@@ -67,8 +68,10 @@ public class FirebaseApp {
6768

6869
public static final String DEFAULT_APP_NAME = "[DEFAULT]";
6970
private static final long TOKEN_REFRESH_INTERVAL_MILLIS = TimeUnit.MINUTES.toMillis(55);
70-
private static final TokenRefresher.Factory DEFAULT_TOKEN_REFRESHER_FACTORY =
71+
72+
static final TokenRefresher.Factory DEFAULT_TOKEN_REFRESHER_FACTORY =
7173
new TokenRefresher.Factory();
74+
static final Clock DEFAULT_CLOCK = new Clock();
7275

7376
/**
7477
* Global lock for synchronizing all SDK-wide application state changes. Specifically, any
@@ -79,23 +82,28 @@ public class FirebaseApp {
7982
private final String name;
8083
private final FirebaseOptions options;
8184
private final TokenRefresher tokenRefresher;
85+
private final Clock clock;
8286

8387
private final AtomicBoolean deleted = new AtomicBoolean();
8488
private final List<AuthStateListener> authStateListeners = new ArrayList<>();
8589
private final AtomicReference<GetTokenResult> currentToken = new AtomicReference<>();
8690
private final Map<String, FirebaseService> services = new HashMap<>();
8791

92+
private Task<GoogleOAuthAccessToken> previousTokenTask;
93+
8894
/**
8995
* Per application lock for synchronizing all internal FirebaseApp state changes.
9096
*/
9197
private final Object lock = new Object();
9298

9399
/** Default constructor. */
94-
private FirebaseApp(String name, FirebaseOptions options, TokenRefresher.Factory factory) {
100+
private FirebaseApp(String name, FirebaseOptions options,
101+
TokenRefresher.Factory factory, Clock clock) {
95102
checkArgument(!Strings.isNullOrEmpty(name));
96103
this.name = name;
97104
this.options = checkNotNull(options);
98-
tokenRefresher = checkNotNull(factory).create(this);
105+
this.tokenRefresher = checkNotNull(factory).create(this);
106+
this.clock = checkNotNull(clock);
99107
}
100108

101109
/** Returns a list of all FirebaseApps. */
@@ -168,11 +176,11 @@ public static FirebaseApp initializeApp(FirebaseOptions options) {
168176
* @throws IllegalStateException if an app with the same name has already been initialized.
169177
*/
170178
public static FirebaseApp initializeApp(FirebaseOptions options, String name) {
171-
return initializeApp(options, name, DEFAULT_TOKEN_REFRESHER_FACTORY);
179+
return initializeApp(options, name, DEFAULT_TOKEN_REFRESHER_FACTORY, DEFAULT_CLOCK);
172180
}
173181

174-
static FirebaseApp initializeApp(
175-
FirebaseOptions options, String name, TokenRefresher.Factory tokenRefresherFactory) {
182+
static FirebaseApp initializeApp(FirebaseOptions options, String name,
183+
TokenRefresher.Factory tokenRefresherFactory, Clock clock) {
176184
FirebaseAppStore appStore = FirebaseAppStore.initialize();
177185
String normalizedName = normalize(name);
178186
final FirebaseApp firebaseApp;
@@ -181,7 +189,7 @@ static FirebaseApp initializeApp(
181189
!instances.containsKey(normalizedName),
182190
"FirebaseApp name " + normalizedName + " already exists!");
183191

184-
firebaseApp = new FirebaseApp(normalizedName, options, tokenRefresherFactory);
192+
firebaseApp = new FirebaseApp(normalizedName, options, tokenRefresherFactory, clock);
185193
instances.put(normalizedName, firebaseApp);
186194
}
187195

@@ -305,6 +313,13 @@ private void checkNotDeleted() {
305313
checkState(!deleted.get(), "FirebaseApp was deleted %s", this);
306314
}
307315

316+
private boolean refreshRequired(
317+
@NonNull Task<GoogleOAuthAccessToken> previousTask, boolean forceRefresh) {
318+
return (previousTask.isComplete()
319+
&& (forceRefresh || !previousTask.isSuccessful()
320+
|| previousTask.getResult().getExpiryTime() <= clock.now()));
321+
}
322+
308323
/**
309324
* Internal-only method to fetch a valid Service Account OAuth2 Token.
310325
*
@@ -313,41 +328,45 @@ private void checkNotDeleted() {
313328
* @return a {@link Task}
314329
*/
315330
Task<GetTokenResult> getToken(boolean forceRefresh) {
316-
checkNotDeleted();
317-
return options
318-
.getCredential()
319-
.getAccessToken(forceRefresh)
320-
.continueWith(
321-
new Continuation<String, GetTokenResult>() {
322-
@Override
323-
public GetTokenResult then(@NonNull Task<String> task) throws Exception {
324-
GetTokenResult newToken = new GetTokenResult(task.getResult());
325-
GetTokenResult oldToken = currentToken.get();
326-
List<AuthStateListener> listenersCopy = null;
327-
if (!newToken.equals(oldToken)) {
328-
synchronized (lock) {
329-
if (deleted.get()) {
330-
return newToken;
331-
}
332-
333-
// Grab the lock before compareAndSet to avoid a potential race
334-
// condition with addAuthStateListener. The same lock also ensures serial
335-
// access to the token refresher.
336-
if (currentToken.compareAndSet(oldToken, newToken)) {
337-
listenersCopy = ImmutableList.copyOf(authStateListeners);
338-
tokenRefresher.scheduleRefresh(TOKEN_REFRESH_INTERVAL_MILLIS);
339-
}
331+
synchronized (lock) {
332+
checkNotDeleted();
333+
if (previousTokenTask == null || refreshRequired(previousTokenTask, forceRefresh)) {
334+
previousTokenTask = options.getCredential().getAccessToken();
335+
}
336+
337+
return previousTokenTask.continueWith(
338+
new Continuation<GoogleOAuthAccessToken, GetTokenResult>() {
339+
@Override
340+
public GetTokenResult then(@NonNull Task<GoogleOAuthAccessToken> task)
341+
throws Exception {
342+
GetTokenResult newToken = new GetTokenResult(task.getResult().getAccessToken());
343+
GetTokenResult oldToken = currentToken.get();
344+
List<AuthStateListener> listenersCopy = null;
345+
if (!newToken.equals(oldToken)) {
346+
synchronized (lock) {
347+
if (deleted.get()) {
348+
return newToken;
340349
}
341-
}
342350

343-
if (listenersCopy != null) {
344-
for (AuthStateListener listener : listenersCopy) {
345-
listener.onAuthStateChanged(newToken);
351+
// Grab the lock before compareAndSet to avoid a potential race
352+
// condition with addAuthStateListener. The same lock also ensures serial
353+
// access to the token refresher.
354+
if (currentToken.compareAndSet(oldToken, newToken)) {
355+
listenersCopy = ImmutableList.copyOf(authStateListeners);
356+
tokenRefresher.scheduleRefresh(TOKEN_REFRESH_INTERVAL_MILLIS);
346357
}
347358
}
348-
return newToken;
349359
}
350-
});
360+
361+
if (listenersCopy != null) {
362+
for (AuthStateListener listener : listenersCopy) {
363+
listener.onAuthStateChanged(newToken);
364+
}
365+
}
366+
return newToken;
367+
}
368+
});
369+
}
351370
}
352371

353372
boolean isDefaultApp() {
@@ -451,4 +470,10 @@ TokenRefresher create(FirebaseApp app) {
451470
}
452471
}
453472
}
473+
474+
static class Clock {
475+
long now() {
476+
return System.currentTimeMillis();
477+
}
478+
}
454479
}

src/main/java/com/google/firebase/auth/FirebaseAuth.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ public Task<String> createCustomToken(
129129
}
130130

131131
return ((FirebaseCredentials.CertCredential) credential)
132-
.getCertificate(false)
132+
.getCertificate()
133133
.continueWith(
134134
new Continuation<GoogleCredential, String>() {
135135
@Override

src/main/java/com/google/firebase/auth/FirebaseCredential.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@
2626
public interface FirebaseCredential {
2727

2828
/**
29-
* Returns a Google OAuth2 access token used to authenticate with Firebase services.
29+
* Returns a Google OAuth2 access token which can be used to authenticate with Firebase services.
30+
* This method does not cache tokens, and therefore each invocation will fetch a fresh token.
31+
* The caller is expected to implement caching by referencing the token expiry details
32+
* available in the returned GoogleOAuthAccessToken instance.
3033
*
31-
* @param forceRefresh Whether to fetch a new token or use a cached one if available.
32-
* @return A {@link Task} providing an access token.
34+
* @return A {@link Task} providing a Google OAuth access token.
3335
*/
34-
Task<String> getAccessToken(boolean forceRefresh);
36+
Task<GoogleOAuthAccessToken> getAccessToken();
3537
}

src/main/java/com/google/firebase/auth/FirebaseCredentials.java

Lines changed: 25 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -142,39 +142,24 @@ abstract static class BaseCredential implements FirebaseCredential {
142142

143143
final HttpTransport transport;
144144
final JsonFactory jsonFactory;
145-
final Clock clock;
146-
private final Object accessTokenTaskLock = new Object();
147145
private GoogleCredential googleCredential;
148-
private Task<FirebaseAccessToken> accessTokenTask;
149146

150147
BaseCredential(HttpTransport transport, JsonFactory jsonFactory) {
151-
this(transport, jsonFactory, new Clock());
152-
}
153-
154-
BaseCredential(HttpTransport transport, JsonFactory jsonFactory, Clock clock) {
155148
this.transport = checkNotNull(transport, "HttpTransport must not be null");
156149
this.jsonFactory = checkNotNull(jsonFactory, "JsonFactory must not be null");
157-
this.clock = checkNotNull(clock, "Clock must not be null");
158150
}
159151

160152
/** Retrieves a GoogleCredential. Should not use caching. */
161153
abstract GoogleCredential fetchCredential() throws IOException;
162154

163-
/** Retrieves an access token from a GoogleCredential. Should not use caching. */
164-
abstract FirebaseAccessToken fetchToken(GoogleCredential credential) throws IOException;
165-
166155
/**
167156
* Returns the associated GoogleCredential for this class. This implementation is cached by
168157
* default.
169-
*
170-
* @param forceRefresh Whether to fetch from cache
171158
*/
172-
final Task<GoogleCredential> getCertificate(boolean forceRefresh) {
173-
if (!forceRefresh) {
174-
synchronized (this) {
175-
if (googleCredential != null) {
176-
return Tasks.forResult(googleCredential);
177-
}
159+
final Task<GoogleCredential> getCertificate() {
160+
synchronized (this) {
161+
if (googleCredential != null) {
162+
return Tasks.forResult(googleCredential);
178163
}
179164
}
180165

@@ -193,44 +178,21 @@ public GoogleCredential call() throws Exception {
193178
});
194179
}
195180

196-
private boolean refreshRequired(
197-
@NonNull Task<FirebaseAccessToken> previousTask, boolean forceRefresh) {
198-
return previousTask == null
199-
|| (previousTask.isComplete()
200-
&& (forceRefresh
201-
|| !previousTask.isSuccessful()
202-
|| previousTask.getResult().isExpired()));
203-
}
181+
abstract GoogleOAuthAccessToken fetchToken(GoogleCredential credential) throws IOException;
204182

205183
/**
206-
* Returns an access token for this credential. This implementation is cached by default.
207-
*
208-
* @param forceRefresh Whether or not to force an access token refresh
184+
* Returns an access token for this credential. Does not cache tokens.
209185
*/
210186
@Override
211-
public final Task<String> getAccessToken(boolean forceRefresh) {
212-
synchronized (accessTokenTaskLock) {
213-
if (refreshRequired(accessTokenTask, forceRefresh)) {
214-
accessTokenTask =
215-
getCertificate(forceRefresh)
216-
.continueWith(
217-
new Continuation<GoogleCredential, FirebaseAccessToken>() {
218-
@Override
219-
public FirebaseAccessToken then(@NonNull Task<GoogleCredential> task)
220-
throws Exception {
221-
return fetchToken(task.getResult());
222-
}
223-
});
224-
}
225-
226-
return accessTokenTask.continueWith(
227-
new Continuation<FirebaseAccessToken, String>() {
228-
@Override
229-
public String then(@NonNull Task<FirebaseAccessToken> task) throws Exception {
230-
return task.getResult().getToken();
231-
}
232-
});
233-
}
187+
public final Task<GoogleOAuthAccessToken> getAccessToken() {
188+
return getCertificate()
189+
.continueWith(new Continuation<GoogleCredential, GoogleOAuthAccessToken>() {
190+
@Override
191+
public GoogleOAuthAccessToken then(@NonNull Task<GoogleCredential> task)
192+
throws Exception {
193+
return fetchToken(task.getResult());
194+
}
195+
});
234196
}
235197
}
236198

@@ -267,9 +229,9 @@ GoogleCredential fetchCredential() throws IOException {
267229
}
268230

269231
@Override
270-
FirebaseAccessToken fetchToken(GoogleCredential credential) throws IOException {
232+
GoogleOAuthAccessToken fetchToken(GoogleCredential credential) throws IOException {
271233
credential.refreshToken();
272-
return new FirebaseAccessToken(credential, clock);
234+
return newAccessToken(credential);
273235
}
274236

275237
Task<String> getProjectId() {
@@ -290,9 +252,9 @@ GoogleCredential fetchCredential() throws IOException {
290252
}
291253

292254
@Override
293-
FirebaseAccessToken fetchToken(GoogleCredential credential) throws IOException {
255+
GoogleOAuthAccessToken fetchToken(GoogleCredential credential) throws IOException {
294256
credential.refreshToken();
295-
return new FirebaseAccessToken(credential, clock);
257+
return newAccessToken(credential);
296258
}
297259
}
298260

@@ -322,9 +284,9 @@ GoogleCredential fetchCredential() throws IOException {
322284
}
323285

324286
@Override
325-
FirebaseAccessToken fetchToken(GoogleCredential credential) throws IOException {
287+
GoogleOAuthAccessToken fetchToken(GoogleCredential credential) throws IOException {
326288
credential.refreshToken();
327-
return new FirebaseAccessToken(credential, clock);
289+
return newAccessToken(credential);
328290
}
329291
}
330292

@@ -334,35 +296,9 @@ private static class DefaultCredentialsHolder {
334296
applicationDefault(Utils.getDefaultTransport(), Utils.getDefaultJsonFactory());
335297
}
336298

337-
static class Clock {
338-
339-
protected long now() {
340-
return System.currentTimeMillis();
341-
}
342-
}
343-
344-
static class FirebaseAccessToken {
345-
346-
private final String token;
347-
private final long expirationTime;
348-
private final Clock clock;
349-
350-
FirebaseAccessToken(GoogleCredential credential, Clock clock) {
351-
checkNotNull(credential, "Google credential is required");
352-
353-
token =
354-
checkNotNull(
355-
credential.getAccessToken(), "Access token should not be null after refresh.");
356-
expirationTime = credential.getExpirationTimeMilliseconds();
357-
this.clock = checkNotNull(clock, "Clock is required");
358-
}
359-
360-
String getToken() {
361-
return token;
362-
}
363-
364-
boolean isExpired() {
365-
return expirationTime < clock.now();
366-
}
299+
static GoogleOAuthAccessToken newAccessToken(GoogleCredential credential) {
300+
checkNotNull(credential);
301+
return new GoogleOAuthAccessToken(credential.getAccessToken(),
302+
credential.getExpirationTimeMilliseconds());
367303
}
368304
}

0 commit comments

Comments
 (0)