Skip to content

Commit 2d50628

Browse files
authored
fix(auth): check if the user is disabled on checkRevoked=true for verifyIdToken and verifySessionCookie (#585)
* fix(auth): check if the user is disabled on checkRevoked=true for verifyIdToken and verifySessionCookie * fix: review comments
1 parent b071b03 commit 2d50628

File tree

5 files changed

+184
-12
lines changed

5 files changed

+184
-12
lines changed

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,11 +256,13 @@ public FirebaseToken verifyIdToken(@NonNull String idToken) throws FirebaseAuthE
256256
* API call.
257257
*
258258
* @param idToken A Firebase ID token string to parse and verify.
259-
* @param checkRevoked A boolean denoting whether to check if the tokens were revoked.
259+
* @param checkRevoked A boolean denoting whether to check if the tokens were revoked or if
260+
* the user is disabled.
260261
* @return A {@link FirebaseToken} representing the verified and decoded token.
261262
* @throws IllegalArgumentException If the token is null, empty, or if the {@link FirebaseApp}
262263
* instance does not have a project ID associated with it.
263-
* @throws FirebaseAuthException If an error occurs while parsing or validating the token.
264+
* @throws FirebaseAuthException If an error occurs while parsing or validating the token, or if
265+
* the user is disabled.
264266
*/
265267
public FirebaseToken verifyIdToken(@NonNull String idToken, boolean checkRevoked)
266268
throws FirebaseAuthException {
@@ -343,8 +345,11 @@ public FirebaseToken verifySessionCookie(String cookie) throws FirebaseAuthExcep
343345
* checkRevoked} is true, throws a {@link FirebaseAuthException}.
344346
*
345347
* @param cookie A Firebase session cookie string to verify and parse.
346-
* @param checkRevoked A boolean indicating whether to check if the cookie was explicitly revoked.
348+
* @param checkRevoked A boolean indicating whether to check if the cookie was explicitly revoked
349+
* or if the user is disabled.
347350
* @return A {@link FirebaseToken} representing the verified and decoded cookie.
351+
* @throws FirebaseAuthException If an error occurs while parsing or validating the token, or if
352+
* the user is disabled.
348353
*/
349354
public FirebaseToken verifySessionCookie(String cookie, boolean checkRevoked)
350355
throws FirebaseAuthException {

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,9 @@ public enum AuthErrorCode {
108108
* No user record found for the given identifier.
109109
*/
110110
USER_NOT_FOUND,
111+
112+
/**
113+
* The user record is disabled.
114+
*/
115+
USER_DISABLED,
111116
}

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

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,22 +51,28 @@ private RevocationCheckDecorator(
5151
@Override
5252
public FirebaseToken verifyToken(String token) throws FirebaseAuthException {
5353
FirebaseToken firebaseToken = tokenVerifier.verifyToken(token);
54-
if (isRevoked(firebaseToken)) {
54+
validateDisabledOrRevoked(firebaseToken);
55+
return firebaseToken;
56+
}
57+
58+
private void validateDisabledOrRevoked(FirebaseToken firebaseToken) throws FirebaseAuthException {
59+
UserRecord user = userManager.getUserById(firebaseToken.getUid());
60+
if (user.isDisabled()) {
61+
throw new FirebaseAuthException(ErrorCode.INVALID_ARGUMENT,
62+
"The user record is disabled.",
63+
/* cause= */ null,
64+
/* response= */ null,
65+
AuthErrorCode.USER_DISABLED);
66+
}
67+
long issuedAtInSeconds = (long) firebaseToken.getClaims().get("iat");
68+
if (user.getTokensValidAfterTimestamp() > issuedAtInSeconds * 1000) {
5569
throw new FirebaseAuthException(
5670
ErrorCode.INVALID_ARGUMENT,
5771
"Firebase " + shortName + " is revoked.",
5872
null,
5973
null,
6074
errorCode);
6175
}
62-
63-
return firebaseToken;
64-
}
65-
66-
private boolean isRevoked(FirebaseToken firebaseToken) throws FirebaseAuthException {
67-
UserRecord user = userManager.getUserById(firebaseToken.getUid());
68-
long issuedAtInSeconds = (long) firebaseToken.getClaims().get("iat");
69-
return user.getTokensValidAfterTimestamp() > issuedAtInSeconds * 1000;
7076
}
7177

7278
static RevocationCheckDecorator decorateIdTokenVerifier(

src/test/java/com/google/firebase/auth/FirebaseAuthIT.java

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,46 @@ public void testVerifyIdToken() throws Exception {
625625
auth.deleteUserAsync("user2");
626626
}
627627

628+
@Test
629+
public void testVerifyIdTokenUserDisabled() throws Exception {
630+
RandomUser user = UserTestUtils.generateRandomUserInfo();
631+
String customToken = auth.createCustomToken(user.getUid());
632+
String idToken = signInWithCustomToken(customToken);
633+
634+
temporaryUser.registerUid(user.getUid());
635+
636+
// User is not disabled, this should not throw an exception.
637+
FirebaseToken decoded = auth.verifyIdToken(idToken, /* checkRevoked= */true);
638+
assertEquals(user.getUid(), decoded.getUid());
639+
640+
// Disable the user record.
641+
auth.updateUser(new UserRecord.UpdateRequest(user.getUid()).setDisabled(true));
642+
643+
// Verify the ID token without checking revocation. This should not throw an exception.
644+
decoded = auth.verifyIdToken(idToken);
645+
assertEquals(user.getUid(), decoded.getUid());
646+
647+
// Verify the ID token while checking revocation. This should throw an exception.
648+
try {
649+
auth.verifyIdToken(idToken, /* checkRevoked= */true);
650+
fail("Should throw a FirebaseAuthException since the user is disabled.");
651+
} catch (FirebaseAuthException e) {
652+
assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode());
653+
assertEquals(AuthErrorCode.USER_DISABLED, e.getAuthErrorCode());
654+
assertEquals("The user record is disabled.", e.getMessage());
655+
}
656+
657+
// Revoke the tokens for the user. The USER_DISABLED should take precedence over
658+
// the revocation error.
659+
auth.revokeRefreshTokens(user.getUid());
660+
try {
661+
auth.verifyIdToken(idToken, /* checkRevoked= */ true);
662+
fail("Should throw an exception as the ID tokens are revoked.");
663+
} catch (FirebaseAuthException e) {
664+
assertEquals(AuthErrorCode.USER_DISABLED, e.getAuthErrorCode());
665+
}
666+
}
667+
628668
@Test
629669
public void testVerifySessionCookie() throws Exception {
630670
String customToken = auth.createCustomTokenAsync("user3").get();
@@ -661,6 +701,52 @@ public void testVerifySessionCookie() throws Exception {
661701
auth.deleteUserAsync("user3");
662702
}
663703

704+
@Test
705+
public void testVerifySessionCookieUserDisabled() throws Exception {
706+
RandomUser user = UserTestUtils.generateRandomUserInfo();
707+
String customToken = auth.createCustomToken(user.getUid());
708+
String idToken = signInWithCustomToken(customToken);
709+
710+
temporaryUser.registerUid(user.getUid());
711+
712+
SessionCookieOptions options = SessionCookieOptions.builder()
713+
.setExpiresIn(TimeUnit.HOURS.toMillis(1))
714+
.build();
715+
String sessionCookie = auth.createSessionCookieAsync(idToken, options).get();
716+
assertFalse(Strings.isNullOrEmpty(sessionCookie));
717+
718+
// User is not disabled, this should not throw an exception.
719+
FirebaseToken decoded = auth.verifySessionCookie(sessionCookie, /* checkRevoked= */true);
720+
assertEquals(user.getUid(), decoded.getUid());
721+
722+
// Disable the user record.
723+
auth.updateUser(new UserRecord.UpdateRequest(user.getUid()).setDisabled(true));
724+
725+
// Verify the session cookie without checking revocation. This should not throw an exception.
726+
decoded = auth.verifySessionCookie(sessionCookie);
727+
assertEquals(user.getUid(), decoded.getUid());
728+
729+
// Verify the session cookie while checking revocation. This should throw an exception.
730+
try {
731+
auth.verifySessionCookie(sessionCookie, /* checkRevoked= */true);
732+
fail("Should throw a FirebaseAuthException since the user is disabled.");
733+
} catch (FirebaseAuthException e) {
734+
assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode());
735+
assertEquals(AuthErrorCode.USER_DISABLED, e.getAuthErrorCode());
736+
assertEquals("The user record is disabled.", e.getMessage());
737+
}
738+
739+
// Revoke the tokens for the user. The USER_DISABLED should take precedence over
740+
// the revocation error.
741+
auth.revokeRefreshTokens(user.getUid());
742+
try {
743+
auth.verifySessionCookie(sessionCookie, /* checkRevoked= */ true);
744+
fail("Should throw an exception as the tokens are revoked.");
745+
} catch (FirebaseAuthException e) {
746+
assertEquals(AuthErrorCode.USER_DISABLED, e.getAuthErrorCode());
747+
}
748+
}
749+
664750
@Test
665751
public void testCustomTokenWithClaims() throws Exception {
666752
Map<String, Object> devClaims = ImmutableMap.<String, Object>of(

src/test/java/com/google/firebase/auth/FirebaseAuthTest.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import static org.junit.Assert.assertTrue;
2626
import static org.junit.Assert.fail;
2727

28+
import com.google.api.client.json.GenericJson;
29+
import com.google.api.client.json.JsonParser;
2830
import com.google.api.client.testing.http.MockHttpTransport;
2931
import com.google.api.client.testing.http.MockLowLevelHttpResponse;
3032
import com.google.api.core.ApiFuture;
@@ -36,10 +38,15 @@
3638
import com.google.firebase.FirebaseApp;
3739
import com.google.firebase.FirebaseOptions;
3840
import com.google.firebase.TestOnlyImplFirebaseTrampolines;
41+
import com.google.firebase.internal.ApiClientUtils;
3942
import com.google.firebase.internal.FirebaseProcessEnvironment;
4043
import com.google.firebase.testing.ServiceAccount;
4144
import com.google.firebase.testing.TestResponseInterceptor;
4245
import com.google.firebase.testing.TestUtils;
46+
47+
import java.io.IOException;
48+
import java.util.ArrayList;
49+
import java.util.Map;
4350
import java.util.concurrent.ExecutionException;
4451
import java.util.concurrent.TimeUnit;
4552
import java.util.concurrent.TimeoutException;
@@ -246,6 +253,22 @@ public void testVerifyIdTokenWithRevocationCheck() throws Exception {
246253
assertEquals("idtoken", tokenVerifier.getLastTokenString());
247254
}
248255

256+
@Test
257+
public void testVerifyIdTokenWithRevocationCheckAndUserDisabled() throws Exception {
258+
MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(
259+
getFirebaseToken(VALID_SINCE + 1000));
260+
FirebaseAuth auth =
261+
getAuthForIdTokenVerificationWithRevocationCheckWithDisabledUser(tokenVerifier);
262+
try {
263+
auth.verifyIdToken("idtoken", true);
264+
fail("No exception thrown for disabled user.");
265+
} catch (FirebaseAuthException e) {
266+
assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode());
267+
assertEquals(AuthErrorCode.USER_DISABLED, e.getAuthErrorCode());
268+
assertEquals("The user record is disabled.", e.getMessage());
269+
}
270+
}
271+
249272
@Test
250273
public void testVerifyIdTokenWithRevocationCheckFailure() {
251274
MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(
@@ -444,6 +467,22 @@ public void testVerifySessionCookieWithRevocationCheck() throws Exception {
444467
assertEquals("cookie", tokenVerifier.getLastTokenString());
445468
}
446469

470+
@Test
471+
public void testVerifySessionCookieWithRevocationCheckAndUserDisabled() throws Exception {
472+
MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(
473+
getFirebaseToken(VALID_SINCE + 1000));
474+
FirebaseAuth auth =
475+
getAuthForSessionCookieVerificationWithRevocationCheckAndUserDisabled(tokenVerifier);
476+
try {
477+
auth.verifySessionCookie("cookie", true);
478+
fail("No exception thrown for disabled user.");
479+
} catch (FirebaseAuthException e) {
480+
assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode());
481+
assertEquals(AuthErrorCode.USER_DISABLED, e.getAuthErrorCode());
482+
assertEquals("The user record is disabled.", e.getMessage());
483+
}
484+
}
485+
447486
@Test
448487
public void testVerifySessionCookieWithRevocationCheckFailure() {
449488
MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(
@@ -513,6 +552,12 @@ FirebaseAuth getAuthForIdTokenVerificationWithRevocationCheck(
513552
return getAuthForIdTokenVerification(app, Suppliers.ofInstance(tokenVerifier));
514553
}
515554

555+
FirebaseAuth getAuthForIdTokenVerificationWithRevocationCheckWithDisabledUser(
556+
FirebaseTokenVerifier tokenVerifier) throws IOException {
557+
FirebaseApp app = getFirebaseAppForDisabledUserRetrieval();
558+
return getAuthForIdTokenVerification(app, Suppliers.ofInstance(tokenVerifier));
559+
}
560+
516561
private FirebaseAuth getAuthForIdTokenVerification(FirebaseTokenVerifier tokenVerifier) {
517562
return getAuthForIdTokenVerification(Suppliers.ofInstance(tokenVerifier));
518563
}
@@ -540,6 +585,12 @@ FirebaseAuth getAuthForSessionCookieVerificationWithRevocationCheck(
540585
return getAuthForSessionCookieVerification(app, Suppliers.ofInstance(tokenVerifier));
541586
}
542587

588+
FirebaseAuth getAuthForSessionCookieVerificationWithRevocationCheckAndUserDisabled(
589+
FirebaseTokenVerifier tokenVerifier) throws IOException {
590+
FirebaseApp app = getFirebaseAppForDisabledUserRetrieval();
591+
return getAuthForSessionCookieVerification(app, Suppliers.ofInstance(tokenVerifier));
592+
}
593+
543594
private FirebaseAuth getAuthForSessionCookieVerification(FirebaseTokenVerifier tokenVerifier) {
544595
return getAuthForSessionCookieVerification(Suppliers.ofInstance(tokenVerifier));
545596
}
@@ -573,6 +624,25 @@ private FirebaseApp getFirebaseAppForUserRetrieval() {
573624
.build());
574625
}
575626

627+
private FirebaseApp getFirebaseAppForDisabledUserRetrieval() throws IOException {
628+
String getUserResponse = TestUtils.loadResource("getUser.json");
629+
JsonParser parser = ApiClientUtils.getDefaultJsonFactory().createJsonParser(getUserResponse);
630+
GenericJson json =
631+
parser.parseAndClose(GenericJson.class);
632+
Map<String, Object> users =
633+
((ArrayList<Map<String, Object>>) json.get("users")).get(0);
634+
users.put("disabled", true);
635+
636+
MockHttpTransport transport = new MockHttpTransport.Builder()
637+
.setLowLevelHttpResponse(new MockLowLevelHttpResponse().setContent(json.toString()))
638+
.build();
639+
return FirebaseApp.initializeApp(FirebaseOptions.builder()
640+
.setCredentials(new MockGoogleCredentials("test-token"))
641+
.setHttpTransport(transport)
642+
.setProjectId("test-project-id")
643+
.build());
644+
}
645+
576646
public static TestResponseInterceptor setUserManager(
577647
AbstractFirebaseAuth.Builder<?> builder, FirebaseApp app, String tenantId) {
578648
TestResponseInterceptor interceptor = new TestResponseInterceptor();

0 commit comments

Comments
 (0)