From 6d4d310729d154cc71218f5c025d7bc326e07bb7 Mon Sep 17 00:00:00 2001 From: Avishalom Shalit Date: Wed, 31 Jan 2018 01:29:23 -0500 Subject: [PATCH 01/14] revoke tokens and tokensValidAfterTime --- .../google/firebase/auth/FirebaseAuth.java | 72 ++++++++++++++++++- .../com/google/firebase/auth/UserRecord.java | 22 ++++++ .../auth/internal/GetAccountInfoResponse.java | 7 ++ .../google/firebase/auth/FirebaseAuthIT.java | 24 +++++++ 4 files changed, 122 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index 5b078504d..58ca1d6da 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -40,7 +40,7 @@ import com.google.firebase.internal.Nullable; import com.google.firebase.internal.TaskToApiFuture; import com.google.firebase.tasks.Task; - +import java.util.Date; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicBoolean; @@ -201,6 +201,10 @@ public ApiFuture createCustomTokenAsync( * @deprecated Use {@link #verifyIdTokenAsync(String)} */ public Task verifyIdToken(final String token) { + return verifyIdToken(token, false); + } + + private Task verifyIdToken(final String token, final boolean checkRevoked) { checkNotDestroyed(); checkState(!Strings.isNullOrEmpty(projectId), "Must initialize FirebaseApp with a project ID to call verifyIdToken()"); @@ -217,12 +221,48 @@ public FirebaseToken call() throws Exception { // This will throw a FirebaseAuthException with details on how the token is invalid. firebaseTokenVerifier.verifyTokenAndSignature(firebaseToken.getToken()); - + + if (checkRevoked) { + String uid = firebaseToken.getUid(); + UserRecord user = userManager.getUserById(uid); + if (user.getTokensValidAfterTime() + > ((long)firebaseToken.getClaims().get("iat")) * 1000) { + throw new FirebaseAuthException("id-token-revoked", "Firebase auth token revoked"); + } + } return firebaseToken; } }); } + private Task revokeRefreshTokens(String uid) { + final UpdateRequest request = new UpdateRequest(uid).setValidSince(new Date().getTime() / 1000); + return call(new Callable() { + @Override + public Void call() throws Exception { + userManager.updateUser(request, jsonFactory); + return null; + } + }); + } + + /** + * + * + * + * + * + * + * + * + * + * + * + */ + public ApiFuture revokeRefreshTokensAsync(String uid) { + return new TaskToApiFuture<>(revokeRefreshTokens(uid)); + } + /** * Parses and verifies a Firebase ID Token. * @@ -244,7 +284,33 @@ public FirebaseToken call() throws Exception { * unsuccessfully with the failure Exception. */ public ApiFuture verifyIdTokenAsync(final String token) { - return new TaskToApiFuture<>(verifyIdToken(token)); + return verifyIdTokenAsync(token, false); + } + + /** + * Parses and verifies a Firebase ID Token and if requested, checks whether it was revoked. + * + *

A Firebase application can identify itself to a trusted backend server by sending its + * Firebase ID Token (accessible via the getToken API in the Firebase Authentication client) with + * its request. + * + *

The backend server can then use the verifyIdToken() method to verify the token is valid, + * meaning: the token is properly signed, has not expired, and it was issued for the project + * associated with this FirebaseAuth instance (which by default is extracted from your service + * account) + * + *

If the token is valid, the returned Future will complete successfully and provide a + * parsed version of the token from which the UID and other claims in the token can be inspected. + * If the token is invalid, the future throws an exception indicating the failure. + * + * @param token A Firebase ID Token to verify and parse. + * @param checkRevoked A boolean denoting whether to check if the tokens were revoked. + * @return An {@code ApiFuture} which will complete successfully with the parsed token, or + * unsuccessfully with the failure Exception. + */ + public ApiFuture verifyIdTokenAsync(final String token, + final boolean checkRevoked) { + return new TaskToApiFuture<>(verifyIdToken(token, checkRevoked)); } /** diff --git a/src/main/java/com/google/firebase/auth/UserRecord.java b/src/main/java/com/google/firebase/auth/UserRecord.java index e6d09278e..b08127e5d 100644 --- a/src/main/java/com/google/firebase/auth/UserRecord.java +++ b/src/main/java/com/google/firebase/auth/UserRecord.java @@ -32,6 +32,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -57,6 +58,7 @@ public class UserRecord implements UserInfo { private final String photoUrl; private final boolean disabled; private final ProviderUserInfo[] providers; + private final long tokensValidAfterTime; private final UserMetadata userMetadata; private final Map customClaims; @@ -79,6 +81,7 @@ public class UserRecord implements UserInfo { this.providers[i] = new ProviderUserInfo(response.getProviders()[i]); } } + this.tokensValidAfterTime = response.getValidSince() * 1000; this.userMetadata = new UserMetadata(response.getCreatedAt(), response.getLastLoginAt()); this.customClaims = parseCustomClaims(response.getCustomClaims(), jsonFactory); } @@ -188,6 +191,15 @@ public UserInfo[] getProviderData() { return providers; } + /** + * Returns the timestamp beginning with which tokens are valid in seconds since the epoch. + * + * @return the timestamp beginning with which tokens are valid in seconds since the epoch. + */ + public long getTokensValidAfterTime() { + return tokensValidAfterTime; + } + /** * Returns additional metadata associated with this user. * @@ -245,6 +257,10 @@ private static void checkCustomClaims(Map customClaims) { } } + private static void checkValidSince(long epochSeconds) { + checkArgument(epochSeconds < 1e12, "validSince must be in epoch seconds"); + } + private static String serializeCustomClaims(Map customClaims, JsonFactory jsonFactory) { checkNotNull(jsonFactory, "JsonFactory must not be null"); if (customClaims == null || customClaims.isEmpty()) { @@ -499,6 +515,12 @@ public UpdateRequest setCustomClaims(Map customClaims) { return this; } + public UpdateRequest setValidSince(long epochSeconds) { + checkValidSince(epochSeconds); + properties.put("validSince", epochSeconds); + return this; + } + Map getProperties(JsonFactory jsonFactory) { Map copy = new HashMap<>(properties); List remove = new ArrayList<>(); diff --git a/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java index 9c5330359..3d17c50f6 100644 --- a/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java +++ b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java @@ -73,6 +73,9 @@ public static class User { @Key("lastLoginAt") private long lastLoginAt; + @Key("validSince") + private long validSince; + @Key("customAttributes") private String customClaims; @@ -116,6 +119,10 @@ public long getLastLoginAt() { return lastLoginAt; } + public long getValidSince() { + return validSince; + } + public String getCustomClaims() { return customClaims; } diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index ea5e8b7c3..022b8531e 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -357,6 +357,30 @@ public void testCustomToken() throws Exception { assertEquals("user1", decoded.getUid()); } + @Test + public void testVerifyIDToken() throws Exception { + String customToken = auth.createCustomTokenAsync("user_ver").get(); + String idToken = signInWithCustomToken(customToken); + FirebaseToken decoded = auth.verifyIdTokenAsync(idToken).get(); + assertEquals("user_ver", decoded.getUid()); + decoded = auth.verifyIdTokenAsync(idToken, true).get(); + assertEquals("user_ver", decoded.getUid()); + Thread.sleep(1100); + auth.revokeRefreshTokensAsync("user_ver").get(); + decoded = auth.verifyIdTokenAsync(idToken, false).get(); + assertEquals("user_ver", decoded.getUid()); + try { + decoded = auth.verifyIdTokenAsync(idToken, true).get(); + fail("expecting exception"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseAuthException); + assertEquals("id-token-revoked", ((FirebaseAuthException) e.getCause()).getErrorCode()); + } + idToken = signInWithCustomToken(customToken); + decoded = auth.verifyIdTokenAsync(idToken, true).get(); + assertEquals("user_ver", decoded.getUid()); + } + @Test public void testCustomTokenWithClaims() throws Exception { Map devClaims = ImmutableMap.of( From e7ac57b48324d459ae796868faea4ab15329e809 Mon Sep 17 00:00:00 2001 From: Avishalom Shalit Date: Wed, 31 Jan 2018 11:38:41 -0500 Subject: [PATCH 02/14] move unit test --- .../java/com/google/firebase/auth/FirebaseAuth.java | 1 + .../java/com/google/firebase/auth/UserRecord.java | 7 +++++-- .../com/google/firebase/auth/UserRecordTest.java | 13 +++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index 58ca1d6da..4a3e260ff 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -236,6 +236,7 @@ public FirebaseToken call() throws Exception { } private Task revokeRefreshTokens(String uid) { + checkNotDestroyed(); final UpdateRequest request = new UpdateRequest(uid).setValidSince(new Date().getTime() / 1000); return call(new Callable() { @Override diff --git a/src/main/java/com/google/firebase/auth/UserRecord.java b/src/main/java/com/google/firebase/auth/UserRecord.java index b08127e5d..bddaf7ad4 100644 --- a/src/main/java/com/google/firebase/auth/UserRecord.java +++ b/src/main/java/com/google/firebase/auth/UserRecord.java @@ -21,6 +21,7 @@ import static com.google.common.base.Preconditions.checkState; import com.google.api.client.json.JsonFactory; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -258,7 +259,8 @@ private static void checkCustomClaims(Map customClaims) { } private static void checkValidSince(long epochSeconds) { - checkArgument(epochSeconds < 1e12, "validSince must be in epoch seconds"); + checkArgument(epochSeconds < 1e12, + "validSince must be in epoch seconds: " + Long.toString(epochSeconds)); } private static String serializeCustomClaims(Map customClaims, JsonFactory jsonFactory) { @@ -515,7 +517,8 @@ public UpdateRequest setCustomClaims(Map customClaims) { return this; } - public UpdateRequest setValidSince(long epochSeconds) { + @VisibleForTesting + UpdateRequest setValidSince(long epochSeconds) { checkValidSince(epochSeconds); properties.put("validSince", epochSeconds); return this; diff --git a/src/test/java/com/google/firebase/auth/UserRecordTest.java b/src/test/java/com/google/firebase/auth/UserRecordTest.java index ce187a48b..d484b61e9 100644 --- a/src/test/java/com/google/firebase/auth/UserRecordTest.java +++ b/src/test/java/com/google/firebase/auth/UserRecordTest.java @@ -11,11 +11,13 @@ import com.google.common.collect.ImmutableMap; import com.google.firebase.auth.internal.DownloadAccountResponse; import com.google.firebase.auth.internal.GetAccountInfoResponse; +import com.google.firebase.auth.UserRecord.UpdateRequest; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; +import java.util.Date; import org.junit.Test; public class UserRecordTest { @@ -146,6 +148,17 @@ public void testEmptyCustomClaims() throws IOException { assertEquals(0, userRecord.getCustomClaims().size()); } + @Test + public void testInvalidVaidSince() { + UpdateRequest update = new UpdateRequest("test"); + try { + update.setValidSince(new Date().getTime()); + fail("No error thrown for time in milliseconds"); + } catch (Exception ignore) { + // expected + } + } + @Test public void testExportedUserUidOnly() throws IOException { ImmutableMap resp = ImmutableMap.of("localId", "user"); From 7b8ac07611884c3050db7b08fa5b73f17920d5a4 Mon Sep 17 00:00:00 2001 From: Avishalom Shalit Date: Wed, 31 Jan 2018 11:40:04 -0500 Subject: [PATCH 03/14] lint --- src/test/java/com/google/firebase/auth/UserRecordTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/google/firebase/auth/UserRecordTest.java b/src/test/java/com/google/firebase/auth/UserRecordTest.java index d484b61e9..d4de7e3d1 100644 --- a/src/test/java/com/google/firebase/auth/UserRecordTest.java +++ b/src/test/java/com/google/firebase/auth/UserRecordTest.java @@ -9,9 +9,9 @@ import com.google.api.client.json.JsonFactory; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.firebase.auth.UserRecord.UpdateRequest; import com.google.firebase.auth.internal.DownloadAccountResponse; import com.google.firebase.auth.internal.GetAccountInfoResponse; -import com.google.firebase.auth.UserRecord.UpdateRequest; import java.io.ByteArrayInputStream; import java.io.IOException; From 9a990ca464dab0c9ab596fd3c487b40454c1953f Mon Sep 17 00:00:00 2001 From: Avishalom Shalit Date: Fri, 2 Feb 2018 02:03:27 -0500 Subject: [PATCH 04/14] comments --- .../google/firebase/auth/FirebaseAuth.java | 27 +++++++++++-------- .../com/google/firebase/auth/UserRecord.java | 6 ----- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index 4a3e260ff..fff990bc6 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -248,17 +248,15 @@ public Void call() throws Exception { } /** + * Revokes all refresh tokens for the specified user. * + *

In addition to revoking all refresh tokens for a user, all ID tokens issued + * before revocation will also be revoked at the Auth backend. Any request with an + * ID token generated before revocation will be rejected with a token expired error. * - * - * - * - * - * - * - * - * - * + * @param uid The user id for which tokens are revoked. + * @return An {@code ApiFuture} which will complete successfully or if updating the user fails, + * unsuccessfully with the failure Exception. */ public ApiFuture revokeRefreshTokensAsync(String uid) { return new TaskToApiFuture<>(revokeRefreshTokens(uid)); @@ -299,9 +297,16 @@ public ApiFuture verifyIdTokenAsync(final String token) { * meaning: the token is properly signed, has not expired, and it was issued for the project * associated with this FirebaseAuth instance (which by default is extracted from your service * account) + * + *

If a request was made to check revoked, the issued-at property of the token (like all + * token timestamps, it is in seconds since the epoch) will be compared with the "tokens valid + * after time" property of the user. (Which, like other user property timetamps is in + * milliseconds since the epoch). + * If the token was issued before the valid-after time, it is considered revoked. * - *

If the token is valid, the returned Future will complete successfully and provide a - * parsed version of the token from which the UID and other claims in the token can be inspected. + *

If the token is valid, and not revoked, the returned Future will complete successfully and + * provide a parsed version of the token from which the UID and other claims in the token can be + * inspected. * If the token is invalid, the future throws an exception indicating the failure. * * @param token A Firebase ID Token to verify and parse. diff --git a/src/main/java/com/google/firebase/auth/UserRecord.java b/src/main/java/com/google/firebase/auth/UserRecord.java index bddaf7ad4..7ba315ba8 100644 --- a/src/main/java/com/google/firebase/auth/UserRecord.java +++ b/src/main/java/com/google/firebase/auth/UserRecord.java @@ -258,11 +258,6 @@ private static void checkCustomClaims(Map customClaims) { } } - private static void checkValidSince(long epochSeconds) { - checkArgument(epochSeconds < 1e12, - "validSince must be in epoch seconds: " + Long.toString(epochSeconds)); - } - private static String serializeCustomClaims(Map customClaims, JsonFactory jsonFactory) { checkNotNull(jsonFactory, "JsonFactory must not be null"); if (customClaims == null || customClaims.isEmpty()) { @@ -519,7 +514,6 @@ public UpdateRequest setCustomClaims(Map customClaims) { @VisibleForTesting UpdateRequest setValidSince(long epochSeconds) { - checkValidSince(epochSeconds); properties.put("validSince", epochSeconds); return this; } From d1f6e297c86d67251a9e169a2aa1795fd84032df Mon Sep 17 00:00:00 2001 From: Avishalom Shalit Date: Fri, 2 Feb 2018 12:06:26 -0500 Subject: [PATCH 05/14] cleanup --- src/test/java/com/google/firebase/auth/FirebaseAuthIT.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index 022b8531e..a40e89d01 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -379,6 +379,7 @@ public void testVerifyIDToken() throws Exception { idToken = signInWithCustomToken(customToken); decoded = auth.verifyIdTokenAsync(idToken, true).get(); assertEquals("user_ver", decoded.getUid()); + auth.deleteUserAsync("user_ver"); } @Test From 38c067e9f18c8aeb0e460f803494baee4e55878a Mon Sep 17 00:00:00 2001 From: Avishalom Shalit Date: Fri, 2 Feb 2018 12:58:24 -0500 Subject: [PATCH 06/14] whitespace --- src/main/java/com/google/firebase/auth/FirebaseAuth.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index fff990bc6..2777ad859 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -250,7 +250,7 @@ public Void call() throws Exception { /** * Revokes all refresh tokens for the specified user. * - *

In addition to revoking all refresh tokens for a user, all ID tokens issued + *

In addition to revoking all refresh tokens for a user, all ID tokens issued * before revocation will also be revoked at the Auth backend. Any request with an * ID token generated before revocation will be rejected with a token expired error. * @@ -298,7 +298,7 @@ public ApiFuture verifyIdTokenAsync(final String token) { * associated with this FirebaseAuth instance (which by default is extracted from your service * account) * - *

If a request was made to check revoked, the issued-at property of the token (like all + *

If a request was made to check revoked, the issued-at property of the token (like all * token timestamps, it is in seconds since the epoch) will be compared with the "tokens valid * after time" property of the user. (Which, like other user property timetamps is in * milliseconds since the epoch). From ff617dff997a1e3b5ca0d5e0b4eb6db8887a4573 Mon Sep 17 00:00:00 2001 From: Avishalom Shalit Date: Fri, 2 Feb 2018 13:09:19 -0500 Subject: [PATCH 07/14] remove time unit test for private function --- .../java/com/google/firebase/auth/UserRecordTest.java | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/test/java/com/google/firebase/auth/UserRecordTest.java b/src/test/java/com/google/firebase/auth/UserRecordTest.java index d4de7e3d1..cee249b22 100644 --- a/src/test/java/com/google/firebase/auth/UserRecordTest.java +++ b/src/test/java/com/google/firebase/auth/UserRecordTest.java @@ -148,17 +148,6 @@ public void testEmptyCustomClaims() throws IOException { assertEquals(0, userRecord.getCustomClaims().size()); } - @Test - public void testInvalidVaidSince() { - UpdateRequest update = new UpdateRequest("test"); - try { - update.setValidSince(new Date().getTime()); - fail("No error thrown for time in milliseconds"); - } catch (Exception ignore) { - // expected - } - } - @Test public void testExportedUserUidOnly() throws IOException { ImmutableMap resp = ImmutableMap.of("localId", "user"); From 446bc53f50fa3e7dde97376ca22f542aed656fbe Mon Sep 17 00:00:00 2001 From: Avishalom Shalit Date: Fri, 2 Feb 2018 13:58:45 -0500 Subject: [PATCH 08/14] time -> timestamp --- src/main/java/com/google/firebase/auth/FirebaseAuth.java | 2 +- src/main/java/com/google/firebase/auth/UserRecord.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index 2777ad859..7184daa32 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -225,7 +225,7 @@ public FirebaseToken call() throws Exception { if (checkRevoked) { String uid = firebaseToken.getUid(); UserRecord user = userManager.getUserById(uid); - if (user.getTokensValidAfterTime() + if (user.getTokensValidAfterTimestamp() > ((long)firebaseToken.getClaims().get("iat")) * 1000) { throw new FirebaseAuthException("id-token-revoked", "Firebase auth token revoked"); } diff --git a/src/main/java/com/google/firebase/auth/UserRecord.java b/src/main/java/com/google/firebase/auth/UserRecord.java index 7ba315ba8..c8a6880c8 100644 --- a/src/main/java/com/google/firebase/auth/UserRecord.java +++ b/src/main/java/com/google/firebase/auth/UserRecord.java @@ -59,7 +59,7 @@ public class UserRecord implements UserInfo { private final String photoUrl; private final boolean disabled; private final ProviderUserInfo[] providers; - private final long tokensValidAfterTime; + private final long tokensValidAfterTimestamp; private final UserMetadata userMetadata; private final Map customClaims; @@ -82,7 +82,7 @@ public class UserRecord implements UserInfo { this.providers[i] = new ProviderUserInfo(response.getProviders()[i]); } } - this.tokensValidAfterTime = response.getValidSince() * 1000; + this.tokensValidAfterTimestamp = response.getValidSince() * 1000; this.userMetadata = new UserMetadata(response.getCreatedAt(), response.getLastLoginAt()); this.customClaims = parseCustomClaims(response.getCustomClaims(), jsonFactory); } @@ -197,8 +197,8 @@ public UserInfo[] getProviderData() { * * @return the timestamp beginning with which tokens are valid in seconds since the epoch. */ - public long getTokensValidAfterTime() { - return tokensValidAfterTime; + public long getTokensValidAfterTimestamp() { + return tokensValidAfterTimestamp; } /** From 52d27c5441a26cbb2b5d0e591cd4ee9103c69259 Mon Sep 17 00:00:00 2001 From: Avishalom Shalit Date: Mon, 5 Feb 2018 11:53:57 -0500 Subject: [PATCH 09/14] PR comments --- .../google/firebase/auth/FirebaseAuth.java | 35 +++++++++++-------- .../firebase/auth/FirebaseUserManager.java | 1 + .../com/google/firebase/auth/UserRecord.java | 13 +++++-- .../google/firebase/auth/FirebaseAuthIT.java | 21 +++++------ .../google/firebase/auth/UserRecordTest.java | 14 +++++++- 5 files changed, 56 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index 7184daa32..b2dca473d 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -225,9 +225,10 @@ public FirebaseToken call() throws Exception { if (checkRevoked) { String uid = firebaseToken.getUid(); UserRecord user = userManager.getUserById(uid); - if (user.getTokensValidAfterTimestamp() - > ((long)firebaseToken.getClaims().get("iat")) * 1000) { - throw new FirebaseAuthException("id-token-revoked", "Firebase auth token revoked"); + long issuedAt = (long) firebaseToken.getClaims().get("iat"); + if (user.getTokensValidAfterTimestamp() > issuedAt * 1000) { + throw new FirebaseAuthException(FirebaseUserManager.ID_TOKEN_REVOKED_ERROR, + "Firebase auth token revoked"); } } return firebaseToken; @@ -237,7 +238,8 @@ public FirebaseToken call() throws Exception { private Task revokeRefreshTokens(String uid) { checkNotDestroyed(); - final UpdateRequest request = new UpdateRequest(uid).setValidSince(new Date().getTime() / 1000); + final UpdateRequest request = new UpdateRequest(uid).setValidSince( + (int) (System.currentTimeMillis() / 1000)); return call(new Callable() { @Override public Void call() throws Exception { @@ -250,9 +252,14 @@ public Void call() throws Exception { /** * Revokes all refresh tokens for the specified user. * - *

In addition to revoking all refresh tokens for a user, all ID tokens issued - * before revocation will also be revoked at the Auth backend. Any request with an - * ID token generated before revocation will be rejected with a token expired error. + *

Updates the user's tokensValidAfterTimestamp to the current UTC second expressed in + * milliseconds since the epoch. It is important that the server on which this is called has its + * clock set correctly and synchronized. + * + *

While this will revoke all sessions for a specified user and disable any new ID tokens for + * existing sessions from getting minted, existing ID tokens may remain active until their + * natural expiration (one hour). + * To verify that ID tokens are revoked, use `verifyIdToken(idToken, true)`. * * @param uid The user id for which tokens are revoked. * @return An {@code ApiFuture} which will complete successfully or if updating the user fails, @@ -277,6 +284,9 @@ public ApiFuture revokeRefreshTokensAsync(String uid) { *

If the token is valid, the returned Future will complete successfully and provide a * parsed version of the token from which the UID and other claims in the token can be inspected. * If the token is invalid, the future throws an exception indicating the failure. + * + *

This does not check whether a token has been revoked, + * see `verifyIdTokenAsync(token, checkRevoked)` below. * * @param token A Firebase ID Token to verify and parse. * @return An {@code ApiFuture} which will complete successfully with the parsed token, or @@ -298,16 +308,13 @@ public ApiFuture verifyIdTokenAsync(final String token) { * associated with this FirebaseAuth instance (which by default is extracted from your service * account) * - *

If a request was made to check revoked, the issued-at property of the token (like all - * token timestamps, it is in seconds since the epoch) will be compared with the "tokens valid - * after time" property of the user. (Which, like other user property timetamps is in - * milliseconds since the epoch). - * If the token was issued before the valid-after time, it is considered revoked. - * + *

If `checkRevoked` is true, additionally checks if the token has been revoked. + * *

If the token is valid, and not revoked, the returned Future will complete successfully and * provide a parsed version of the token from which the UID and other claims in the token can be * inspected. - * If the token is invalid, the future throws an exception indicating the failure. + * If the token is invalid or has been revoked, the future throws an exception indicating the + * failure. * * @param token A Firebase ID Token to verify and parse. * @param checkRevoked A boolean denoting whether to check if the tokens were revoked. diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index a1a78c7a6..504476400 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -58,6 +58,7 @@ class FirebaseUserManager { static final String USER_NOT_FOUND_ERROR = "user-not-found"; static final String INTERNAL_ERROR = "internal-error"; + static final String ID_TOKEN_REVOKED_ERROR = "id-token-revoked"; // Map of server-side error codes to SDK error codes. // SDK error codes defined at: https://firebase.google.com/docs/auth/admin/errors diff --git a/src/main/java/com/google/firebase/auth/UserRecord.java b/src/main/java/com/google/firebase/auth/UserRecord.java index c8a6880c8..299e4e774 100644 --- a/src/main/java/com/google/firebase/auth/UserRecord.java +++ b/src/main/java/com/google/firebase/auth/UserRecord.java @@ -193,9 +193,10 @@ public UserInfo[] getProviderData() { } /** - * Returns the timestamp beginning with which tokens are valid in seconds since the epoch. + * Returns the timestamp beginning with which tokens are valid in milliseconds since the epoch. + * Truncated to 1 second accuracy. * - * @return the timestamp beginning with which tokens are valid in seconds since the epoch. + * @return the timestamp beginning with which tokens are valid in milliseconds since the epoch. */ public long getTokensValidAfterTimestamp() { return tokensValidAfterTimestamp; @@ -258,6 +259,12 @@ private static void checkCustomClaims(Map customClaims) { } } + private static void checkValidSince(long epochSeconds) { + checkArgument(epochSeconds > 0, + "validSince must be greater than 0 in seconds since the epoch: " + + Long.toString(epochSeconds)); + } + private static String serializeCustomClaims(Map customClaims, JsonFactory jsonFactory) { checkNotNull(jsonFactory, "JsonFactory must not be null"); if (customClaims == null || customClaims.isEmpty()) { @@ -512,8 +519,8 @@ public UpdateRequest setCustomClaims(Map customClaims) { return this; } - @VisibleForTesting UpdateRequest setValidSince(long epochSeconds) { + checkValidSince(epochSeconds); properties.put("validSince", epochSeconds); return this; } diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index a40e89d01..2995fe35a 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -358,28 +358,29 @@ public void testCustomToken() throws Exception { } @Test - public void testVerifyIDToken() throws Exception { - String customToken = auth.createCustomTokenAsync("user_ver").get(); + public void testVerifyIdToken() throws Exception { + String customToken = auth.createCustomTokenAsync("user2").get(); String idToken = signInWithCustomToken(customToken); FirebaseToken decoded = auth.verifyIdTokenAsync(idToken).get(); - assertEquals("user_ver", decoded.getUid()); + assertEquals("user2", decoded.getUid()); decoded = auth.verifyIdTokenAsync(idToken, true).get(); - assertEquals("user_ver", decoded.getUid()); - Thread.sleep(1100); - auth.revokeRefreshTokensAsync("user_ver").get(); + assertEquals("user2", decoded.getUid()); + Thread.sleep(1000); + auth.revokeRefreshTokensAsync("user2").get(); decoded = auth.verifyIdTokenAsync(idToken, false).get(); - assertEquals("user_ver", decoded.getUid()); + assertEquals("user2", decoded.getUid()); try { decoded = auth.verifyIdTokenAsync(idToken, true).get(); fail("expecting exception"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals("id-token-revoked", ((FirebaseAuthException) e.getCause()).getErrorCode()); + assertEquals(FirebaseUserManager.ID_TOKEN_REVOKED_ERROR, + ((FirebaseAuthException) e.getCause()).getErrorCode()); } idToken = signInWithCustomToken(customToken); decoded = auth.verifyIdTokenAsync(idToken, true).get(); - assertEquals("user_ver", decoded.getUid()); - auth.deleteUserAsync("user_ver"); + assertEquals("user2", decoded.getUid()); + auth.deleteUserAsync("user2"); } @Test diff --git a/src/test/java/com/google/firebase/auth/UserRecordTest.java b/src/test/java/com/google/firebase/auth/UserRecordTest.java index cee249b22..ce2e08448 100644 --- a/src/test/java/com/google/firebase/auth/UserRecordTest.java +++ b/src/test/java/com/google/firebase/auth/UserRecordTest.java @@ -195,4 +195,16 @@ private ExportedUserRecord parseExportedUser(String json) throws IOException { .parseAndClose(stream, Charset.defaultCharset(), DownloadAccountResponse.User.class); return new ExportedUserRecord(user, JSON_FACTORY); } -} + + + @Test + public void testInvalidVaidSince() { + UpdateRequest update = new UpdateRequest("test"); + try { + update.setValidSince(-1); + fail("No error thrown for negative time"); + } catch (Exception ignore) { + // expected + } + } +} \ No newline at end of file From ed5765826d9df3599b7aa417cc7e31f72cbeb8bb Mon Sep 17 00:00:00 2001 From: Avishalom Shalit Date: Mon, 5 Feb 2018 11:54:33 -0500 Subject: [PATCH 10/14] clean imports --- src/test/java/com/google/firebase/auth/UserRecordTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/com/google/firebase/auth/UserRecordTest.java b/src/test/java/com/google/firebase/auth/UserRecordTest.java index ce2e08448..881fcbc72 100644 --- a/src/test/java/com/google/firebase/auth/UserRecordTest.java +++ b/src/test/java/com/google/firebase/auth/UserRecordTest.java @@ -17,7 +17,6 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; -import java.util.Date; import org.junit.Test; public class UserRecordTest { From 4b0a0fcf3e279b802d4d19863734646a34fde58a Mon Sep 17 00:00:00 2001 From: Avishalom Shalit Date: Mon, 5 Feb 2018 12:05:28 -0500 Subject: [PATCH 11/14] tests --- .../java/com/google/firebase/auth/FirebaseUserManagerTest.java | 1 + src/test/java/com/google/firebase/auth/UserRecordTest.java | 1 + 2 files changed, 2 insertions(+) diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index 85a8d7fed..f1e92ce7f 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -776,6 +776,7 @@ private void checkUserRecord(UserRecord userRecord) { assertEquals(2, userRecord.getProviderData().length); assertFalse(userRecord.isDisabled()); assertTrue(userRecord.isEmailVerified()); + assertEquals(1494364393000L, userRecord.getTokensValidAfterTimestamp()); UserInfo provider = userRecord.getProviderData()[0]; assertEquals("testuser@example.com", provider.getUid()); diff --git a/src/test/java/com/google/firebase/auth/UserRecordTest.java b/src/test/java/com/google/firebase/auth/UserRecordTest.java index 881fcbc72..c7d9b3262 100644 --- a/src/test/java/com/google/firebase/auth/UserRecordTest.java +++ b/src/test/java/com/google/firebase/auth/UserRecordTest.java @@ -165,6 +165,7 @@ public void testExportedUserUidOnly() throws IOException { assertEquals(0, userRecord.getProviderData().length); assertNull(userRecord.getPasswordHash()); assertNull(userRecord.getPasswordSalt()); + assertEquals(0L, userRecord.getTokensValidAfterTimestamp()); } @Test From 107e6ab2f4e380955a0a90c4972bf6fa7e80cc2c Mon Sep 17 00:00:00 2001 From: Avishalom Shalit Date: Mon, 5 Feb 2018 15:01:53 -0500 Subject: [PATCH 12/14] fix PR comments --- .../google/firebase/auth/FirebaseAuth.java | 20 +++++++++---------- .../com/google/firebase/auth/UserRecord.java | 14 ++++++------- .../google/firebase/auth/UserRecordTest.java | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index b2dca473d..6278cddab 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -227,8 +227,8 @@ public FirebaseToken call() throws Exception { UserRecord user = userManager.getUserById(uid); long issuedAt = (long) firebaseToken.getClaims().get("iat"); if (user.getTokensValidAfterTimestamp() > issuedAt * 1000) { - throw new FirebaseAuthException(FirebaseUserManager.ID_TOKEN_REVOKED_ERROR, - "Firebase auth token revoked"); + throw new FirebaseAuthException(FirebaseUserManager.ID_TOKEN_REVOKED_ERROR, + "Firebase auth token revoked"); } } return firebaseToken; @@ -237,9 +237,9 @@ public FirebaseToken call() throws Exception { } private Task revokeRefreshTokens(String uid) { - checkNotDestroyed(); - final UpdateRequest request = new UpdateRequest(uid).setValidSince( - (int) (System.currentTimeMillis() / 1000)); + checkNotDestroyed(); + int currentTimeSeconds = (int) (System.currentTimeMillis() / 1000); + final UpdateRequest request = new UpdateRequest(uid).setValidSince(currentTimeSeconds); return call(new Callable() { @Override public Void call() throws Exception { @@ -252,9 +252,9 @@ public Void call() throws Exception { /** * Revokes all refresh tokens for the specified user. * - *

Updates the user's tokensValidAfterTimestamp to the current UTC second expressed in - * milliseconds since the epoch. It is important that the server on which this is called has its - * clock set correctly and synchronized. + *

Updates the user's tokensValidAfterTimestamp to the current UTC time expressed in + * milliseconds since the epoch and truncated to 1 second accuracy. It is important that the + * server on which this is called has its clock set correctly and synchronized. * *

While this will revoke all sessions for a specified user and disable any new ID tokens for * existing sessions from getting minted, existing ID tokens may remain active until their @@ -285,8 +285,8 @@ public ApiFuture revokeRefreshTokensAsync(String uid) { * parsed version of the token from which the UID and other claims in the token can be inspected. * If the token is invalid, the future throws an exception indicating the failure. * - *

This does not check whether a token has been revoked, - * see `verifyIdTokenAsync(token, checkRevoked)` below. + *

This does not check whether a token has been revoked. + * See `verifyIdTokenAsync(token, checkRevoked)` below. * * @param token A Firebase ID Token to verify and parse. * @return An {@code ApiFuture} which will complete successfully with the parsed token, or diff --git a/src/main/java/com/google/firebase/auth/UserRecord.java b/src/main/java/com/google/firebase/auth/UserRecord.java index 299e4e774..08efa45bf 100644 --- a/src/main/java/com/google/firebase/auth/UserRecord.java +++ b/src/main/java/com/google/firebase/auth/UserRecord.java @@ -193,10 +193,11 @@ public UserInfo[] getProviderData() { } /** - * Returns the timestamp beginning with which tokens are valid in milliseconds since the epoch. - * Truncated to 1 second accuracy. - * - * @return the timestamp beginning with which tokens are valid in milliseconds since the epoch. + * Returns a timestamp in milliseconds since epoch, truncated down to the closest second. + * Tokens minted before this timestamp are considered invalid. + * + * @return Timestamp in milliseconds since the epoch. Tokens minted before this timestamp are + * considered invalid. */ public long getTokensValidAfterTimestamp() { return tokensValidAfterTimestamp; @@ -260,9 +261,8 @@ private static void checkCustomClaims(Map customClaims) { } private static void checkValidSince(long epochSeconds) { - checkArgument(epochSeconds > 0, - "validSince must be greater than 0 in seconds since the epoch: " - + Long.toString(epochSeconds)); + checkArgument(epochSeconds > 0, "validSince (seconds since epoch) must be greater than 0: " + + Long.toString(epochSeconds)); } private static String serializeCustomClaims(Map customClaims, JsonFactory jsonFactory) { diff --git a/src/test/java/com/google/firebase/auth/UserRecordTest.java b/src/test/java/com/google/firebase/auth/UserRecordTest.java index c7d9b3262..22911d537 100644 --- a/src/test/java/com/google/firebase/auth/UserRecordTest.java +++ b/src/test/java/com/google/firebase/auth/UserRecordTest.java @@ -207,4 +207,4 @@ public void testInvalidVaidSince() { // expected } } -} \ No newline at end of file +} From e41773ed8ea8a2df176ad591cde9423f3a57b03b Mon Sep 17 00:00:00 2001 From: Avishalom Shalit Date: Mon, 5 Feb 2018 15:22:31 -0500 Subject: [PATCH 13/14] lint --- src/main/java/com/google/firebase/auth/UserRecord.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/google/firebase/auth/UserRecord.java b/src/main/java/com/google/firebase/auth/UserRecord.java index 08efa45bf..cc27bd197 100644 --- a/src/main/java/com/google/firebase/auth/UserRecord.java +++ b/src/main/java/com/google/firebase/auth/UserRecord.java @@ -197,7 +197,7 @@ public UserInfo[] getProviderData() { * Tokens minted before this timestamp are considered invalid. * * @return Timestamp in milliseconds since the epoch. Tokens minted before this timestamp are - * considered invalid. + * considered invalid. */ public long getTokensValidAfterTimestamp() { return tokensValidAfterTimestamp; From 97306fe3e3fc09e8e67af6be8d30dc7817f643ca Mon Sep 17 00:00:00 2001 From: Avishalom Shalit Date: Mon, 5 Feb 2018 15:50:50 -0500 Subject: [PATCH 14/14] changelog --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 109c30be7..e96929966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Unreleased +### Token revokaction +- [added] The ['verifyIdToken(...)'](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseAuth.html#verifyIdTokenAsync) + method has an added signature that accepts a boolean `checkRevoked` parameter. When `true`, an + additional check is performed to see whether the token has been revoked. +- [added] A new method ['FirebaseAuth.revokeRefreshTokens(uid)'](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseAuth.html#revokeRefreshTokens) + has been added to invalidate all tokens issued to a user before the current second. +- [added] A new getter `getTokensValidAfterMillis` has been added to the + ['UserRecord'](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/UserRecord), + which denotes the time in epoch milliseconds before which tokens are not valid. This is truncated to 1000 milliseconds. ### Initialization - [fixed] The [`FirebaseOptions.Builder.setStorageBucket()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/FirebaseOptions.Builder.html#setStorageBucket(java.lang.String))