Skip to content

Commit 04144f3

Browse files
authored
Error handling revamp for token verification APIs (#362)
* Error handling revamp for token verification APIs * Updated javadocs
1 parent 2a6f45f commit 04144f3

File tree

9 files changed

+451
-153
lines changed

9 files changed

+451
-153
lines changed

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,56 @@
2121
*/
2222
public enum AuthErrorCode {
2323

24+
/**
25+
* Failed to retrieve public key certificates required to verify JWTs.
26+
*/
27+
CERTIFICATE_FETCH_FAILED,
28+
2429
/**
2530
* A user already exists with the provided email.
2631
*/
2732
EMAIL_ALREADY_EXISTS,
2833

34+
/**
35+
* The specified ID token is expired.
36+
*/
37+
EXPIRED_ID_TOKEN,
38+
39+
/**
40+
* The specified session cookie is expired.
41+
*/
42+
EXPIRED_SESSION_COOKIE,
43+
2944
/**
3045
* The provided dynamic link domain is not configured or authorized for the current project.
3146
*/
3247
INVALID_DYNAMIC_LINK_DOMAIN,
3348

49+
/**
50+
* The specified ID token is invalid.
51+
*/
52+
INVALID_ID_TOKEN,
53+
54+
/**
55+
* The specified session cookie is invalid.
56+
*/
57+
INVALID_SESSION_COOKIE,
58+
3459
/**
3560
* A user already exists with the provided phone number.
3661
*/
3762
PHONE_NUMBER_ALREADY_EXISTS,
3863

64+
/**
65+
* The specified ID token has been revoked.
66+
*/
67+
REVOKED_ID_TOKEN,
68+
69+
/**
70+
* The specified session cookie has been revoked.
71+
*/
72+
REVOKED_SESSION_COOKIE,
73+
3974
/**
4075
* A user already exists with the provided UID.
4176
*/

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

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
public class FirebaseAuthException extends FirebaseException {
3333

3434
private final AuthErrorCode errorCode;
35-
private final String deprecatedErrorCode;
3635

3736
FirebaseAuthException(
3837
@NonNull ErrorCode errorCode,
@@ -42,12 +41,6 @@ public class FirebaseAuthException extends FirebaseException {
4241
AuthErrorCode authErrorCode) {
4342
super(errorCode, message, cause, response);
4443
this.errorCode = authErrorCode;
45-
this.deprecatedErrorCode = null;
46-
}
47-
48-
@Deprecated
49-
public FirebaseAuthException(@NonNull String errorCode, @NonNull String detailMessage) {
50-
this(errorCode, detailMessage, null);
5144
}
5245

5346
@Deprecated
@@ -56,17 +49,10 @@ public FirebaseAuthException(
5649
super(detailMessage, throwable);
5750
checkArgument(!Strings.isNullOrEmpty(errorCode));
5851
this.errorCode = null;
59-
this.deprecatedErrorCode = errorCode;
6052
}
6153

6254
@Nullable
6355
public AuthErrorCode getAuthErrorCode() {
6456
return errorCode;
6557
}
66-
67-
/** Returns an error code that may provide more information about the error. */
68-
@Deprecated
69-
public String getDeprecatedErrorCode() {
70-
return deprecatedErrorCode;
71-
}
7258
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ static FirebaseTokenVerifierImpl createIdTokenVerifier(FirebaseApp app, Clock cl
8282
.setJsonFactory(app.getOptions().getJsonFactory())
8383
.setPublicKeysManager(publicKeysManager)
8484
.setIdTokenVerifier(idTokenVerifier)
85+
.setInvalidTokenErrorCode(AuthErrorCode.INVALID_ID_TOKEN)
86+
.setExpiredTokenErrorCode(AuthErrorCode.EXPIRED_ID_TOKEN)
8587
.build();
8688
}
8789

@@ -100,6 +102,8 @@ static FirebaseTokenVerifierImpl createSessionCookieVerifier(FirebaseApp app, Cl
100102
.setShortName("session cookie")
101103
.setMethod("verifySessionCookie()")
102104
.setDocUrl("https://firebase.google.com/docs/auth/admin/manage-cookies")
105+
.setInvalidTokenErrorCode(AuthErrorCode.INVALID_SESSION_COOKIE)
106+
.setExpiredTokenErrorCode(AuthErrorCode.EXPIRED_SESSION_COOKIE)
103107
.build();
104108
}
105109

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

Lines changed: 85 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@
2828
import com.google.api.client.util.ArrayMap;
2929
import com.google.common.base.Joiner;
3030
import com.google.common.base.Strings;
31+
import com.google.firebase.ErrorCode;
3132
import java.io.IOException;
3233
import java.math.BigDecimal;
3334
import java.security.GeneralSecurityException;
3435
import java.security.PublicKey;
36+
import java.util.List;
3537

3638
/**
3739
* The default implementation of the {@link FirebaseTokenVerifier} interface. Uses the Google API
@@ -43,8 +45,6 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier {
4345
private static final String RS256 = "RS256";
4446
private static final String FIREBASE_AUDIENCE =
4547
"https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit";
46-
private static final String ERROR_INVALID_CREDENTIAL = "ERROR_INVALID_CREDENTIAL";
47-
private static final String ERROR_RUNTIME_EXCEPTION = "ERROR_RUNTIME_EXCEPTION";
4848

4949
private final JsonFactory jsonFactory;
5050
private final GooglePublicKeysManager publicKeysManager;
@@ -53,6 +53,8 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier {
5353
private final String shortName;
5454
private final String articledShortName;
5555
private final String docUrl;
56+
private final AuthErrorCode invalidTokenErrorCode;
57+
private final AuthErrorCode expiredTokenErrorCode;
5658

5759
private FirebaseTokenVerifierImpl(Builder builder) {
5860
this.jsonFactory = checkNotNull(builder.jsonFactory);
@@ -65,6 +67,8 @@ private FirebaseTokenVerifierImpl(Builder builder) {
6567
this.shortName = builder.shortName;
6668
this.articledShortName = prefixWithIndefiniteArticle(this.shortName);
6769
this.docUrl = builder.docUrl;
70+
this.invalidTokenErrorCode = checkNotNull(builder.invalidTokenErrorCode);
71+
this.expiredTokenErrorCode = checkNotNull(builder.expiredTokenErrorCode);
6872
}
6973

7074
/**
@@ -137,38 +141,28 @@ private IdToken parse(String token) throws FirebaseAuthException {
137141
shortName,
138142
docUrl,
139143
articledShortName);
140-
throw new FirebaseAuthException(ERROR_INVALID_CREDENTIAL, detailedError, e);
141-
}
142-
}
143-
144-
private void checkContents(final IdToken token) throws FirebaseAuthException {
145-
String errorMessage = getErrorIfContentInvalid(token);
146-
if (errorMessage != null) {
147-
String detailedError = String.format("%s %s", errorMessage, getVerifyTokenMessage());
148-
throw new FirebaseAuthException(ERROR_INVALID_CREDENTIAL, detailedError);
144+
throw newException(detailedError, invalidTokenErrorCode, e);
149145
}
150146
}
151147

152148
private void checkSignature(IdToken token) throws FirebaseAuthException {
153-
try {
154-
if (!isSignatureValid(token)) {
155-
throw new FirebaseAuthException(ERROR_INVALID_CREDENTIAL,
156-
String.format(
157-
"Failed to verify the signature of Firebase %s. %s",
158-
shortName,
159-
getVerifyTokenMessage()));
160-
}
161-
} catch (GeneralSecurityException | IOException e) {
162-
throw new FirebaseAuthException(
163-
ERROR_RUNTIME_EXCEPTION, "Error while verifying signature.", e);
149+
if (!isSignatureValid(token)) {
150+
String message = String.format(
151+
"Failed to verify the signature of Firebase %s. %s",
152+
shortName,
153+
getVerifyTokenMessage());
154+
throw newException(message, invalidTokenErrorCode);
164155
}
165156
}
166157

167-
private String getErrorIfContentInvalid(final IdToken idToken) {
158+
private void checkContents(final IdToken idToken) throws FirebaseAuthException {
168159
final Header header = idToken.getHeader();
169160
final Payload payload = idToken.getPayload();
170161

162+
final long currentTimeMillis = idTokenVerifier.getClock().currentTimeMillis();
171163
String errorMessage = null;
164+
AuthErrorCode errorCode = invalidTokenErrorCode;
165+
172166
if (header.getKeyId() == null) {
173167
errorMessage = getErrorForTokenWithoutKid(header, payload);
174168
} else if (!RS256.equals(header.getAlgorithm())) {
@@ -203,14 +197,35 @@ private String getErrorIfContentInvalid(final IdToken idToken) {
203197
errorMessage = String.format(
204198
"Firebase %s has \"sub\" (subject) claim longer than 128 characters.",
205199
shortName);
206-
} else if (!verifyTimestamps(idToken)) {
200+
} else if (!idToken.verifyExpirationTime(
201+
currentTimeMillis, idTokenVerifier.getAcceptableTimeSkewSeconds())) {
207202
errorMessage = String.format(
208-
"Firebase %s has expired or is not yet valid. Get a fresh %s and try again.",
203+
"Firebase %s has expired. Get a fresh %s and try again.",
209204
shortName,
210205
shortName);
206+
// Also set the expired error code.
207+
errorCode = expiredTokenErrorCode;
208+
} else if (!idToken.verifyIssuedAtTime(
209+
currentTimeMillis, idTokenVerifier.getAcceptableTimeSkewSeconds())) {
210+
errorMessage = String.format(
211+
"Firebase %s is not yet valid.",
212+
shortName);
213+
}
214+
215+
if (errorMessage != null) {
216+
String detailedError = String.format("%s %s", errorMessage, getVerifyTokenMessage());
217+
throw newException(detailedError, errorCode);
211218
}
219+
}
220+
221+
private FirebaseAuthException newException(String message, AuthErrorCode errorCode) {
222+
return newException(message, errorCode, null);
223+
}
212224

213-
return errorMessage;
225+
private FirebaseAuthException newException(
226+
String message, AuthErrorCode errorCode, Throwable cause) {
227+
return new FirebaseAuthException(
228+
ErrorCode.INVALID_ARGUMENT, message, cause, null, errorCode);
214229
}
215230

216231
private String getVerifyTokenMessage() {
@@ -224,15 +239,44 @@ private String getVerifyTokenMessage() {
224239
* Verifies the cryptographic signature on the FirebaseToken. Can block on a web request to fetch
225240
* the keys if they have expired.
226241
*/
227-
private boolean isSignatureValid(IdToken token) throws GeneralSecurityException, IOException {
228-
for (PublicKey key : publicKeysManager.getPublicKeys()) {
229-
if (token.verifySignature(key)) {
242+
private boolean isSignatureValid(IdToken token) throws FirebaseAuthException {
243+
for (PublicKey key : fetchPublicKeys()) {
244+
if (isSignatureValid(token, key)) {
230245
return true;
231246
}
232247
}
248+
233249
return false;
234250
}
235251

252+
private boolean isSignatureValid(IdToken token, PublicKey key) throws FirebaseAuthException {
253+
try {
254+
return token.verifySignature(key);
255+
} catch (GeneralSecurityException e) {
256+
// This doesn't happen under usual circumstances. Seems to only happen if the crypto
257+
// setup of the runtime is incorrect in some way.
258+
throw new FirebaseAuthException(
259+
ErrorCode.UNKNOWN,
260+
String.format("Unexpected error while verifying %s: %s", shortName, e.getMessage()),
261+
e,
262+
null,
263+
invalidTokenErrorCode);
264+
}
265+
}
266+
267+
private List<PublicKey> fetchPublicKeys() throws FirebaseAuthException {
268+
try {
269+
return publicKeysManager.getPublicKeys();
270+
} catch (GeneralSecurityException | IOException e) {
271+
throw new FirebaseAuthException(
272+
ErrorCode.UNKNOWN,
273+
"Error while fetching public key certificates: " + e.getMessage(),
274+
e,
275+
null,
276+
AuthErrorCode.CERTIFICATE_FETCH_FAILED);
277+
}
278+
}
279+
236280
private String getErrorForTokenWithoutKid(IdToken.Header header, IdToken.Payload payload) {
237281
if (isCustomToken(payload)) {
238282
return String.format("%s expects %s, but was given a custom token.",
@@ -255,11 +299,6 @@ private String getProjectIdMatchMessage() {
255299
shortName);
256300
}
257301

258-
private boolean verifyTimestamps(IdToken token) {
259-
long currentTimeMillis = idTokenVerifier.getClock().currentTimeMillis();
260-
return token.verifyTime(currentTimeMillis, idTokenVerifier.getAcceptableTimeSkewSeconds());
261-
}
262-
263302
private boolean isCustomToken(IdToken.Payload payload) {
264303
return FIREBASE_AUDIENCE.equals(payload.getAudience());
265304
}
@@ -290,6 +329,8 @@ static final class Builder {
290329
private String shortName;
291330
private IdTokenVerifier idTokenVerifier;
292331
private String docUrl;
332+
private AuthErrorCode invalidTokenErrorCode;
333+
private AuthErrorCode expiredTokenErrorCode;
293334

294335
private Builder() { }
295336

@@ -323,6 +364,16 @@ Builder setDocUrl(String docUrl) {
323364
return this;
324365
}
325366

367+
public Builder setInvalidTokenErrorCode(AuthErrorCode invalidTokenErrorCode) {
368+
this.invalidTokenErrorCode = invalidTokenErrorCode;
369+
return this;
370+
}
371+
372+
public Builder setExpiredTokenErrorCode(AuthErrorCode expiredTokenErrorCode) {
373+
this.expiredTokenErrorCode = expiredTokenErrorCode;
374+
return this;
375+
}
376+
326377
FirebaseTokenVerifierImpl build() {
327378
return new FirebaseTokenVerifierImpl(this);
328379
}

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

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,30 +20,27 @@
2020
import static com.google.common.base.Preconditions.checkNotNull;
2121

2222
import com.google.common.base.Strings;
23+
import com.google.firebase.ErrorCode;
2324

2425
/**
2526
* A decorator for adding token revocation checks to an existing {@link FirebaseTokenVerifier}.
2627
*/
2728
class RevocationCheckDecorator implements FirebaseTokenVerifier {
2829

29-
static final String ID_TOKEN_REVOKED_ERROR = "id-token-revoked";
30-
static final String SESSION_COOKIE_REVOKED_ERROR = "session-cookie-revoked";
31-
3230
private final FirebaseTokenVerifier tokenVerifier;
3331
private final FirebaseUserManager userManager;
34-
private final String errorCode;
32+
private final AuthErrorCode errorCode;
3533
private final String shortName;
3634

3735
private RevocationCheckDecorator(
3836
FirebaseTokenVerifier tokenVerifier,
3937
FirebaseUserManager userManager,
40-
String errorCode,
38+
AuthErrorCode errorCode,
4139
String shortName) {
4240
this.tokenVerifier = checkNotNull(tokenVerifier);
4341
this.userManager = checkNotNull(userManager);
44-
checkArgument(!Strings.isNullOrEmpty(errorCode));
42+
this.errorCode = checkNotNull(errorCode);
4543
checkArgument(!Strings.isNullOrEmpty(shortName));
46-
this.errorCode = errorCode;
4744
this.shortName = shortName;
4845
}
4946

@@ -55,8 +52,14 @@ private RevocationCheckDecorator(
5552
public FirebaseToken verifyToken(String token) throws FirebaseAuthException {
5653
FirebaseToken firebaseToken = tokenVerifier.verifyToken(token);
5754
if (isRevoked(firebaseToken)) {
58-
throw new FirebaseAuthException(errorCode, "Firebase " + shortName + " revoked");
55+
throw new FirebaseAuthException(
56+
ErrorCode.INVALID_ARGUMENT,
57+
"Firebase " + shortName + " is revoked.",
58+
null,
59+
null,
60+
errorCode);
5961
}
62+
6063
return firebaseToken;
6164
}
6265

@@ -69,12 +72,12 @@ private boolean isRevoked(FirebaseToken firebaseToken) throws FirebaseAuthExcept
6972
static RevocationCheckDecorator decorateIdTokenVerifier(
7073
FirebaseTokenVerifier tokenVerifier, FirebaseUserManager userManager) {
7174
return new RevocationCheckDecorator(
72-
tokenVerifier, userManager, ID_TOKEN_REVOKED_ERROR, "id token");
75+
tokenVerifier, userManager, AuthErrorCode.REVOKED_ID_TOKEN, "id token");
7376
}
7477

7578
static RevocationCheckDecorator decorateSessionCookieVerifier(
7679
FirebaseTokenVerifier tokenVerifier, FirebaseUserManager userManager) {
7780
return new RevocationCheckDecorator(
78-
tokenVerifier, userManager, SESSION_COOKIE_REVOKED_ERROR, "session cookie");
81+
tokenVerifier, userManager, AuthErrorCode.REVOKED_SESSION_COOKIE, "session cookie");
7982
}
8083
}

0 commit comments

Comments
 (0)