@@ -18,8 +18,13 @@ import { AuthClientErrorCode, FirebaseAuthError, ErrorInfo } from '../utils/erro
18
18
import * as util from '../utils/index' ;
19
19
import * as validator from '../utils/validator' ;
20
20
import * as jwt from 'jsonwebtoken' ;
21
- import { HttpClient , HttpRequestConfig , HttpError } from '../utils/api-request' ;
22
- import { DecodedToken , JwtDecoder , JwtDecoderError , JwtDecoderErrorCode } from '../utils/jwt-decoder' ;
21
+ import {
22
+ DecodedToken , decodeJwt , JwtDecoderError , JwtDecoderErrorCode
23
+ } from '../utils/jwt-decoder' ;
24
+ import {
25
+ EmulatorSignatureVerifier , NO_MATCHING_KID_ERROR_MESSAGE ,
26
+ PublicKeySignatureVerifier , SignatureVerifierError , SignatureVerifierErrorCode
27
+ } from '../utils/jwt-signature-verifier' ;
23
28
import { FirebaseApp } from '../firebase-app' ;
24
29
import { auth } from './index' ;
25
30
@@ -70,15 +75,14 @@ export interface FirebaseTokenInfo {
70
75
}
71
76
72
77
/**
73
- * Class for verifying general purpose Firebase JWTs. This verifies ID tokens and session cookies.
78
+ * Class for verifying ID tokens and session cookies.
74
79
*/
75
80
export class FirebaseTokenVerifier {
76
- private publicKeys : { [ key : string ] : string } ;
77
- private publicKeysExpireAt : number ;
78
81
private readonly shortNameArticle : string ;
79
- private readonly jwtDecoder : JwtDecoder ;
82
+ private readonly signatureVerifier : PublicKeySignatureVerifier ;
83
+ private readonly emulatorSignatureVerifier : EmulatorSignatureVerifier ;
80
84
81
- constructor ( private clientCertUrl : string , private algorithm : jwt . Algorithm ,
85
+ constructor ( clientCertUrl : string , private algorithm : jwt . Algorithm ,
82
86
private issuer : string , private tokenInfo : FirebaseTokenInfo ,
83
87
private readonly app : FirebaseApp ) {
84
88
@@ -129,7 +133,9 @@ export class FirebaseTokenVerifier {
129
133
) ;
130
134
}
131
135
this . shortNameArticle = tokenInfo . shortName . charAt ( 0 ) . match ( / [ a e i o u ] / i) ? 'an' : 'a' ;
132
- this . jwtDecoder = new JwtDecoder ( algorithm ) ;
136
+
137
+ this . signatureVerifier = new PublicKeySignatureVerifier ( clientCertUrl , algorithm , app ) ;
138
+ this . emulatorSignatureVerifier = new EmulatorSignatureVerifier ( ) ;
133
139
134
140
// For backward compatibility, the project ID is validated in the verification call.
135
141
}
@@ -152,8 +158,10 @@ export class FirebaseTokenVerifier {
152
158
153
159
return util . findProjectId ( this . app )
154
160
. then ( ( projectId ) => {
155
- const fullDecodedToken = this . safeDecode ( jwtToken ) ;
156
- this . validateJWT ( fullDecodedToken , projectId , isEmulator ) ;
161
+ return Promise . all ( [ this . safeDecode ( jwtToken ) , projectId ] ) ;
162
+ } )
163
+ . then ( ( [ fullDecodedToken , projectId ] ) => {
164
+ this . validateToken ( fullDecodedToken , projectId , isEmulator ) ;
157
165
return Promise . all ( [
158
166
fullDecodedToken ,
159
167
this . verifySignature ( jwtToken , fullDecodedToken , isEmulator )
@@ -166,25 +174,27 @@ export class FirebaseTokenVerifier {
166
174
} ) ;
167
175
}
168
176
169
- private safeDecode ( jwtToken : string ) : DecodedToken {
170
- try {
171
- return this . jwtDecoder . decodeToken ( jwtToken ) ;
172
- } catch ( err ) {
173
- if ( ! ( err instanceof JwtDecoderError ) ) {
174
- return err ;
175
- }
176
- if ( err . code == JwtDecoderErrorCode . INVALID_ARGUMENT ) {
177
- const verifyJwtTokenDocsMessage = ` See ${ this . tokenInfo . url } ` +
178
- `for details on how to retrieve ${ this . shortNameArticle } ${ this . tokenInfo . shortName } .` ;
179
- const errorMessage = `Decoding ${ this . tokenInfo . jwtName } failed. Make sure you passed the entire string JWT ` +
180
- `which represents ${ this . shortNameArticle } ${ this . tokenInfo . shortName } .` + verifyJwtTokenDocsMessage ;
181
- throw new FirebaseAuthError ( AuthClientErrorCode . INVALID_ARGUMENT , errorMessage ) ;
182
- }
183
- throw new FirebaseAuthError ( AuthClientErrorCode . INTERNAL_ERROR , err . message ) ;
184
- }
177
+ private safeDecode ( jwtToken : string ) : Promise < DecodedToken > {
178
+ return decodeJwt ( jwtToken )
179
+ . catch ( ( err ) => {
180
+ if ( ! ( err instanceof JwtDecoderError ) ) {
181
+ return Promise . reject ( err ) ;
182
+ }
183
+ if ( err . code == JwtDecoderErrorCode . INVALID_ARGUMENT ) {
184
+ const verifyJwtTokenDocsMessage = ` See ${ this . tokenInfo . url } ` +
185
+ `for details on how to retrieve ${ this . shortNameArticle } ${ this . tokenInfo . shortName } .` ;
186
+ const errorMessage = `Decoding ${ this . tokenInfo . jwtName } failed. Make sure you passed ` +
187
+ `the entire string JWT which represents ${ this . shortNameArticle } ` +
188
+ `${ this . tokenInfo . shortName } .` + verifyJwtTokenDocsMessage ;
189
+ return Promise . reject (
190
+ new FirebaseAuthError ( AuthClientErrorCode . INVALID_ARGUMENT , errorMessage ) ) ;
191
+ }
192
+ return Promise . reject (
193
+ new FirebaseAuthError ( AuthClientErrorCode . INTERNAL_ERROR , err . message ) ) ;
194
+ } ) ;
185
195
}
186
196
187
- private validateJWT (
197
+ private validateToken (
188
198
fullDecodedToken : DecodedToken ,
189
199
projectId : string | null ,
190
200
isEmulator : boolean ) : void {
@@ -247,113 +257,42 @@ export class FirebaseTokenVerifier {
247
257
private verifySignature ( jwtToken : string , decodeToken : DecodedToken , isEmulator : boolean ) :
248
258
Promise < void > {
249
259
if ( isEmulator ) {
250
- // Signature checks skipped for emulator; no need to fetch public keys.
251
- return this . verifyJwtSignatureWithKey ( jwtToken , null ) ;
260
+ return this . emulatorSignatureVerifier . verify ( jwtToken )
261
+ . catch ( ( error ) => {
262
+ return Promise . reject ( this . mapSignatureVerifierErrorToAuthError ( error ) ) ;
263
+ } ) ;
252
264
}
253
265
254
- return this . fetchPublicKeys ( ) . then ( ( publicKeys ) => {
255
- if ( ! Object . prototype . hasOwnProperty . call ( publicKeys , decodeToken . header . kid ) ) {
256
- return Promise . reject (
257
- new FirebaseAuthError (
258
- AuthClientErrorCode . INVALID_ARGUMENT ,
259
- `${ this . tokenInfo . jwtName } has "kid" claim which does not correspond to a known public key. ` +
260
- `Most likely the ${ this . tokenInfo . shortName } is expired, so get a fresh token from your ` +
261
- 'client app and try again.' ,
262
- ) ,
263
- ) ;
264
- } else {
265
- return this . verifyJwtSignatureWithKey ( jwtToken , publicKeys [ decodeToken . header . kid ] ) ;
266
- }
267
-
268
- } ) ;
266
+ return this . signatureVerifier . verify ( jwtToken )
267
+ . catch ( ( error ) => {
268
+ return Promise . reject ( this . mapSignatureVerifierErrorToAuthError ( error ) ) ;
269
+ } ) ;
269
270
}
270
271
271
- /**
272
- * Verifies the JWT signature using the provided public key.
273
- * @param {string } jwtToken The JWT token to verify.
274
- * @param {string } publicKey The public key certificate.
275
- * @return {Promise<void> } A promise that resolves with the decoded JWT claims on successful
276
- * verification.
277
- */
278
- private verifyJwtSignatureWithKey ( jwtToken : string , publicKey : string | null ) : Promise < void > {
272
+ private mapSignatureVerifierErrorToAuthError ( error : SignatureVerifierError ) : Error {
279
273
const verifyJwtTokenDocsMessage = ` See ${ this . tokenInfo . url } ` +
280
274
`for details on how to retrieve ${ this . shortNameArticle } ${ this . tokenInfo . shortName } .` ;
281
- const errorMessage = `${ this . tokenInfo . jwtName } has invalid signature.` + verifyJwtTokenDocsMessage ;
282
- const invalidTokenError = new FirebaseAuthError ( AuthClientErrorCode . INVALID_ARGUMENT , errorMessage ) ;
283
- return new Promise ( ( resolve , reject ) => {
284
- this . jwtDecoder . isSignatureValid ( jwtToken , publicKey )
285
- . then ( isValid => {
286
- return isValid ? resolve ( ) : reject ( invalidTokenError ) ;
287
- } )
288
- . catch ( error => {
289
- if ( ! ( error instanceof JwtDecoderError ) ) {
290
- return reject ( error ) ;
291
- }
292
- if ( error . code === JwtDecoderErrorCode . TOKEN_EXPIRED ) {
293
- const errorMessage = `${ this . tokenInfo . jwtName } has expired. Get a fresh ${ this . tokenInfo . shortName } ` +
294
- ` from your client app and try again (auth/${ this . tokenInfo . expiredErrorCode . code } ).` +
295
- verifyJwtTokenDocsMessage ;
296
- return reject ( new FirebaseAuthError ( this . tokenInfo . expiredErrorCode , errorMessage ) ) ;
297
- }
298
- return reject ( new FirebaseAuthError ( AuthClientErrorCode . INVALID_ARGUMENT , error . message ) ) ;
299
- } ) ;
300
- } ) ;
301
- }
302
-
303
- /**
304
- * Fetches the public keys for the Google certs.
305
- *
306
- * @return {Promise<object> } A promise fulfilled with public keys for the Google certs.
307
- */
308
- private fetchPublicKeys ( ) : Promise < { [ key : string ] : string } > {
309
- const publicKeysExist = ( typeof this . publicKeys !== 'undefined' ) ;
310
- const publicKeysExpiredExists = ( typeof this . publicKeysExpireAt !== 'undefined' ) ;
311
- const publicKeysStillValid = ( publicKeysExpiredExists && Date . now ( ) < this . publicKeysExpireAt ) ;
312
- if ( publicKeysExist && publicKeysStillValid ) {
313
- return Promise . resolve ( this . publicKeys ) ;
275
+ if ( ! ( error instanceof SignatureVerifierError ) ) {
276
+ return ( error ) ;
314
277
}
315
-
316
- const client = new HttpClient ( ) ;
317
- const request : HttpRequestConfig = {
318
- method : 'GET' ,
319
- url : this . clientCertUrl ,
320
- httpAgent : this . app . options . httpAgent ,
321
- } ;
322
- return client . send ( request ) . then ( ( resp ) => {
323
- if ( ! resp . isJson ( ) || resp . data . error ) {
324
- // Treat all non-json messages and messages with an 'error' field as
325
- // error responses.
326
- throw new HttpError ( resp ) ;
327
- }
328
- if ( Object . prototype . hasOwnProperty . call ( resp . headers , 'cache-control' ) ) {
329
- const cacheControlHeader : string = resp . headers [ 'cache-control' ] ;
330
- const parts = cacheControlHeader . split ( ',' ) ;
331
- parts . forEach ( ( part ) => {
332
- const subParts = part . trim ( ) . split ( '=' ) ;
333
- if ( subParts [ 0 ] === 'max-age' ) {
334
- const maxAge : number = + subParts [ 1 ] ;
335
- this . publicKeysExpireAt = Date . now ( ) + ( maxAge * 1000 ) ;
336
- }
337
- } ) ;
338
- }
339
- this . publicKeys = resp . data ;
340
- return resp . data ;
341
- } ) . catch ( ( err ) => {
342
- if ( err instanceof HttpError ) {
343
- let errorMessage = 'Error fetching public keys for Google certs: ' ;
344
- const resp = err . response ;
345
- if ( resp . isJson ( ) && resp . data . error ) {
346
- errorMessage += `${ resp . data . error } ` ;
347
- if ( resp . data . error_description ) {
348
- errorMessage += ' (' + resp . data . error_description + ')' ;
349
- }
350
- } else {
351
- errorMessage += `${ resp . text } ` ;
352
- }
353
- throw new FirebaseAuthError ( AuthClientErrorCode . INTERNAL_ERROR , errorMessage ) ;
354
- }
355
- throw err ;
356
- } ) ;
278
+ if ( error . code === SignatureVerifierErrorCode . TOKEN_EXPIRED ) {
279
+ const errorMessage = `${ this . tokenInfo . jwtName } has expired. Get a fresh ${ this . tokenInfo . shortName } ` +
280
+ ` from your client app and try again (auth/${ this . tokenInfo . expiredErrorCode . code } ).` +
281
+ verifyJwtTokenDocsMessage ;
282
+ return new FirebaseAuthError ( this . tokenInfo . expiredErrorCode , errorMessage ) ;
283
+ }
284
+ else if ( error . code === SignatureVerifierErrorCode . INVALID_TOKEN ) {
285
+ const errorMessage = `${ this . tokenInfo . jwtName } has invalid signature.` + verifyJwtTokenDocsMessage ;
286
+ return new FirebaseAuthError ( AuthClientErrorCode . INVALID_ARGUMENT , errorMessage ) ;
287
+ }
288
+ else if ( error . code === SignatureVerifierErrorCode . INVALID_ARGUMENT &&
289
+ error . message === NO_MATCHING_KID_ERROR_MESSAGE ) {
290
+ const errorMessage = `${ this . tokenInfo . jwtName } has "kid" claim which does not ` +
291
+ `correspond to a known public key. Most likely the ${ this . tokenInfo . shortName } ` +
292
+ 'is expired, so get a fresh token from your client app and try again.' ;
293
+ return new FirebaseAuthError ( AuthClientErrorCode . INVALID_ARGUMENT , errorMessage ) ;
294
+ }
295
+ return new FirebaseAuthError ( AuthClientErrorCode . INVALID_ARGUMENT , error . message ) ;
357
296
}
358
297
}
359
298
0 commit comments