diff --git a/src/main/java/com/google/firebase/auth/AuthErrorCode.java b/src/main/java/com/google/firebase/auth/AuthErrorCode.java index 83287d712..463c24318 100644 --- a/src/main/java/com/google/firebase/auth/AuthErrorCode.java +++ b/src/main/java/com/google/firebase/auth/AuthErrorCode.java @@ -21,21 +21,56 @@ */ public enum AuthErrorCode { + /** + * Failed to retrieve public key certificates required to verify JWTs. + */ + CERTIFICATE_FETCH_FAILED, + /** * A user already exists with the provided email. */ EMAIL_ALREADY_EXISTS, + /** + * The specified ID token is expired. + */ + EXPIRED_ID_TOKEN, + + /** + * The specified session cookie is expired. + */ + EXPIRED_SESSION_COOKIE, + /** * The provided dynamic link domain is not configured or authorized for the current project. */ INVALID_DYNAMIC_LINK_DOMAIN, + /** + * The specified ID token is invalid. + */ + INVALID_ID_TOKEN, + + /** + * The specified session cookie is invalid. + */ + INVALID_SESSION_COOKIE, + /** * A user already exists with the provided phone number. */ PHONE_NUMBER_ALREADY_EXISTS, + /** + * The specified ID token has been revoked. + */ + REVOKED_ID_TOKEN, + + /** + * The specified session cookie has been revoked. + */ + REVOKED_SESSION_COOKIE, + /** * A user already exists with the provided UID. */ diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuthException.java b/src/main/java/com/google/firebase/auth/FirebaseAuthException.java index bd7f60e7f..f50bc1ff0 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuthException.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuthException.java @@ -32,7 +32,6 @@ public class FirebaseAuthException extends FirebaseException { private final AuthErrorCode errorCode; - private final String deprecatedErrorCode; FirebaseAuthException( @NonNull ErrorCode errorCode, @@ -42,12 +41,6 @@ public class FirebaseAuthException extends FirebaseException { AuthErrorCode authErrorCode) { super(errorCode, message, cause, response); this.errorCode = authErrorCode; - this.deprecatedErrorCode = null; - } - - @Deprecated - public FirebaseAuthException(@NonNull String errorCode, @NonNull String detailMessage) { - this(errorCode, detailMessage, null); } @Deprecated @@ -56,17 +49,10 @@ public FirebaseAuthException( super(detailMessage, throwable); checkArgument(!Strings.isNullOrEmpty(errorCode)); this.errorCode = null; - this.deprecatedErrorCode = errorCode; } @Nullable public AuthErrorCode getAuthErrorCode() { return errorCode; } - - /** Returns an error code that may provide more information about the error. */ - @Deprecated - public String getDeprecatedErrorCode() { - return deprecatedErrorCode; - } } diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java index dbb562872..cff1a6cfd 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java +++ b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java @@ -82,6 +82,8 @@ static FirebaseTokenVerifierImpl createIdTokenVerifier(FirebaseApp app, Clock cl .setJsonFactory(app.getOptions().getJsonFactory()) .setPublicKeysManager(publicKeysManager) .setIdTokenVerifier(idTokenVerifier) + .setInvalidTokenErrorCode(AuthErrorCode.INVALID_ID_TOKEN) + .setExpiredTokenErrorCode(AuthErrorCode.EXPIRED_ID_TOKEN) .build(); } @@ -100,6 +102,8 @@ static FirebaseTokenVerifierImpl createSessionCookieVerifier(FirebaseApp app, Cl .setShortName("session cookie") .setMethod("verifySessionCookie()") .setDocUrl("https://firebase.google.com/docs/auth/admin/manage-cookies") + .setInvalidTokenErrorCode(AuthErrorCode.INVALID_SESSION_COOKIE) + .setExpiredTokenErrorCode(AuthErrorCode.EXPIRED_SESSION_COOKIE) .build(); } diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java index c164173a6..b34cfb516 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java +++ b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java @@ -28,10 +28,12 @@ import com.google.api.client.util.ArrayMap; import com.google.common.base.Joiner; import com.google.common.base.Strings; +import com.google.firebase.ErrorCode; import java.io.IOException; import java.math.BigDecimal; import java.security.GeneralSecurityException; import java.security.PublicKey; +import java.util.List; /** * The default implementation of the {@link FirebaseTokenVerifier} interface. Uses the Google API @@ -43,8 +45,6 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier { private static final String RS256 = "RS256"; private static final String FIREBASE_AUDIENCE = "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit"; - private static final String ERROR_INVALID_CREDENTIAL = "ERROR_INVALID_CREDENTIAL"; - private static final String ERROR_RUNTIME_EXCEPTION = "ERROR_RUNTIME_EXCEPTION"; private final JsonFactory jsonFactory; private final GooglePublicKeysManager publicKeysManager; @@ -53,6 +53,8 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier { private final String shortName; private final String articledShortName; private final String docUrl; + private final AuthErrorCode invalidTokenErrorCode; + private final AuthErrorCode expiredTokenErrorCode; private FirebaseTokenVerifierImpl(Builder builder) { this.jsonFactory = checkNotNull(builder.jsonFactory); @@ -65,6 +67,8 @@ private FirebaseTokenVerifierImpl(Builder builder) { this.shortName = builder.shortName; this.articledShortName = prefixWithIndefiniteArticle(this.shortName); this.docUrl = builder.docUrl; + this.invalidTokenErrorCode = checkNotNull(builder.invalidTokenErrorCode); + this.expiredTokenErrorCode = checkNotNull(builder.expiredTokenErrorCode); } /** @@ -137,38 +141,28 @@ private IdToken parse(String token) throws FirebaseAuthException { shortName, docUrl, articledShortName); - throw new FirebaseAuthException(ERROR_INVALID_CREDENTIAL, detailedError, e); - } - } - - private void checkContents(final IdToken token) throws FirebaseAuthException { - String errorMessage = getErrorIfContentInvalid(token); - if (errorMessage != null) { - String detailedError = String.format("%s %s", errorMessage, getVerifyTokenMessage()); - throw new FirebaseAuthException(ERROR_INVALID_CREDENTIAL, detailedError); + throw newException(detailedError, invalidTokenErrorCode, e); } } private void checkSignature(IdToken token) throws FirebaseAuthException { - try { - if (!isSignatureValid(token)) { - throw new FirebaseAuthException(ERROR_INVALID_CREDENTIAL, - String.format( - "Failed to verify the signature of Firebase %s. %s", - shortName, - getVerifyTokenMessage())); - } - } catch (GeneralSecurityException | IOException e) { - throw new FirebaseAuthException( - ERROR_RUNTIME_EXCEPTION, "Error while verifying signature.", e); + if (!isSignatureValid(token)) { + String message = String.format( + "Failed to verify the signature of Firebase %s. %s", + shortName, + getVerifyTokenMessage()); + throw newException(message, invalidTokenErrorCode); } } - private String getErrorIfContentInvalid(final IdToken idToken) { + private void checkContents(final IdToken idToken) throws FirebaseAuthException { final Header header = idToken.getHeader(); final Payload payload = idToken.getPayload(); + final long currentTimeMillis = idTokenVerifier.getClock().currentTimeMillis(); String errorMessage = null; + AuthErrorCode errorCode = invalidTokenErrorCode; + if (header.getKeyId() == null) { errorMessage = getErrorForTokenWithoutKid(header, payload); } else if (!RS256.equals(header.getAlgorithm())) { @@ -203,14 +197,35 @@ private String getErrorIfContentInvalid(final IdToken idToken) { errorMessage = String.format( "Firebase %s has \"sub\" (subject) claim longer than 128 characters.", shortName); - } else if (!verifyTimestamps(idToken)) { + } else if (!idToken.verifyExpirationTime( + currentTimeMillis, idTokenVerifier.getAcceptableTimeSkewSeconds())) { errorMessage = String.format( - "Firebase %s has expired or is not yet valid. Get a fresh %s and try again.", + "Firebase %s has expired. Get a fresh %s and try again.", shortName, shortName); + // Also set the expired error code. + errorCode = expiredTokenErrorCode; + } else if (!idToken.verifyIssuedAtTime( + currentTimeMillis, idTokenVerifier.getAcceptableTimeSkewSeconds())) { + errorMessage = String.format( + "Firebase %s is not yet valid.", + shortName); + } + + if (errorMessage != null) { + String detailedError = String.format("%s %s", errorMessage, getVerifyTokenMessage()); + throw newException(detailedError, errorCode); } + } + + private FirebaseAuthException newException(String message, AuthErrorCode errorCode) { + return newException(message, errorCode, null); + } - return errorMessage; + private FirebaseAuthException newException( + String message, AuthErrorCode errorCode, Throwable cause) { + return new FirebaseAuthException( + ErrorCode.INVALID_ARGUMENT, message, cause, null, errorCode); } private String getVerifyTokenMessage() { @@ -224,15 +239,44 @@ private String getVerifyTokenMessage() { * Verifies the cryptographic signature on the FirebaseToken. Can block on a web request to fetch * the keys if they have expired. */ - private boolean isSignatureValid(IdToken token) throws GeneralSecurityException, IOException { - for (PublicKey key : publicKeysManager.getPublicKeys()) { - if (token.verifySignature(key)) { + private boolean isSignatureValid(IdToken token) throws FirebaseAuthException { + for (PublicKey key : fetchPublicKeys()) { + if (isSignatureValid(token, key)) { return true; } } + return false; } + private boolean isSignatureValid(IdToken token, PublicKey key) throws FirebaseAuthException { + try { + return token.verifySignature(key); + } catch (GeneralSecurityException e) { + // This doesn't happen under usual circumstances. Seems to only happen if the crypto + // setup of the runtime is incorrect in some way. + throw new FirebaseAuthException( + ErrorCode.UNKNOWN, + String.format("Unexpected error while verifying %s: %s", shortName, e.getMessage()), + e, + null, + invalidTokenErrorCode); + } + } + + private List fetchPublicKeys() throws FirebaseAuthException { + try { + return publicKeysManager.getPublicKeys(); + } catch (GeneralSecurityException | IOException e) { + throw new FirebaseAuthException( + ErrorCode.UNKNOWN, + "Error while fetching public key certificates: " + e.getMessage(), + e, + null, + AuthErrorCode.CERTIFICATE_FETCH_FAILED); + } + } + private String getErrorForTokenWithoutKid(IdToken.Header header, IdToken.Payload payload) { if (isCustomToken(payload)) { return String.format("%s expects %s, but was given a custom token.", @@ -255,11 +299,6 @@ private String getProjectIdMatchMessage() { shortName); } - private boolean verifyTimestamps(IdToken token) { - long currentTimeMillis = idTokenVerifier.getClock().currentTimeMillis(); - return token.verifyTime(currentTimeMillis, idTokenVerifier.getAcceptableTimeSkewSeconds()); - } - private boolean isCustomToken(IdToken.Payload payload) { return FIREBASE_AUDIENCE.equals(payload.getAudience()); } @@ -290,6 +329,8 @@ static final class Builder { private String shortName; private IdTokenVerifier idTokenVerifier; private String docUrl; + private AuthErrorCode invalidTokenErrorCode; + private AuthErrorCode expiredTokenErrorCode; private Builder() { } @@ -323,6 +364,16 @@ Builder setDocUrl(String docUrl) { return this; } + public Builder setInvalidTokenErrorCode(AuthErrorCode invalidTokenErrorCode) { + this.invalidTokenErrorCode = invalidTokenErrorCode; + return this; + } + + public Builder setExpiredTokenErrorCode(AuthErrorCode expiredTokenErrorCode) { + this.expiredTokenErrorCode = expiredTokenErrorCode; + return this; + } + FirebaseTokenVerifierImpl build() { return new FirebaseTokenVerifierImpl(this); } diff --git a/src/main/java/com/google/firebase/auth/RevocationCheckDecorator.java b/src/main/java/com/google/firebase/auth/RevocationCheckDecorator.java index e53ad25c4..74cda69c9 100644 --- a/src/main/java/com/google/firebase/auth/RevocationCheckDecorator.java +++ b/src/main/java/com/google/firebase/auth/RevocationCheckDecorator.java @@ -20,30 +20,27 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.base.Strings; +import com.google.firebase.ErrorCode; /** * A decorator for adding token revocation checks to an existing {@link FirebaseTokenVerifier}. */ class RevocationCheckDecorator implements FirebaseTokenVerifier { - static final String ID_TOKEN_REVOKED_ERROR = "id-token-revoked"; - static final String SESSION_COOKIE_REVOKED_ERROR = "session-cookie-revoked"; - private final FirebaseTokenVerifier tokenVerifier; private final FirebaseUserManager userManager; - private final String errorCode; + private final AuthErrorCode errorCode; private final String shortName; private RevocationCheckDecorator( FirebaseTokenVerifier tokenVerifier, FirebaseUserManager userManager, - String errorCode, + AuthErrorCode errorCode, String shortName) { this.tokenVerifier = checkNotNull(tokenVerifier); this.userManager = checkNotNull(userManager); - checkArgument(!Strings.isNullOrEmpty(errorCode)); + this.errorCode = checkNotNull(errorCode); checkArgument(!Strings.isNullOrEmpty(shortName)); - this.errorCode = errorCode; this.shortName = shortName; } @@ -55,8 +52,14 @@ private RevocationCheckDecorator( public FirebaseToken verifyToken(String token) throws FirebaseAuthException { FirebaseToken firebaseToken = tokenVerifier.verifyToken(token); if (isRevoked(firebaseToken)) { - throw new FirebaseAuthException(errorCode, "Firebase " + shortName + " revoked"); + throw new FirebaseAuthException( + ErrorCode.INVALID_ARGUMENT, + "Firebase " + shortName + " is revoked.", + null, + null, + errorCode); } + return firebaseToken; } @@ -69,12 +72,12 @@ private boolean isRevoked(FirebaseToken firebaseToken) throws FirebaseAuthExcept static RevocationCheckDecorator decorateIdTokenVerifier( FirebaseTokenVerifier tokenVerifier, FirebaseUserManager userManager) { return new RevocationCheckDecorator( - tokenVerifier, userManager, ID_TOKEN_REVOKED_ERROR, "id token"); + tokenVerifier, userManager, AuthErrorCode.REVOKED_ID_TOKEN, "id token"); } static RevocationCheckDecorator decorateSessionCookieVerifier( FirebaseTokenVerifier tokenVerifier, FirebaseUserManager userManager) { return new RevocationCheckDecorator( - tokenVerifier, userManager, SESSION_COOKIE_REVOKED_ERROR, "session cookie"); + tokenVerifier, userManager, AuthErrorCode.REVOKED_SESSION_COOKIE, "session cookie"); } } diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index 804739e38..9b8a58212 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -439,8 +439,8 @@ public void testVerifyIdToken() throws Exception { fail("expecting exception"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(RevocationCheckDecorator.ID_TOKEN_REVOKED_ERROR, - ((FirebaseAuthException) e.getCause()).getDeprecatedErrorCode()); + assertEquals(AuthErrorCode.REVOKED_ID_TOKEN, + ((FirebaseAuthException) e.getCause()).getAuthErrorCode()); } idToken = signInWithCustomToken(customToken); decoded = auth.verifyIdTokenAsync(idToken, true).get(); @@ -473,8 +473,8 @@ public void testVerifySessionCookie() throws Exception { fail("expecting exception"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(RevocationCheckDecorator.SESSION_COOKIE_REVOKED_ERROR, - ((FirebaseAuthException) e.getCause()).getDeprecatedErrorCode()); + assertEquals(AuthErrorCode.REVOKED_SESSION_COOKIE, + ((FirebaseAuthException) e.getCause()).getAuthErrorCode()); } idToken = signInWithCustomToken(customToken); diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java index 3bc8ab2da..bc8461280 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java @@ -20,15 +20,19 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.api.core.ApiFuture; import com.google.common.base.Defaults; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; @@ -53,6 +57,11 @@ public class FirebaseAuthTest { .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) .build(); + private static final FirebaseAuthException testException = new FirebaseAuthException( + ErrorCode.INVALID_ARGUMENT, "Test error message", null, null, null); + private static final long VALID_SINCE = 1494364393; + public static final String TEST_USER = "testUser"; + @After public void cleanup() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); @@ -145,14 +154,14 @@ public void testProjectIdNotRequiredAtInitialization() { assertNotNull(FirebaseAuth.getInstance(app)); } - @Test(expected = IllegalArgumentException.class) + @Test(expected = NullPointerException.class) public void testAuthExceptionNullErrorCode() { - new FirebaseAuthException(null, "test"); + new FirebaseAuthException(null, "test", null, null, null); } @Test(expected = IllegalArgumentException.class) - public void testAuthExceptionEmptyErrorCode() { - new FirebaseAuthException("", "test"); + public void testAuthExceptionNullMessage() { + new FirebaseAuthException(ErrorCode.INTERNAL, null, null, null, null); } @Test @@ -224,19 +233,48 @@ public void testVerifyIdToken() throws Exception { assertEquals("idtoken", tokenVerifier.getLastTokenString()); } + @Test + public void testVerifyIdTokenWithRevocationCheck() throws Exception { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( + getFirebaseToken(VALID_SINCE + 1000)); + FirebaseAuth auth = getAuthForIdTokenVerificationWithRevocationCheck(tokenVerifier); + + FirebaseToken firebaseToken = auth.verifyIdToken("idtoken", true); + + assertEquals("testUser", firebaseToken.getUid()); + assertEquals("idtoken", tokenVerifier.getLastTokenString()); + } + + @Test + public void testVerifyIdTokenWithRevocationCheckFailure() { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( + getFirebaseToken(VALID_SINCE - 1000)); + FirebaseAuth auth = getAuthForIdTokenVerificationWithRevocationCheck(tokenVerifier); + + try { + auth.verifyIdToken("idtoken", true); + fail("No error thrown for revoked ID token"); + } catch (FirebaseAuthException e) { + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCodeNew()); + assertEquals("Firebase id token is revoked.", e.getMessage()); + assertNull(e.getCause()); + assertNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.REVOKED_ID_TOKEN, e.getAuthErrorCode()); + } + + assertEquals("idtoken", tokenVerifier.getLastTokenString()); + } + @Test public void testVerifyIdTokenFailure() { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException( - new FirebaseAuthException("TEST_CODE", "Test error message")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(); FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); try { auth.verifyIdToken("idtoken"); fail("No error thrown for invalid token"); } catch (FirebaseAuthException authException) { - assertEquals("TEST_CODE", authException.getDeprecatedErrorCode()); - assertEquals("Test error message", authException.getMessage()); - assertEquals("idtoken", tokenVerifier.getLastTokenString()); + assertSame(testException, authException); } } @@ -254,8 +292,7 @@ public void testVerifyIdTokenAsync() throws Exception { @Test public void testVerifyIdTokenAsyncFailure() throws InterruptedException { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException( - new FirebaseAuthException("TEST_CODE", "Test error message")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(); FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); try { @@ -263,16 +300,13 @@ public void testVerifyIdTokenAsyncFailure() throws InterruptedException { fail("No error thrown for invalid token"); } catch (ExecutionException e) { FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals("TEST_CODE", authException.getDeprecatedErrorCode()); - assertEquals("Test error message", authException.getMessage()); - assertEquals("idtoken", tokenVerifier.getLastTokenString()); + assertSame(testException, authException); } } @Test public void testVerifyIdTokenWithCheckRevokedAsyncFailure() throws InterruptedException { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException( - new FirebaseAuthException("TEST_CODE", "Test error message")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(); FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); try { @@ -280,9 +314,7 @@ public void testVerifyIdTokenWithCheckRevokedAsyncFailure() throws InterruptedEx fail("No error thrown for invalid token"); } catch (ExecutionException e) { FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals("TEST_CODE", authException.getDeprecatedErrorCode()); - assertEquals("Test error message", authException.getMessage()); - assertEquals("idtoken", tokenVerifier.getLastTokenString()); + assertSame(testException, authException); } } @@ -357,20 +389,49 @@ public void testVerifySessionCookie() throws Exception { @Test public void testVerifySessionCookieFailure() { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException( - new FirebaseAuthException("TEST_CODE", "Test error message")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(); FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); try { auth.verifySessionCookie("idtoken"); fail("No error thrown for invalid token"); } catch (FirebaseAuthException authException) { - assertEquals("TEST_CODE", authException.getDeprecatedErrorCode()); - assertEquals("Test error message", authException.getMessage()); - assertEquals("idtoken", tokenVerifier.getLastTokenString()); + assertSame(testException, authException); } } + @Test + public void testVerifySessionCookieWithRevocationCheck() throws Exception { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( + getFirebaseToken(VALID_SINCE + 1000)); + FirebaseAuth auth = getAuthForSessionCookieVerificationWithRevocationCheck(tokenVerifier); + + FirebaseToken firebaseToken = auth.verifySessionCookie("cookie", true); + + assertEquals("testUser", firebaseToken.getUid()); + assertEquals("cookie", tokenVerifier.getLastTokenString()); + } + + @Test + public void testVerifySessionCookieWithRevocationCheckFailure() { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( + getFirebaseToken(VALID_SINCE - 1000)); + FirebaseAuth auth = getAuthForSessionCookieVerificationWithRevocationCheck(tokenVerifier); + + try { + auth.verifySessionCookie("cookie", true); + fail("No error thrown for revoked session cookie"); + } catch (FirebaseAuthException e) { + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCodeNew()); + assertEquals("Firebase session cookie is revoked.", e.getMessage()); + assertNull(e.getCause()); + assertNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.REVOKED_SESSION_COOKIE, e.getAuthErrorCode()); + } + + assertEquals("cookie", tokenVerifier.getLastTokenString()); + } + @Test public void testVerifySessionCookieAsync() throws Exception { MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( @@ -385,8 +446,7 @@ public void testVerifySessionCookieAsync() throws Exception { @Test public void testVerifySessionCookieAsyncFailure() throws InterruptedException { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException( - new FirebaseAuthException("TEST_CODE", "Test error message")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(); FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); try { @@ -394,16 +454,13 @@ public void testVerifySessionCookieAsyncFailure() throws InterruptedException { fail("No error thrown for invalid token"); } catch (ExecutionException e) { FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals("TEST_CODE", authException.getDeprecatedErrorCode()); - assertEquals("Test error message", authException.getMessage()); - assertEquals("idtoken", tokenVerifier.getLastTokenString()); + assertSame(testException, authException); } } @Test public void testVerifySessionCookieWithCheckRevokedAsyncFailure() throws InterruptedException { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException( - new FirebaseAuthException("TEST_CODE", "Test error message")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(); FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); try { @@ -411,9 +468,7 @@ public void testVerifySessionCookieWithCheckRevokedAsyncFailure() throws Interru fail("No error thrown for invalid token"); } catch (ExecutionException e) { FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals("TEST_CODE", authException.getDeprecatedErrorCode()); - assertEquals("Test error message", authException.getMessage()); - assertEquals("idtoken", tokenVerifier.getLastTokenString()); + assertSame(testException, authException); } } @@ -421,6 +476,16 @@ private FirebaseToken getFirebaseToken(String subject) { return new FirebaseToken(ImmutableMap.of("sub", subject)); } + private FirebaseToken getFirebaseToken(long issuedAt) { + return new FirebaseToken(ImmutableMap.of("sub", TEST_USER, "iat", issuedAt)); + } + + FirebaseAuth getAuthForIdTokenVerificationWithRevocationCheck( + FirebaseTokenVerifier tokenVerifier) { + FirebaseApp app = getFirebaseAppForUserRetrieval(); + return getAuthForIdTokenVerification(app, Suppliers.ofInstance(tokenVerifier)); + } + private FirebaseAuth getAuthForIdTokenVerification(FirebaseTokenVerifier tokenVerifier) { return getAuthForIdTokenVerification(Suppliers.ofInstance(tokenVerifier)); } @@ -428,6 +493,12 @@ private FirebaseAuth getAuthForIdTokenVerification(FirebaseTokenVerifier tokenVe private FirebaseAuth getAuthForIdTokenVerification( Supplier tokenVerifierSupplier) { FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions); + return getAuthForIdTokenVerification(app, tokenVerifierSupplier); + } + + private FirebaseAuth getAuthForIdTokenVerification( + FirebaseApp app, + Supplier tokenVerifierSupplier) { FirebaseUserManager userManager = new FirebaseUserManager(app); return FirebaseAuth.builder() .setFirebaseApp(app) @@ -436,6 +507,12 @@ private FirebaseAuth getAuthForIdTokenVerification( .build(); } + FirebaseAuth getAuthForSessionCookieVerificationWithRevocationCheck( + FirebaseTokenVerifier tokenVerifier) { + FirebaseApp app = getFirebaseAppForUserRetrieval(); + return getAuthForSessionCookieVerification(app, Suppliers.ofInstance(tokenVerifier)); + } + private FirebaseAuth getAuthForSessionCookieVerification(FirebaseTokenVerifier tokenVerifier) { return getAuthForSessionCookieVerification(Suppliers.ofInstance(tokenVerifier)); } @@ -443,6 +520,12 @@ private FirebaseAuth getAuthForSessionCookieVerification(FirebaseTokenVerifier t private FirebaseAuth getAuthForSessionCookieVerification( Supplier tokenVerifierSupplier) { FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions); + return getAuthForSessionCookieVerification(app, tokenVerifierSupplier); + } + + private FirebaseAuth getAuthForSessionCookieVerification( + FirebaseApp app, + Supplier tokenVerifierSupplier) { FirebaseUserManager userManager = new FirebaseUserManager(app); return FirebaseAuth.builder() .setFirebaseApp(app) @@ -451,6 +534,18 @@ private FirebaseAuth getAuthForSessionCookieVerification( .build(); } + private FirebaseApp getFirebaseAppForUserRetrieval() { + String getUserResponse = TestUtils.loadResource("getUser.json"); + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(new MockLowLevelHttpResponse().setContent(getUserResponse)) + .build(); + return FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setHttpTransport(transport) + .setProjectId("test-project-id") + .build()); + } + private static class MockTokenVerifier implements FirebaseTokenVerifier { private String lastTokenString; @@ -480,8 +575,8 @@ static MockTokenVerifier fromResult(FirebaseToken result) { return new MockTokenVerifier(result, null); } - static MockTokenVerifier fromException(FirebaseAuthException exception) { - return new MockTokenVerifier(null, exception); + static MockTokenVerifier fromException() { + return new MockTokenVerifier(null, testException); } } diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java index b10817afb..83b28ec8d 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java @@ -17,6 +17,7 @@ package com.google.firebase.auth; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import com.google.api.client.auth.openidconnect.IdTokenVerifier; @@ -29,15 +30,15 @@ import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; +import com.google.firebase.ErrorCode; import com.google.firebase.testing.ServiceAccount; import java.io.IOException; +import java.security.GeneralSecurityException; import java.util.concurrent.TimeUnit; import org.junit.Assert; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; public class FirebaseTokenVerifierImplTest { @@ -54,9 +55,6 @@ public class FirebaseTokenVerifierImplTest { private static final String TEST_TOKEN_ISSUER = "https://test.token.issuer"; - @Rule - public ExpectedException thrown = ExpectedException.none(); - private FirebaseTokenVerifier tokenVerifier; private TestTokenFactory tokenFactory; @@ -79,108 +77,188 @@ public void testVerifyToken() throws Exception { } @Test - public void testVerifyTokenWithoutKeyId() throws Exception { + public void testVerifyTokenWithoutKeyId() { String token = createTokenWithoutKeyId(); - thrown.expectMessage("Firebase test token has no \"kid\" claim."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token has no \"kid\" claim. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenFirebaseCustomToken() throws Exception { + public void testVerifyTokenFirebaseCustomToken() { String token = createCustomToken(); - thrown.expectMessage("verifyTestToken() expects a test token, but was given a custom token."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "verifyTestToken() expects a test token, but was given a custom token. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenIncorrectAlgorithm() throws Exception { + public void testVerifyTokenIncorrectAlgorithm() { String token = createTokenWithIncorrectAlgorithm(); - thrown.expectMessage("Firebase test token has incorrect algorithm."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token has incorrect algorithm. " + + "Expected \"RS256\" but got \"HSA\". " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenIncorrectAudience() throws Exception { + public void testVerifyTokenIncorrectAudience() { String token = createTokenWithIncorrectAudience(); - thrown.expectMessage("Firebase test token has incorrect \"aud\" (audience) claim."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token has incorrect \"aud\" (audience) claim. " + + "Expected \"proj-test-101\" but got \"invalid-audience\". " + + "Make sure the test token comes from the same Firebase project as the service account " + + "used to authenticate this SDK. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenIncorrectIssuer() throws Exception { + public void testVerifyTokenIncorrectIssuer() { String token = createTokenWithIncorrectIssuer(); - thrown.expectMessage("Firebase test token has incorrect \"iss\" (issuer) claim."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token has incorrect \"iss\" (issuer) claim. " + + "Expected \"https://test.token.issuer\" but got " + + "\"https://incorrect.issuer.prefix/proj-test-101\". Make sure the test token comes " + + "from the same Firebase project as the service account used to authenticate this SDK. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenMissingSubject() throws Exception { + public void testVerifyTokenMissingSubject() { String token = createTokenWithSubject(null); - thrown.expectMessage("Firebase test token has no \"sub\" (subject) claim."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token has no \"sub\" (subject) claim. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenEmptySubject() throws Exception { + public void testVerifyTokenEmptySubject() { String token = createTokenWithSubject(""); - thrown.expectMessage("Firebase test token has an empty string \"sub\" (subject) claim."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token has an empty string \"sub\" (subject) claim. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenLongSubject() throws Exception { + public void testVerifyTokenLongSubject() { String token = createTokenWithSubject(Strings.repeat("a", 129)); - thrown.expectMessage( - "Firebase test token has \"sub\" (subject) claim longer than 128 characters."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token has \"sub\" (subject) claim longer " + + "than 128 characters. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenIssuedAtInFuture() throws Exception { + public void testVerifyTokenIssuedAtInFuture() { long tenMinutesIntoTheFuture = (TestTokenFactory.CLOCK.currentTimeMillis() / 1000) + TimeUnit.MINUTES.toSeconds(10); String token = createTokenWithTimestamps( tenMinutesIntoTheFuture, tenMinutesIntoTheFuture + TimeUnit.HOURS.toSeconds(1)); - thrown.expectMessage("Firebase test token has expired or is not yet valid."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token is not yet valid. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenExpired() throws Exception { + public void testVerifyTokenExpired() { long twoHoursInPast = (TestTokenFactory.CLOCK.currentTimeMillis() / 1000) - TimeUnit.HOURS.toSeconds(2); String token = createTokenWithTimestamps( twoHoursInPast, twoHoursInPast + TimeUnit.HOURS.toSeconds(1)); - thrown.expectMessage("Firebase test token has expired or is not yet valid."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token has expired. " + + "Get a fresh test token and try again. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkException(e, message, AuthErrorCode.EXPIRED_ID_TOKEN); + } } @Test - public void testVerifyTokenIncorrectCert() throws Exception { + public void testVerifyTokenSignatureMismatch() { String token = tokenFactory.createToken(); GooglePublicKeysManager publicKeysManager = newPublicKeysManager( ServiceAccount.NONE.getCert()); FirebaseTokenVerifier tokenVerifier = newTestTokenVerifier(publicKeysManager); - thrown.expectMessage("Failed to verify the signature of Firebase test token. " - + "See https://test.doc.url for details on how to retrieve a test token."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Failed to verify the signature of Firebase test token. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } + } + + @Test + public void testMalformedCert() { + String token = tokenFactory.createToken(); + GooglePublicKeysManager publicKeysManager = newPublicKeysManager("malformed.cert"); + FirebaseTokenVerifier tokenVerifier = newTestTokenVerifier(publicKeysManager); + + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Error while fetching public key certificates: Could not parse certificate"; + assertEquals(ErrorCode.UNKNOWN, e.getErrorCodeNew()); + assertTrue(e.getMessage().startsWith(message)); + assertTrue(e.getCause() instanceof GeneralSecurityException); + assertNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.CERTIFICATE_FETCH_FAILED, e.getAuthErrorCode()); + } } @Test - public void verifyTokenCertificateError() { + public void testCertificateFetchError() { MockHttpTransport failingTransport = new MockHttpTransport() { @Override public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { @@ -194,26 +272,57 @@ public LowLevelHttpRequest buildRequest(String method, String url) throws IOExce try { idTokenVerifier.verifyToken(token); Assert.fail("No exception thrown"); - } catch (FirebaseAuthException expected) { - assertTrue(expected.getCause() instanceof IOException); - assertEquals("Expected error", expected.getCause().getMessage()); + } catch (FirebaseAuthException e) { + String message = "Error while fetching public key certificates: Expected error"; + assertEquals(ErrorCode.UNKNOWN, e.getErrorCodeNew()); + assertEquals(message, e.getMessage()); + assertTrue(e.getCause() instanceof IOException); + assertNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.CERTIFICATE_FETCH_FAILED, e.getAuthErrorCode()); } } @Test - public void testLegacyCustomToken() throws Exception { - thrown.expectMessage( - "verifyTestToken() expects a test token, but was given a legacy custom token."); - tokenVerifier.verifyToken(LEGACY_CUSTOM_TOKEN); + public void testMalformedSignature() { + String token = tokenFactory.createToken(); + String[] segments = token.split("\\."); + token = String.format("%s.%s.%s", segments[0], segments[1], "MalformedSignature"); + + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Failed to verify the signature of Firebase test token. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testMalformedToken() throws Exception { - thrown.expectMessage( - "Failed to parse Firebase test token. Make sure you passed a string that represents a " - + "complete and valid JWT. See https://test.doc.url for details on how to retrieve " - + "a test token."); - tokenVerifier.verifyToken("not.a.jwt"); + public void testLegacyCustomToken() { + try { + tokenVerifier.verifyToken(LEGACY_CUSTOM_TOKEN); + } catch (FirebaseAuthException e) { + String message = "verifyTestToken() expects a test token, but was given a " + + "legacy custom token. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } + } + + @Test + public void testMalformedToken() { + try { + tokenVerifier.verifyToken("not.a.jwt"); + } catch (FirebaseAuthException e) { + String message = "Failed to parse Firebase test token. " + + "Make sure you passed a string that represents a complete and valid JWT. " + + "See https://test.doc.url for details on how to retrieve a test token."; + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCodeNew()); + assertEquals(message, e.getMessage()); + assertTrue(e.getCause() instanceof IllegalArgumentException); + assertNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.INVALID_ID_TOKEN, e.getAuthErrorCode()); + } } @Test(expected = NullPointerException.class) @@ -272,6 +381,8 @@ private FirebaseTokenVerifier newTestTokenVerifier(GooglePublicKeysManager publi .setJsonFactory(TestTokenFactory.JSON_FACTORY) .setPublicKeysManager(publicKeysManager) .setIdTokenVerifier(newIdTokenVerifier()) + .setInvalidTokenErrorCode(AuthErrorCode.INVALID_ID_TOKEN) + .setExpiredTokenErrorCode(AuthErrorCode.EXPIRED_ID_TOKEN) .build(); } @@ -337,4 +448,16 @@ private String createTokenWithTimestamps(long issuedAtSeconds, long expirationSe payload.setExpirationTimeSeconds(expirationSeconds); return tokenFactory.createToken(payload); } + + private void checkInvalidTokenException(FirebaseAuthException e, String message) { + checkException(e, message, AuthErrorCode.INVALID_ID_TOKEN); + } + + private void checkException(FirebaseAuthException e, String message, AuthErrorCode errorCode) { + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCodeNew()); + assertEquals(message, e.getMessage()); + assertNull(e.getCause()); + assertNull(e.getHttpResponse()); + assertEquals(errorCode, e.getAuthErrorCode()); + } } diff --git a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java index 24b90ad45..c26845dc7 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java @@ -18,6 +18,7 @@ import com.google.common.io.BaseEncoding; import com.google.firebase.auth.ActionCodeSettings; +import com.google.firebase.auth.AuthErrorCode; import com.google.firebase.auth.ErrorInfo; import com.google.firebase.auth.ExportedUserRecord; import com.google.firebase.auth.FirebaseAuth; @@ -254,7 +255,7 @@ public static void verifyIdTokenCheckRevoked(String idToken) { // Token is valid and not revoked. String uid = decodedToken.getUid(); } catch (FirebaseAuthException e) { - if (e.getDeprecatedErrorCode().equals("id-token-revoked")) { + if (e.getAuthErrorCode() == AuthErrorCode.REVOKED_ID_TOKEN) { // Token has been revoked. Inform the user to re-authenticate or signOut() the user. } else { // Token is invalid.