@@ -18,8 +18,13 @@ import { AuthClientErrorCode, FirebaseAuthError, ErrorInfo } from '../utils/erro
1818import * as util from '../utils/index' ;
1919import * as validator from '../utils/validator' ;
2020import * 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' ;
2328import { FirebaseApp } from '../firebase-app' ;
2429import { auth } from './index' ;
2530
@@ -70,15 +75,14 @@ export interface FirebaseTokenInfo {
7075}
7176
7277/**
73- * Class for verifying general purpose Firebase JWTs. This verifies ID tokens and session cookies.
78+ * Class for verifying ID tokens and session cookies.
7479 */
7580export class FirebaseTokenVerifier {
76- private publicKeys : { [ key : string ] : string } ;
77- private publicKeysExpireAt : number ;
7881 private readonly shortNameArticle : string ;
79- private readonly jwtDecoder : JwtDecoder ;
82+ private readonly signatureVerifier : PublicKeySignatureVerifier ;
83+ private readonly emulatorSignatureVerifier : EmulatorSignatureVerifier ;
8084
81- constructor ( private clientCertUrl : string , private algorithm : jwt . Algorithm ,
85+ constructor ( clientCertUrl : string , private algorithm : jwt . Algorithm ,
8286 private issuer : string , private tokenInfo : FirebaseTokenInfo ,
8387 private readonly app : FirebaseApp ) {
8488
@@ -129,7 +133,9 @@ export class FirebaseTokenVerifier {
129133 ) ;
130134 }
131135 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 ( ) ;
133139
134140 // For backward compatibility, the project ID is validated in the verification call.
135141 }
@@ -152,8 +158,10 @@ export class FirebaseTokenVerifier {
152158
153159 return util . findProjectId ( this . app )
154160 . 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 ) ;
157165 return Promise . all ( [
158166 fullDecodedToken ,
159167 this . verifySignature ( jwtToken , fullDecodedToken , isEmulator )
@@ -166,25 +174,27 @@ export class FirebaseTokenVerifier {
166174 } ) ;
167175 }
168176
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+ } ) ;
185195 }
186196
187- private validateJWT (
197+ private validateToken (
188198 fullDecodedToken : DecodedToken ,
189199 projectId : string | null ,
190200 isEmulator : boolean ) : void {
@@ -247,113 +257,42 @@ export class FirebaseTokenVerifier {
247257 private verifySignature ( jwtToken : string , decodeToken : DecodedToken , isEmulator : boolean ) :
248258 Promise < void > {
249259 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+ } ) ;
252264 }
253265
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+ } ) ;
269270 }
270271
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 {
279273 const verifyJwtTokenDocsMessage = ` See ${ this . tokenInfo . url } ` +
280274 `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 ) ;
314277 }
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 ) ;
357296 }
358297}
359298
0 commit comments