Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 62 additions & 37 deletions src/main/java/com/google/firebase/FirebaseApp.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.io.BaseEncoding;
import com.google.firebase.auth.GoogleOAuthAccessToken;
import com.google.firebase.internal.AuthStateListener;
import com.google.firebase.internal.FirebaseAppStore;
import com.google.firebase.internal.FirebaseExecutors;
Expand Down Expand Up @@ -67,8 +68,10 @@ public class FirebaseApp {

public static final String DEFAULT_APP_NAME = "[DEFAULT]";
private static final long TOKEN_REFRESH_INTERVAL_MILLIS = TimeUnit.MINUTES.toMillis(55);
private static final TokenRefresher.Factory DEFAULT_TOKEN_REFRESHER_FACTORY =

static final TokenRefresher.Factory DEFAULT_TOKEN_REFRESHER_FACTORY =
new TokenRefresher.Factory();
static final Clock DEFAULT_CLOCK = new Clock();

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

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

private Task<GoogleOAuthAccessToken> previousTokenTask;

/**
* Per application lock for synchronizing all internal FirebaseApp state changes.
*/
private final Object lock = new Object();

/** Default constructor. */
private FirebaseApp(String name, FirebaseOptions options, TokenRefresher.Factory factory) {
private FirebaseApp(String name, FirebaseOptions options,
TokenRefresher.Factory factory, Clock clock) {
checkArgument(!Strings.isNullOrEmpty(name));
this.name = name;
this.options = checkNotNull(options);
tokenRefresher = checkNotNull(factory).create(this);
this.tokenRefresher = checkNotNull(factory).create(this);
this.clock = checkNotNull(clock);
}

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

static FirebaseApp initializeApp(
FirebaseOptions options, String name, TokenRefresher.Factory tokenRefresherFactory) {
static FirebaseApp initializeApp(FirebaseOptions options, String name,
TokenRefresher.Factory tokenRefresherFactory, Clock clock) {
FirebaseAppStore appStore = FirebaseAppStore.initialize();
String normalizedName = normalize(name);
final FirebaseApp firebaseApp;
Expand All @@ -181,7 +189,7 @@ static FirebaseApp initializeApp(
!instances.containsKey(normalizedName),
"FirebaseApp name " + normalizedName + " already exists!");

firebaseApp = new FirebaseApp(normalizedName, options, tokenRefresherFactory);
firebaseApp = new FirebaseApp(normalizedName, options, tokenRefresherFactory, clock);
instances.put(normalizedName, firebaseApp);
}

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

private boolean refreshRequired(
@NonNull Task<GoogleOAuthAccessToken> previousTask, boolean forceRefresh) {
return (previousTask.isComplete()
&& (forceRefresh || !previousTask.isSuccessful()
|| previousTask.getResult().getExpiryTime() <= clock.now()));
}

/**
* Internal-only method to fetch a valid Service Account OAuth2 Token.
*
Expand All @@ -313,41 +328,45 @@ private void checkNotDeleted() {
* @return a {@link Task}
*/
Task<GetTokenResult> getToken(boolean forceRefresh) {
checkNotDeleted();
return options
.getCredential()
.getAccessToken(forceRefresh)
.continueWith(
new Continuation<String, GetTokenResult>() {
@Override
public GetTokenResult then(@NonNull Task<String> task) throws Exception {
GetTokenResult newToken = new GetTokenResult(task.getResult());
GetTokenResult oldToken = currentToken.get();
List<AuthStateListener> listenersCopy = null;
if (!newToken.equals(oldToken)) {
synchronized (lock) {
if (deleted.get()) {
return newToken;
}

// Grab the lock before compareAndSet to avoid a potential race
// condition with addAuthStateListener. The same lock also ensures serial
// access to the token refresher.
if (currentToken.compareAndSet(oldToken, newToken)) {
listenersCopy = ImmutableList.copyOf(authStateListeners);
tokenRefresher.scheduleRefresh(TOKEN_REFRESH_INTERVAL_MILLIS);
}
synchronized (lock) {
checkNotDeleted();
if (previousTokenTask == null || refreshRequired(previousTokenTask, forceRefresh)) {
previousTokenTask = options.getCredential().getAccessToken();
}

return previousTokenTask.continueWith(
new Continuation<GoogleOAuthAccessToken, GetTokenResult>() {
@Override
public GetTokenResult then(@NonNull Task<GoogleOAuthAccessToken> task)
throws Exception {
GetTokenResult newToken = new GetTokenResult(task.getResult().getAccessToken());
GetTokenResult oldToken = currentToken.get();
List<AuthStateListener> listenersCopy = null;
if (!newToken.equals(oldToken)) {
synchronized (lock) {
if (deleted.get()) {
return newToken;
}
}

if (listenersCopy != null) {
for (AuthStateListener listener : listenersCopy) {
listener.onAuthStateChanged(newToken);
// Grab the lock before compareAndSet to avoid a potential race
// condition with addAuthStateListener. The same lock also ensures serial
// access to the token refresher.
if (currentToken.compareAndSet(oldToken, newToken)) {
listenersCopy = ImmutableList.copyOf(authStateListeners);
tokenRefresher.scheduleRefresh(TOKEN_REFRESH_INTERVAL_MILLIS);
}
}
return newToken;
}
});

if (listenersCopy != null) {
for (AuthStateListener listener : listenersCopy) {
listener.onAuthStateChanged(newToken);
}
}
return newToken;
}
});
}
}

boolean isDefaultApp() {
Expand Down Expand Up @@ -451,4 +470,10 @@ TokenRefresher create(FirebaseApp app) {
}
}
}

static class Clock {
long now() {
return System.currentTimeMillis();
}
}
}
2 changes: 1 addition & 1 deletion src/main/java/com/google/firebase/auth/FirebaseAuth.java
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ public Task<String> createCustomToken(
}

return ((FirebaseCredentials.CertCredential) credential)
.getCertificate(false)
.getCertificate()
.continueWith(
new Continuation<GoogleCredential, String>() {
@Override
Expand Down
10 changes: 6 additions & 4 deletions src/main/java/com/google/firebase/auth/FirebaseCredential.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@
public interface FirebaseCredential {

/**
* Returns a Google OAuth2 access token used to authenticate with Firebase services.
* Returns a Google OAuth2 access token which can be used to authenticate with Firebase services.
* This method does not cache tokens, and therefore each invocation will fetch a fresh token.
* The caller is expected to implement caching by referencing the token expiry details
* available in the returned GoogleOAuthAccessToken instance.
*
* @param forceRefresh Whether to fetch a new token or use a cached one if available.
* @return A {@link Task} providing an access token.
* @return A {@link Task} providing a Google OAuth access token.
*/
Task<String> getAccessToken(boolean forceRefresh);
Task<GoogleOAuthAccessToken> getAccessToken();
}
114 changes: 25 additions & 89 deletions src/main/java/com/google/firebase/auth/FirebaseCredentials.java
Original file line number Diff line number Diff line change
Expand Up @@ -142,39 +142,24 @@ abstract static class BaseCredential implements FirebaseCredential {

final HttpTransport transport;
final JsonFactory jsonFactory;
final Clock clock;
private final Object accessTokenTaskLock = new Object();
private GoogleCredential googleCredential;
private Task<FirebaseAccessToken> accessTokenTask;

BaseCredential(HttpTransport transport, JsonFactory jsonFactory) {
this(transport, jsonFactory, new Clock());
}

BaseCredential(HttpTransport transport, JsonFactory jsonFactory, Clock clock) {
this.transport = checkNotNull(transport, "HttpTransport must not be null");
this.jsonFactory = checkNotNull(jsonFactory, "JsonFactory must not be null");
this.clock = checkNotNull(clock, "Clock must not be null");
}

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

/** Retrieves an access token from a GoogleCredential. Should not use caching. */
abstract FirebaseAccessToken fetchToken(GoogleCredential credential) throws IOException;

/**
* Returns the associated GoogleCredential for this class. This implementation is cached by
* default.
*
* @param forceRefresh Whether to fetch from cache
*/
final Task<GoogleCredential> getCertificate(boolean forceRefresh) {
if (!forceRefresh) {
synchronized (this) {
if (googleCredential != null) {
return Tasks.forResult(googleCredential);
}
final Task<GoogleCredential> getCertificate() {
synchronized (this) {
if (googleCredential != null) {
return Tasks.forResult(googleCredential);
}
}

Expand All @@ -193,44 +178,21 @@ public GoogleCredential call() throws Exception {
});
}

private boolean refreshRequired(
@NonNull Task<FirebaseAccessToken> previousTask, boolean forceRefresh) {
return previousTask == null
|| (previousTask.isComplete()
&& (forceRefresh
|| !previousTask.isSuccessful()
|| previousTask.getResult().isExpired()));
}
abstract GoogleOAuthAccessToken fetchToken(GoogleCredential credential) throws IOException;

/**
* Returns an access token for this credential. This implementation is cached by default.
*
* @param forceRefresh Whether or not to force an access token refresh
* Returns an access token for this credential. Does not cache tokens.
*/
@Override
public final Task<String> getAccessToken(boolean forceRefresh) {
synchronized (accessTokenTaskLock) {
if (refreshRequired(accessTokenTask, forceRefresh)) {
accessTokenTask =
getCertificate(forceRefresh)
.continueWith(
new Continuation<GoogleCredential, FirebaseAccessToken>() {
@Override
public FirebaseAccessToken then(@NonNull Task<GoogleCredential> task)
throws Exception {
return fetchToken(task.getResult());
}
});
}

return accessTokenTask.continueWith(
new Continuation<FirebaseAccessToken, String>() {
@Override
public String then(@NonNull Task<FirebaseAccessToken> task) throws Exception {
return task.getResult().getToken();
}
});
}
public final Task<GoogleOAuthAccessToken> getAccessToken() {
return getCertificate()
.continueWith(new Continuation<GoogleCredential, GoogleOAuthAccessToken>() {
@Override
public GoogleOAuthAccessToken then(@NonNull Task<GoogleCredential> task)
throws Exception {
return fetchToken(task.getResult());
}
});
}
}

Expand Down Expand Up @@ -267,9 +229,9 @@ GoogleCredential fetchCredential() throws IOException {
}

@Override
FirebaseAccessToken fetchToken(GoogleCredential credential) throws IOException {
GoogleOAuthAccessToken fetchToken(GoogleCredential credential) throws IOException {
credential.refreshToken();
return new FirebaseAccessToken(credential, clock);
return newAccessToken(credential);
}

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

@Override
FirebaseAccessToken fetchToken(GoogleCredential credential) throws IOException {
GoogleOAuthAccessToken fetchToken(GoogleCredential credential) throws IOException {
credential.refreshToken();
return new FirebaseAccessToken(credential, clock);
return newAccessToken(credential);
}
}

Expand Down Expand Up @@ -322,9 +284,9 @@ GoogleCredential fetchCredential() throws IOException {
}

@Override
FirebaseAccessToken fetchToken(GoogleCredential credential) throws IOException {
GoogleOAuthAccessToken fetchToken(GoogleCredential credential) throws IOException {
credential.refreshToken();
return new FirebaseAccessToken(credential, clock);
return newAccessToken(credential);
}
}

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

static class Clock {

protected long now() {
return System.currentTimeMillis();
}
}

static class FirebaseAccessToken {

private final String token;
private final long expirationTime;
private final Clock clock;

FirebaseAccessToken(GoogleCredential credential, Clock clock) {
checkNotNull(credential, "Google credential is required");

token =
checkNotNull(
credential.getAccessToken(), "Access token should not be null after refresh.");
expirationTime = credential.getExpirationTimeMilliseconds();
this.clock = checkNotNull(clock, "Clock is required");
}

String getToken() {
return token;
}

boolean isExpired() {
return expirationTime < clock.now();
}
static GoogleOAuthAccessToken newAccessToken(GoogleCredential credential) {
checkNotNull(credential);
return new GoogleOAuthAccessToken(credential.getAccessToken(),
credential.getExpirationTimeMilliseconds());
}
}
Loading