Skip to content

Commit 4a3f6f6

Browse files
authored
Refactoring the FirebaseAuth Token Verification (#246)
* Refactoring the token verification logic * Added more tests * Cleaning up the unit tests * Implemented lazy init for token verifiers * Using Supplier interface instead of the custom LazyInitializer * Made the FirebaseAuth more testable * Removed FirebaseIdToken * More unit tests for token verification * Decoupled FirebaseTokenVerifier from IdToken API * Decoupled FirebaseToken from IdToken API * Added some documentation * reordering some methods for clarity * Fixed a comment typo * Minor readability improvements * Added tests to verify default token verifiers in FirebaseAuth * Making args of all op() methods final
1 parent f338383 commit 4a3f6f6

19 files changed

+1746
-1187
lines changed

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

Lines changed: 118 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import com.google.api.core.ApiFuture;
2626
import com.google.common.annotations.VisibleForTesting;
2727
import com.google.common.base.Strings;
28+
import com.google.common.base.Supplier;
29+
import com.google.common.base.Suppliers;
2830
import com.google.firebase.FirebaseApp;
2931
import com.google.firebase.ImplFirebaseTrampolines;
3032
import com.google.firebase.auth.FirebaseUserManager.EmailLinkType;
@@ -34,17 +36,15 @@
3436
import com.google.firebase.auth.UserRecord.CreateRequest;
3537
import com.google.firebase.auth.UserRecord.UpdateRequest;
3638
import com.google.firebase.auth.internal.FirebaseTokenFactory;
37-
import com.google.firebase.auth.internal.FirebaseTokenVerifier;
38-
import com.google.firebase.auth.internal.KeyManagers;
3939
import com.google.firebase.internal.CallableOperation;
4040
import com.google.firebase.internal.FirebaseService;
4141
import com.google.firebase.internal.NonNull;
4242
import com.google.firebase.internal.Nullable;
43+
4344
import java.io.IOException;
4445
import java.util.List;
4546
import java.util.Map;
4647
import java.util.concurrent.atomic.AtomicBoolean;
47-
import java.util.concurrent.atomic.AtomicReference;
4848

4949
/**
5050
* This class is the entry point for all server-side Firebase Authentication actions.
@@ -56,40 +56,27 @@
5656
*/
5757
public class FirebaseAuth {
5858

59+
private static final String SERVICE_ID = FirebaseAuth.class.getName();
60+
5961
private static final String ERROR_CUSTOM_TOKEN = "ERROR_CUSTOM_TOKEN";
60-
private static final String ERROR_INVALID_ID_TOKEN = "ERROR_INVALID_CREDENTIAL";
61-
private static final String ERROR_INVALID_SESSION_COOKIE = "ERROR_INVALID_COOKIE";
6262

63-
private final Clock clock;
63+
private final Object lock = new Object();
64+
private final AtomicBoolean destroyed = new AtomicBoolean(false);
6465

6566
private final FirebaseApp firebaseApp;
66-
private final KeyManagers keyManagers;
67-
private final String projectId;
67+
private final Supplier<FirebaseTokenFactory> tokenFactory;
68+
private final Supplier<? extends FirebaseTokenVerifier> idTokenVerifier;
69+
private final Supplier<? extends FirebaseTokenVerifier> cookieVerifier;
6870
private final JsonFactory jsonFactory;
6971
private final FirebaseUserManager userManager;
70-
private final AtomicReference<FirebaseTokenFactory> tokenFactory;
71-
private final AtomicBoolean destroyed;
72-
private final Object lock;
73-
74-
private FirebaseAuth(FirebaseApp firebaseApp) {
75-
this(firebaseApp, KeyManagers.getDefault(firebaseApp, Clock.SYSTEM), Clock.SYSTEM);
76-
}
7772

78-
/**
79-
* Constructor for injecting a GooglePublicKeysManager, which is used to verify tokens are
80-
* correctly signed. This should only be used for testing to override the default key manager.
81-
*/
82-
@VisibleForTesting
83-
FirebaseAuth(FirebaseApp firebaseApp, KeyManagers keyManagers, Clock clock) {
84-
this.firebaseApp = checkNotNull(firebaseApp);
85-
this.keyManagers = checkNotNull(keyManagers);
86-
this.clock = checkNotNull(clock);
87-
this.projectId = ImplFirebaseTrampolines.getProjectId(firebaseApp);
73+
private FirebaseAuth(Builder builder) {
74+
this.firebaseApp = checkNotNull(builder.firebaseApp);
75+
this.tokenFactory = threadSafeMemoize(builder.tokenFactory);
76+
this.idTokenVerifier = threadSafeMemoize(builder.idTokenVerifier);
77+
this.cookieVerifier = threadSafeMemoize(builder.cookieVerifier);
8878
this.jsonFactory = firebaseApp.getOptions().getJsonFactory();
8979
this.userManager = new FirebaseUserManager(firebaseApp);
90-
this.tokenFactory = new AtomicReference<>(null);
91-
this.destroyed = new AtomicBoolean(false);
92-
this.lock = new Object();
9380
}
9481

9582
/**
@@ -224,40 +211,23 @@ public ApiFuture<FirebaseToken> verifySessionCookieAsync(String cookie, boolean
224211
private CallableOperation<FirebaseToken, FirebaseAuthException> verifySessionCookieOp(
225212
final String cookie, final boolean checkRevoked) {
226213
checkNotDestroyed();
227-
checkState(!Strings.isNullOrEmpty(projectId),
228-
"Must initialize FirebaseApp with a project ID to call verifySessionCookie()");
214+
checkArgument(!Strings.isNullOrEmpty(cookie), "Session cookie must not be null or empty");
215+
final FirebaseTokenVerifier sessionCookieVerifier = getSessionCookieVerifier(checkRevoked);
229216
return new CallableOperation<FirebaseToken, FirebaseAuthException>() {
230217
@Override
231218
public FirebaseToken execute() throws FirebaseAuthException {
232-
FirebaseTokenVerifier firebaseTokenVerifier =
233-
FirebaseTokenVerifier.createSessionCookieVerifier(projectId, keyManagers, clock);
234-
FirebaseToken firebaseToken;
235-
try {
236-
firebaseToken = FirebaseToken.parse(jsonFactory, cookie);
237-
} catch (IOException e) {
238-
throw new FirebaseAuthException(ERROR_INVALID_SESSION_COOKIE,
239-
"Failed to parse cookie", e);
240-
}
241-
// This will throw a FirebaseAuthException with details on how the token is invalid.
242-
firebaseTokenVerifier.verifyTokenAndSignature(firebaseToken.getToken());
243-
244-
if (checkRevoked) {
245-
checkRevoked(firebaseToken, "session cookie",
246-
FirebaseUserManager.SESSION_COOKIE_REVOKED_ERROR);
247-
}
248-
return firebaseToken;
219+
return sessionCookieVerifier.verifyToken(cookie);
249220
}
250221
};
251222
}
252223

253-
private void checkRevoked(
254-
FirebaseToken firebaseToken, String label, String errorCode) throws FirebaseAuthException {
255-
String uid = firebaseToken.getUid();
256-
UserRecord user = userManager.getUserById(uid);
257-
long issuedAt = (long) firebaseToken.getClaims().get("iat");
258-
if (user.getTokensValidAfterTimestamp() > issuedAt * 1000) {
259-
throw new FirebaseAuthException(errorCode, "Firebase " + label + " revoked");
224+
@VisibleForTesting
225+
FirebaseTokenVerifier getSessionCookieVerifier(boolean checkRevoked) {
226+
FirebaseTokenVerifier verifier = cookieVerifier.get();
227+
if (checkRevoked) {
228+
verifier = RevocationCheckDecorator.decorateSessionCookieVerifier(verifier, userManager);
260229
}
230+
return verifier;
261231
}
262232

263233
/**
@@ -355,7 +325,7 @@ private CallableOperation<String, FirebaseAuthException> createCustomTokenOp(
355325
final String uid, final Map<String, Object> developerClaims) {
356326
checkNotDestroyed();
357327
checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty");
358-
final FirebaseTokenFactory tokenFactory = ensureTokenFactory();
328+
final FirebaseTokenFactory tokenFactory = this.tokenFactory.get();
359329
return new CallableOperation<String, FirebaseAuthException>() {
360330
@Override
361331
public String execute() throws FirebaseAuthException {
@@ -369,29 +339,6 @@ public String execute() throws FirebaseAuthException {
369339
};
370340
}
371341

372-
private FirebaseTokenFactory ensureTokenFactory() {
373-
FirebaseTokenFactory result = this.tokenFactory.get();
374-
if (result == null) {
375-
synchronized (lock) {
376-
result = this.tokenFactory.get();
377-
if (result == null) {
378-
try {
379-
result = FirebaseTokenFactory.fromApp(firebaseApp, clock);
380-
this.tokenFactory.set(result);
381-
} catch (IOException e) {
382-
throw new IllegalStateException(
383-
"Failed to initialize FirebaseTokenFactory. Make sure to initialize the SDK "
384-
+ "with service account credentials or specify a service account "
385-
+ "ID with iam.serviceAccounts.signBlob permission. Please refer to "
386-
+ "https://firebase.google.com/docs/auth/admin/create-custom-tokens for more "
387-
+ "details on creating custom tokens.", e);
388-
}
389-
}
390-
}
391-
}
392-
return result;
393-
}
394-
395342
/**
396343
* Parses and verifies a Firebase ID Token.
397344
*
@@ -472,31 +419,24 @@ private CallableOperation<FirebaseToken, FirebaseAuthException> verifyIdTokenOp(
472419
final String token, final boolean checkRevoked) {
473420
checkNotDestroyed();
474421
checkArgument(!Strings.isNullOrEmpty(token), "ID token must not be null or empty");
475-
checkArgument(!Strings.isNullOrEmpty(projectId),
476-
"Must initialize FirebaseApp with a project ID to call verifyIdToken()");
422+
final FirebaseTokenVerifier verifier = getIdTokenVerifier(checkRevoked);
477423
return new CallableOperation<FirebaseToken, FirebaseAuthException>() {
478424
@Override
479425
protected FirebaseToken execute() throws FirebaseAuthException {
480-
FirebaseTokenVerifier firebaseTokenVerifier =
481-
FirebaseTokenVerifier.createIdTokenVerifier(projectId, keyManagers, clock);
482-
FirebaseToken firebaseToken;
483-
try {
484-
firebaseToken = FirebaseToken.parse(jsonFactory, token);
485-
} catch (IOException e) {
486-
throw new FirebaseAuthException(ERROR_INVALID_ID_TOKEN, "Failed to parse token", e);
487-
}
488-
489-
// This will throw a FirebaseAuthException with details on how the token is invalid.
490-
firebaseTokenVerifier.verifyTokenAndSignature(firebaseToken.getToken());
491-
492-
if (checkRevoked) {
493-
checkRevoked(firebaseToken, "auth token", FirebaseUserManager.ID_TOKEN_REVOKED_ERROR);
494-
}
495-
return firebaseToken;
426+
return verifier.verifyToken(token);
496427
}
497428
};
498429
}
499430

431+
@VisibleForTesting
432+
FirebaseTokenVerifier getIdTokenVerifier(boolean checkRevoked) {
433+
FirebaseTokenVerifier verifier = idTokenVerifier.get();
434+
if (checkRevoked) {
435+
verifier = RevocationCheckDecorator.decorateIdTokenVerifier(verifier, userManager);
436+
}
437+
return verifier;
438+
}
439+
500440
/**
501441
* Revokes all refresh tokens for the specified user.
502442
*
@@ -705,13 +645,12 @@ public ApiFuture<ListUsersPage> listUsersAsync(@Nullable String pageToken) {
705645
* @throws IllegalArgumentException If the specified page token is empty, or max results value
706646
* is invalid.
707647
*/
708-
public ApiFuture<ListUsersPage> listUsersAsync(
709-
@Nullable final String pageToken, final int maxResults) {
648+
public ApiFuture<ListUsersPage> listUsersAsync(@Nullable String pageToken, int maxResults) {
710649
return listUsersOp(pageToken, maxResults).callAsync(firebaseApp);
711650
}
712651

713652
private CallableOperation<ListUsersPage, FirebaseAuthException> listUsersOp(
714-
@Nullable String pageToken, int maxResults) {
653+
@Nullable final String pageToken, final int maxResults) {
715654
checkNotDestroyed();
716655
final PageFactory factory = new PageFactory(
717656
new DefaultUserSource(userManager, jsonFactory), maxResults, pageToken);
@@ -874,7 +813,7 @@ public void deleteUser(@NonNull String uid) throws FirebaseAuthException {
874813
* {@link FirebaseAuthException}.
875814
* @throws IllegalArgumentException If the user ID string is null or empty.
876815
*/
877-
public ApiFuture<Void> deleteUserAsync(final String uid) {
816+
public ApiFuture<Void> deleteUserAsync(String uid) {
878817
return deleteUserOp(uid).callAsync(firebaseApp);
879818
}
880819

@@ -959,7 +898,7 @@ public ApiFuture<UserImportResult> importUsersAsync(List<ImportUserRecord> users
959898
}
960899

961900
private CallableOperation<UserImportResult, FirebaseAuthException> importUsersOp(
962-
List<ImportUserRecord> users, UserImportOptions options) {
901+
final List<ImportUserRecord> users, final UserImportOptions options) {
963902
checkNotDestroyed();
964903
final UserImportRequest request = new UserImportRequest(users, options, jsonFactory);
965904
return new CallableOperation<UserImportResult, FirebaseAuthException>() {
@@ -1130,6 +1069,11 @@ public ApiFuture<String> generateSignInWithEmailLinkAsync(
11301069
.callAsync(firebaseApp);
11311070
}
11321071

1072+
@VisibleForTesting
1073+
FirebaseUserManager getUserManager() {
1074+
return this.userManager;
1075+
}
1076+
11331077
private CallableOperation<String, FirebaseAuthException> generateEmailActionLinkOp(
11341078
final EmailLinkType type, final String email, final ActionCodeSettings settings) {
11351079
checkNotDestroyed();
@@ -1145,9 +1089,17 @@ protected String execute() throws FirebaseAuthException {
11451089
};
11461090
}
11471091

1148-
@VisibleForTesting
1149-
FirebaseUserManager getUserManager() {
1150-
return this.userManager;
1092+
private <T> Supplier<T> threadSafeMemoize(final Supplier<T> supplier) {
1093+
checkNotNull(supplier);
1094+
return Suppliers.memoize(new Supplier<T>() {
1095+
@Override
1096+
public T get() {
1097+
synchronized (lock) {
1098+
checkNotDestroyed();
1099+
return supplier.get();
1100+
}
1101+
}
1102+
});
11511103
}
11521104

11531105
private void checkNotDestroyed() {
@@ -1163,12 +1115,72 @@ private void destroy() {
11631115
}
11641116
}
11651117

1166-
private static final String SERVICE_ID = FirebaseAuth.class.getName();
1118+
private static FirebaseAuth fromApp(final FirebaseApp app) {
1119+
return FirebaseAuth.builder()
1120+
.setFirebaseApp(app)
1121+
.setTokenFactory(new Supplier<FirebaseTokenFactory>() {
1122+
@Override
1123+
public FirebaseTokenFactory get() {
1124+
return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM);
1125+
}
1126+
})
1127+
.setIdTokenVerifier(new Supplier<FirebaseTokenVerifier>() {
1128+
@Override
1129+
public FirebaseTokenVerifier get() {
1130+
return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM);
1131+
}
1132+
})
1133+
.setCookieVerifier(new Supplier<FirebaseTokenVerifier>() {
1134+
@Override
1135+
public FirebaseTokenVerifier get() {
1136+
return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM);
1137+
}
1138+
})
1139+
.build();
1140+
}
1141+
1142+
@VisibleForTesting
1143+
static Builder builder() {
1144+
return new Builder();
1145+
}
1146+
1147+
static class Builder {
1148+
private FirebaseApp firebaseApp;
1149+
private Supplier<FirebaseTokenFactory> tokenFactory;
1150+
private Supplier<? extends FirebaseTokenVerifier> idTokenVerifier;
1151+
private Supplier<? extends FirebaseTokenVerifier> cookieVerifier;
1152+
1153+
private Builder() { }
1154+
1155+
Builder setFirebaseApp(FirebaseApp firebaseApp) {
1156+
this.firebaseApp = firebaseApp;
1157+
return this;
1158+
}
1159+
1160+
Builder setTokenFactory(Supplier<FirebaseTokenFactory> tokenFactory) {
1161+
this.tokenFactory = tokenFactory;
1162+
return this;
1163+
}
1164+
1165+
Builder setIdTokenVerifier(Supplier<? extends FirebaseTokenVerifier> idTokenVerifier) {
1166+
this.idTokenVerifier = idTokenVerifier;
1167+
return this;
1168+
}
1169+
1170+
Builder setCookieVerifier(Supplier<? extends FirebaseTokenVerifier> cookieVerifier) {
1171+
this.cookieVerifier = cookieVerifier;
1172+
return this;
1173+
}
1174+
1175+
FirebaseAuth build() {
1176+
return new FirebaseAuth(this);
1177+
}
1178+
}
11671179

11681180
private static class FirebaseAuthService extends FirebaseService<FirebaseAuth> {
11691181

11701182
FirebaseAuthService(FirebaseApp app) {
1171-
super(SERVICE_ID, new FirebaseAuth(app));
1183+
super(SERVICE_ID, FirebaseAuth.fromApp(app));
11721184
}
11731185

11741186
@Override

0 commit comments

Comments
 (0)