@@ -19,6 +19,7 @@ import * as util from '../utils/index';
1919import * as validator from '../utils/validator' ;
2020import * as jwt from 'jsonwebtoken' ;
2121import { HttpClient , HttpRequestConfig , HttpError } from '../utils/api-request' ;
22+ import { DecodedToken , JwtDecoder , JwtDecoderError , JwtDecoderErrorCode } from '../utils/jwt-decoder' ;
2223import { FirebaseApp } from '../firebase-app' ;
2324import { auth } from './index' ;
2425
@@ -75,6 +76,7 @@ export class FirebaseTokenVerifier {
7576 private publicKeys : { [ key : string ] : string } ;
7677 private publicKeysExpireAt : number ;
7778 private readonly shortNameArticle : string ;
79+ private readonly jwtDecoder : JwtDecoder ;
7880
7981 constructor ( private clientCertUrl : string , private algorithm : jwt . Algorithm ,
8082 private issuer : string , private tokenInfo : FirebaseTokenInfo ,
@@ -127,6 +129,7 @@ export class FirebaseTokenVerifier {
127129 ) ;
128130 }
129131 this . shortNameArticle = tokenInfo . shortName . charAt ( 0 ) . match ( / [ a e i o u ] / i) ? 'an' : 'a' ;
132+ this . jwtDecoder = new JwtDecoder ( algorithm ) ;
130133
131134 // For backward compatibility, the project ID is validated in the verification call.
132135 }
@@ -149,27 +152,50 @@ export class FirebaseTokenVerifier {
149152
150153 return util . findProjectId ( this . app )
151154 . then ( ( projectId ) => {
152- return this . verifyJWTWithProjectId ( jwtToken , projectId , isEmulator ) ;
155+ const fullDecodedToken = this . safeDecode ( jwtToken ) ;
156+ this . validateJWT ( fullDecodedToken , projectId , isEmulator ) ;
157+ return Promise . all ( [
158+ fullDecodedToken ,
159+ this . verifySignature ( jwtToken , fullDecodedToken , isEmulator )
160+ ] ) ;
161+ } )
162+ . then ( ( [ fullDecodedToken ] ) => {
163+ const decodedIdToken = fullDecodedToken . payload as DecodedIdToken ;
164+ decodedIdToken . uid = decodedIdToken . sub ;
165+ return decodedIdToken ;
153166 } ) ;
154167 }
155168
156- private verifyJWTWithProjectId (
157- jwtToken : string ,
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+ }
185+ }
186+
187+ private validateJWT (
188+ fullDecodedToken : DecodedToken ,
158189 projectId : string | null ,
159- isEmulator : boolean
160- ) : Promise < DecodedIdToken > {
190+ isEmulator : boolean ) : void {
161191 if ( ! validator . isNonEmptyString ( projectId ) ) {
162192 throw new FirebaseAuthError (
163193 AuthClientErrorCode . INVALID_CREDENTIAL ,
164194 'Must initialize app with a cert credential or set your Firebase project ID as the ' +
165195 `GOOGLE_CLOUD_PROJECT environment variable to call ${ this . tokenInfo . verifyApiName } .` ,
166196 ) ;
167197 }
168-
169- const fullDecodedToken : any = jwt . decode ( jwtToken , {
170- complete : true ,
171- } ) ;
172-
198+
173199 const header = fullDecodedToken && fullDecodedToken . header ;
174200 const payload = fullDecodedToken && fullDecodedToken . payload ;
175201
@@ -179,10 +205,7 @@ export class FirebaseTokenVerifier {
179205 `for details on how to retrieve ${ this . shortNameArticle } ${ this . tokenInfo . shortName } .` ;
180206
181207 let errorMessage : string | undefined ;
182- if ( ! fullDecodedToken ) {
183- errorMessage = `Decoding ${ this . tokenInfo . jwtName } failed. Make sure you passed the entire string JWT ` +
184- `which represents ${ this . shortNameArticle } ${ this . tokenInfo . shortName } .` + verifyJwtTokenDocsMessage ;
185- } else if ( ! isEmulator && typeof header . kid === 'undefined' ) {
208+ if ( ! isEmulator && typeof header . kid === 'undefined' ) {
186209 const isCustomToken = ( payload . aud === FIREBASE_AUDIENCE ) ;
187210 const isLegacyCustomToken = ( header . alg === 'HS256' && payload . v === 0 && 'd' in payload && 'uid' in payload . d ) ;
188211
@@ -217,16 +240,19 @@ export class FirebaseTokenVerifier {
217240 verifyJwtTokenDocsMessage ;
218241 }
219242 if ( errorMessage ) {
220- return Promise . reject ( new FirebaseAuthError ( AuthClientErrorCode . INVALID_ARGUMENT , errorMessage ) ) ;
243+ throw new FirebaseAuthError ( AuthClientErrorCode . INVALID_ARGUMENT , errorMessage ) ;
221244 }
245+ }
222246
247+ private verifySignature ( jwtToken : string , decodeToken : DecodedToken , isEmulator : boolean ) :
248+ Promise < void > {
223249 if ( isEmulator ) {
224250 // Signature checks skipped for emulator; no need to fetch public keys.
225251 return this . verifyJwtSignatureWithKey ( jwtToken , null ) ;
226252 }
227253
228254 return this . fetchPublicKeys ( ) . then ( ( publicKeys ) => {
229- if ( ! Object . prototype . hasOwnProperty . call ( publicKeys , header . kid ) ) {
255+ if ( ! Object . prototype . hasOwnProperty . call ( publicKeys , decodeToken . header . kid ) ) {
230256 return Promise . reject (
231257 new FirebaseAuthError (
232258 AuthClientErrorCode . INVALID_ARGUMENT ,
@@ -236,7 +262,7 @@ export class FirebaseTokenVerifier {
236262 ) ,
237263 ) ;
238264 } else {
239- return this . verifyJwtSignatureWithKey ( jwtToken , publicKeys [ header . kid ] ) ;
265+ return this . verifyJwtSignatureWithKey ( jwtToken , publicKeys [ decodeToken . header . kid ] ) ;
240266 }
241267
242268 } ) ;
@@ -246,35 +272,30 @@ export class FirebaseTokenVerifier {
246272 * Verifies the JWT signature using the provided public key.
247273 * @param {string } jwtToken The JWT token to verify.
248274 * @param {string } publicKey The public key certificate.
249- * @return {Promise<DecodedIdToken > } A promise that resolves with the decoded JWT claims on successful
275+ * @return {Promise<void > } A promise that resolves with the decoded JWT claims on successful
250276 * verification.
251277 */
252- private verifyJwtSignatureWithKey ( jwtToken : string , publicKey : string | null ) : Promise < DecodedIdToken > {
278+ private verifyJwtSignatureWithKey ( jwtToken : string , publicKey : string | null ) : Promise < void > {
253279 const verifyJwtTokenDocsMessage = ` See ${ this . tokenInfo . url } ` +
254280 `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 ) ;
255283 return new Promise ( ( resolve , reject ) => {
256- const verifyOptions : jwt . VerifyOptions = { } ;
257- if ( publicKey !== null ) {
258- verifyOptions . algorithms = [ this . algorithm ] ;
259- }
260- jwt . verify ( jwtToken , publicKey || '' , verifyOptions ,
261- ( error : jwt . VerifyErrors | null , decodedToken : object | undefined ) => {
262- if ( error ) {
263- if ( error . name === 'TokenExpiredError' ) {
264- const errorMessage = `${ this . tokenInfo . jwtName } has expired. Get a fresh ${ this . tokenInfo . shortName } ` +
265- ` from your client app and try again (auth/${ this . tokenInfo . expiredErrorCode . code } ).` +
266- verifyJwtTokenDocsMessage ;
267- return reject ( new FirebaseAuthError ( this . tokenInfo . expiredErrorCode , errorMessage ) ) ;
268- } else if ( error . name === 'JsonWebTokenError' ) {
269- const errorMessage = `${ this . tokenInfo . jwtName } has invalid signature.` + verifyJwtTokenDocsMessage ;
270- return reject ( new FirebaseAuthError ( AuthClientErrorCode . INVALID_ARGUMENT , errorMessage ) ) ;
271- }
272- return reject ( new FirebaseAuthError ( AuthClientErrorCode . INVALID_ARGUMENT , error . message ) ) ;
273- } else {
274- const decodedIdToken = ( decodedToken as DecodedIdToken ) ;
275- decodedIdToken . uid = decodedIdToken . sub ;
276- resolve ( decodedIdToken ) ;
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 ) ) ;
277297 }
298+ return reject ( new FirebaseAuthError ( AuthClientErrorCode . INVALID_ARGUMENT , error . message ) ) ;
278299 } ) ;
279300 } ) ;
280301 }
0 commit comments