28
28
import com .google .api .client .util .ArrayMap ;
29
29
import com .google .common .base .Joiner ;
30
30
import com .google .common .base .Strings ;
31
+ import com .google .firebase .ErrorCode ;
31
32
import java .io .IOException ;
32
33
import java .math .BigDecimal ;
33
34
import java .security .GeneralSecurityException ;
34
35
import java .security .PublicKey ;
36
+ import java .util .List ;
35
37
36
38
/**
37
39
* The default implementation of the {@link FirebaseTokenVerifier} interface. Uses the Google API
@@ -43,8 +45,6 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier {
43
45
private static final String RS256 = "RS256" ;
44
46
private static final String FIREBASE_AUDIENCE =
45
47
"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" ;
48
48
49
49
private final JsonFactory jsonFactory ;
50
50
private final GooglePublicKeysManager publicKeysManager ;
@@ -53,6 +53,8 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier {
53
53
private final String shortName ;
54
54
private final String articledShortName ;
55
55
private final String docUrl ;
56
+ private final AuthErrorCode invalidTokenErrorCode ;
57
+ private final AuthErrorCode expiredTokenErrorCode ;
56
58
57
59
private FirebaseTokenVerifierImpl (Builder builder ) {
58
60
this .jsonFactory = checkNotNull (builder .jsonFactory );
@@ -65,6 +67,8 @@ private FirebaseTokenVerifierImpl(Builder builder) {
65
67
this .shortName = builder .shortName ;
66
68
this .articledShortName = prefixWithIndefiniteArticle (this .shortName );
67
69
this .docUrl = builder .docUrl ;
70
+ this .invalidTokenErrorCode = checkNotNull (builder .invalidTokenErrorCode );
71
+ this .expiredTokenErrorCode = checkNotNull (builder .expiredTokenErrorCode );
68
72
}
69
73
70
74
/**
@@ -137,38 +141,28 @@ private IdToken parse(String token) throws FirebaseAuthException {
137
141
shortName ,
138
142
docUrl ,
139
143
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 );
149
145
}
150
146
}
151
147
152
148
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 );
164
155
}
165
156
}
166
157
167
- private String getErrorIfContentInvalid (final IdToken idToken ) {
158
+ private void checkContents (final IdToken idToken ) throws FirebaseAuthException {
168
159
final Header header = idToken .getHeader ();
169
160
final Payload payload = idToken .getPayload ();
170
161
162
+ final long currentTimeMillis = idTokenVerifier .getClock ().currentTimeMillis ();
171
163
String errorMessage = null ;
164
+ AuthErrorCode errorCode = invalidTokenErrorCode ;
165
+
172
166
if (header .getKeyId () == null ) {
173
167
errorMessage = getErrorForTokenWithoutKid (header , payload );
174
168
} else if (!RS256 .equals (header .getAlgorithm ())) {
@@ -203,14 +197,35 @@ private String getErrorIfContentInvalid(final IdToken idToken) {
203
197
errorMessage = String .format (
204
198
"Firebase %s has \" sub\" (subject) claim longer than 128 characters." ,
205
199
shortName );
206
- } else if (!verifyTimestamps (idToken )) {
200
+ } else if (!idToken .verifyExpirationTime (
201
+ currentTimeMillis , idTokenVerifier .getAcceptableTimeSkewSeconds ())) {
207
202
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." ,
209
204
shortName ,
210
205
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 );
211
218
}
219
+ }
220
+
221
+ private FirebaseAuthException newException (String message , AuthErrorCode errorCode ) {
222
+ return newException (message , errorCode , null );
223
+ }
212
224
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 );
214
229
}
215
230
216
231
private String getVerifyTokenMessage () {
@@ -224,15 +239,44 @@ private String getVerifyTokenMessage() {
224
239
* Verifies the cryptographic signature on the FirebaseToken. Can block on a web request to fetch
225
240
* the keys if they have expired.
226
241
*/
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 )) {
230
245
return true ;
231
246
}
232
247
}
248
+
233
249
return false ;
234
250
}
235
251
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
+
236
280
private String getErrorForTokenWithoutKid (IdToken .Header header , IdToken .Payload payload ) {
237
281
if (isCustomToken (payload )) {
238
282
return String .format ("%s expects %s, but was given a custom token." ,
@@ -255,11 +299,6 @@ private String getProjectIdMatchMessage() {
255
299
shortName );
256
300
}
257
301
258
- private boolean verifyTimestamps (IdToken token ) {
259
- long currentTimeMillis = idTokenVerifier .getClock ().currentTimeMillis ();
260
- return token .verifyTime (currentTimeMillis , idTokenVerifier .getAcceptableTimeSkewSeconds ());
261
- }
262
-
263
302
private boolean isCustomToken (IdToken .Payload payload ) {
264
303
return FIREBASE_AUDIENCE .equals (payload .getAudience ());
265
304
}
@@ -290,6 +329,8 @@ static final class Builder {
290
329
private String shortName ;
291
330
private IdTokenVerifier idTokenVerifier ;
292
331
private String docUrl ;
332
+ private AuthErrorCode invalidTokenErrorCode ;
333
+ private AuthErrorCode expiredTokenErrorCode ;
293
334
294
335
private Builder () { }
295
336
@@ -323,6 +364,16 @@ Builder setDocUrl(String docUrl) {
323
364
return this ;
324
365
}
325
366
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
+
326
377
FirebaseTokenVerifierImpl build () {
327
378
return new FirebaseTokenVerifierImpl (this );
328
379
}
0 commit comments