17
17
import { AuthClientErrorCode , FirebaseAuthError , ErrorInfo } from '../utils/error' ;
18
18
import * as util from '../utils/index' ;
19
19
import * as validator from '../utils/validator' ;
20
- import * as jwt from 'jsonwebtoken' ;
21
- import { HttpClient , HttpRequestConfig , HttpError } from '../utils/api-request' ;
20
+ import {
21
+ DecodedToken , decodeJwt , JwtError , JwtErrorCode ,
22
+ EmulatorSignatureVerifier , PublicKeySignatureVerifier , ALGORITHM_RS256 , SignatureVerifier ,
23
+ } from '../utils/jwt' ;
22
24
import { FirebaseApp } from '../firebase-app' ;
23
25
import { auth } from './index' ;
24
26
@@ -27,15 +29,15 @@ import DecodedIdToken = auth.DecodedIdToken;
27
29
// Audience to use for Firebase Auth Custom tokens
28
30
const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit' ;
29
31
30
- export const ALGORITHM_RS256 = 'RS256' ;
31
-
32
32
// URL containing the public keys for the Google certs (whose private keys are used to sign Firebase
33
33
// Auth ID tokens)
34
34
const CLIENT_CERT_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/[email protected] ' ;
35
35
36
36
// URL containing the public keys for Firebase session cookies. This will be updated to a different URL soon.
37
37
const SESSION_COOKIE_CERT_URL = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys' ;
38
38
39
+ const EMULATOR_VERIFIER = new EmulatorSignatureVerifier ( ) ;
40
+
39
41
/** User facing token information related to the Firebase ID token. */
40
42
export const ID_TOKEN_INFO : FirebaseTokenInfo = {
41
43
url : 'https://firebase.google.com/docs/auth/admin/verify-id-tokens' ,
@@ -69,27 +71,20 @@ export interface FirebaseTokenInfo {
69
71
}
70
72
71
73
/**
72
- * Class for verifying general purpose Firebase JWTs. This verifies ID tokens and session cookies.
74
+ * Class for verifying ID tokens and session cookies.
73
75
*/
74
76
export class FirebaseTokenVerifier {
75
- private publicKeys : { [ key : string ] : string } ;
76
- private publicKeysExpireAt : number ;
77
77
private readonly shortNameArticle : string ;
78
+ private readonly signatureVerifier : SignatureVerifier ;
78
79
79
- constructor ( private clientCertUrl : string , private algorithm : jwt . Algorithm ,
80
- private issuer : string , private tokenInfo : FirebaseTokenInfo ,
80
+ constructor ( clientCertUrl : string , private issuer : string , private tokenInfo : FirebaseTokenInfo ,
81
81
private readonly app : FirebaseApp ) {
82
82
83
83
if ( ! validator . isURL ( clientCertUrl ) ) {
84
84
throw new FirebaseAuthError (
85
85
AuthClientErrorCode . INVALID_ARGUMENT ,
86
86
'The provided public client certificate URL is an invalid URL.' ,
87
87
) ;
88
- } else if ( ! validator . isNonEmptyString ( algorithm ) ) {
89
- throw new FirebaseAuthError (
90
- AuthClientErrorCode . INVALID_ARGUMENT ,
91
- 'The provided JWT algorithm is an empty string.' ,
92
- ) ;
93
88
} else if ( ! validator . isURL ( issuer ) ) {
94
89
throw new FirebaseAuthError (
95
90
AuthClientErrorCode . INVALID_ARGUMENT ,
@@ -128,16 +123,18 @@ export class FirebaseTokenVerifier {
128
123
}
129
124
this . shortNameArticle = tokenInfo . shortName . charAt ( 0 ) . match ( / [ a e i o u ] / i) ? 'an' : 'a' ;
130
125
126
+ this . signatureVerifier =
127
+ PublicKeySignatureVerifier . withCertificateUrl ( clientCertUrl , app . options . httpAgent ) ;
128
+
131
129
// For backward compatibility, the project ID is validated in the verification call.
132
130
}
133
131
134
132
/**
135
133
* Verifies the format and signature of a Firebase Auth JWT token.
136
134
*
137
- * @param {string } jwtToken The Firebase Auth JWT token to verify.
138
- * @param {boolean= } isEmulator Whether to accept Auth Emulator tokens.
139
- * @return {Promise<DecodedIdToken> } A promise fulfilled with the decoded claims of the Firebase Auth ID
140
- * token.
135
+ * @param jwtToken The Firebase Auth JWT token to verify.
136
+ * @param isEmulator Whether to accept Auth Emulator tokens.
137
+ * @return A promise fulfilled with the decoded claims of the Firebase Auth ID token.
141
138
*/
142
139
public verifyJWT ( jwtToken : string , isEmulator = false ) : Promise < DecodedIdToken > {
143
140
if ( ! validator . isString ( jwtToken ) ) {
@@ -147,29 +144,68 @@ export class FirebaseTokenVerifier {
147
144
) ;
148
145
}
149
146
150
- return util . findProjectId ( this . app )
147
+ return this . ensureProjectId ( )
151
148
. then ( ( projectId ) => {
152
- return this . verifyJWTWithProjectId ( jwtToken , projectId , isEmulator ) ;
149
+ return this . decodeAndVerify ( jwtToken , projectId , isEmulator ) ;
150
+ } )
151
+ . then ( ( decoded ) => {
152
+ const decodedIdToken = decoded . payload as DecodedIdToken ;
153
+ decodedIdToken . uid = decodedIdToken . sub ;
154
+ return decodedIdToken ;
153
155
} ) ;
154
156
}
155
157
156
- private verifyJWTWithProjectId (
157
- jwtToken : string ,
158
- projectId : string | null ,
159
- isEmulator : boolean
160
- ) : Promise < DecodedIdToken > {
161
- if ( ! validator . isNonEmptyString ( projectId ) ) {
162
- throw new FirebaseAuthError (
163
- AuthClientErrorCode . INVALID_CREDENTIAL ,
164
- 'Must initialize app with a cert credential or set your Firebase project ID as the ' +
165
- `GOOGLE_CLOUD_PROJECT environment variable to call ${ this . tokenInfo . verifyApiName } .` ,
166
- ) ;
167
- }
158
+ private ensureProjectId ( ) : Promise < string > {
159
+ return util . findProjectId ( this . app )
160
+ . then ( ( projectId ) => {
161
+ if ( ! validator . isNonEmptyString ( projectId ) ) {
162
+ throw new FirebaseAuthError (
163
+ AuthClientErrorCode . INVALID_CREDENTIAL ,
164
+ 'Must initialize app with a cert credential or set your Firebase project ID as the ' +
165
+ `GOOGLE_CLOUD_PROJECT environment variable to call ${ this . tokenInfo . verifyApiName } .` ,
166
+ ) ;
167
+ }
168
+ return Promise . resolve ( projectId ) ;
169
+ } )
170
+ }
168
171
169
- const fullDecodedToken : any = jwt . decode ( jwtToken , {
170
- complete : true ,
171
- } ) ;
172
+ private decodeAndVerify ( token : string , projectId : string , isEmulator : boolean ) : Promise < DecodedToken > {
173
+ return this . safeDecode ( token )
174
+ . then ( ( decodedToken ) => {
175
+ this . verifyContent ( decodedToken , projectId , isEmulator ) ;
176
+ return this . verifySignature ( token , isEmulator )
177
+ . then ( ( ) => decodedToken ) ;
178
+ } ) ;
179
+ }
172
180
181
+ private safeDecode ( jwtToken : string ) : Promise < DecodedToken > {
182
+ return decodeJwt ( jwtToken )
183
+ . catch ( ( err : JwtError ) => {
184
+ if ( err . code == JwtErrorCode . INVALID_ARGUMENT ) {
185
+ const verifyJwtTokenDocsMessage = ` See ${ this . tokenInfo . url } ` +
186
+ `for details on how to retrieve ${ this . shortNameArticle } ${ this . tokenInfo . shortName } .` ;
187
+ const errorMessage = `Decoding ${ this . tokenInfo . jwtName } failed. Make sure you passed ` +
188
+ `the entire string JWT which represents ${ this . shortNameArticle } ` +
189
+ `${ this . tokenInfo . shortName } .` + verifyJwtTokenDocsMessage ;
190
+ throw new FirebaseAuthError ( AuthClientErrorCode . INVALID_ARGUMENT ,
191
+ errorMessage ) ;
192
+ }
193
+ throw new FirebaseAuthError ( AuthClientErrorCode . INTERNAL_ERROR , err . message ) ;
194
+ } ) ;
195
+ }
196
+
197
+ /**
198
+ * Verifies the content of a Firebase Auth JWT.
199
+ *
200
+ * @param fullDecodedToken The decoded JWT.
201
+ * @param projectId The Firebase Project Id.
202
+ * @param isEmulator Whether the token is an Emulator token.
203
+ */
204
+ private verifyContent (
205
+ fullDecodedToken : DecodedToken ,
206
+ projectId : string | null ,
207
+ isEmulator : boolean ) : void {
208
+
173
209
const header = fullDecodedToken && fullDecodedToken . header ;
174
210
const payload = fullDecodedToken && fullDecodedToken . payload ;
175
211
@@ -179,10 +215,7 @@ export class FirebaseTokenVerifier {
179
215
`for details on how to retrieve ${ this . shortNameArticle } ${ this . tokenInfo . shortName } .` ;
180
216
181
217
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' ) {
218
+ if ( ! isEmulator && typeof header . kid === 'undefined' ) {
186
219
const isCustomToken = ( payload . aud === FIREBASE_AUDIENCE ) ;
187
220
const isLegacyCustomToken = ( header . alg === 'HS256' && payload . v === 0 && 'd' in payload && 'uid' in payload . d ) ;
188
221
@@ -197,8 +230,8 @@ export class FirebaseTokenVerifier {
197
230
}
198
231
199
232
errorMessage += verifyJwtTokenDocsMessage ;
200
- } else if ( ! isEmulator && header . alg !== this . algorithm ) {
201
- errorMessage = `${ this . tokenInfo . jwtName } has incorrect algorithm. Expected "` + this . algorithm + '" but got ' +
233
+ } else if ( ! isEmulator && header . alg !== ALGORITHM_RS256 ) {
234
+ errorMessage = `${ this . tokenInfo . jwtName } has incorrect algorithm. Expected "` + ALGORITHM_RS256 + '" but got ' +
202
235
'"' + header . alg + '".' + verifyJwtTokenDocsMessage ;
203
236
} else if ( payload . aud !== projectId ) {
204
237
errorMessage = `${ this . tokenInfo . jwtName } has incorrect "aud" (audience) claim. Expected "` +
@@ -217,135 +250,55 @@ export class FirebaseTokenVerifier {
217
250
verifyJwtTokenDocsMessage ;
218
251
}
219
252
if ( errorMessage ) {
220
- return Promise . reject ( new FirebaseAuthError ( AuthClientErrorCode . INVALID_ARGUMENT , errorMessage ) ) ;
253
+ throw new FirebaseAuthError ( AuthClientErrorCode . INVALID_ARGUMENT , errorMessage ) ;
221
254
}
255
+ }
222
256
223
- if ( isEmulator ) {
224
- // Signature checks skipped for emulator; no need to fetch public keys.
225
- return this . verifyJwtSignatureWithKey ( jwtToken , null ) ;
226
- }
227
-
228
- return this . fetchPublicKeys ( ) . then ( ( publicKeys ) => {
229
- if ( ! Object . prototype . hasOwnProperty . call ( publicKeys , header . kid ) ) {
230
- return Promise . reject (
231
- new FirebaseAuthError (
232
- AuthClientErrorCode . INVALID_ARGUMENT ,
233
- `${ this . tokenInfo . jwtName } has "kid" claim which does not correspond to a known public key. ` +
234
- `Most likely the ${ this . tokenInfo . shortName } is expired, so get a fresh token from your ` +
235
- 'client app and try again.' ,
236
- ) ,
237
- ) ;
238
- } else {
239
- return this . verifyJwtSignatureWithKey ( jwtToken , publicKeys [ header . kid ] ) ;
240
- }
241
-
242
- } ) ;
257
+ private verifySignature ( jwtToken : string , isEmulator : boolean ) :
258
+ Promise < void > {
259
+ const verifier = isEmulator ? EMULATOR_VERIFIER : this . signatureVerifier ;
260
+ return verifier . verify ( jwtToken )
261
+ . catch ( ( error ) => {
262
+ throw this . mapJwtErrorToAuthError ( error ) ;
263
+ } ) ;
243
264
}
244
265
245
266
/**
246
- * Verifies the JWT signature using the provided public key.
247
- * @param {string } jwtToken The JWT token to verify.
248
- * @param {string } publicKey The public key certificate.
249
- * @return {Promise<DecodedIdToken> } A promise that resolves with the decoded JWT claims on successful
250
- * verification.
267
+ * Maps JwtError to FirebaseAuthError
268
+ *
269
+ * @param error JwtError to be mapped.
270
+ * @returns FirebaseAuthError or Error instance.
251
271
*/
252
- private verifyJwtSignatureWithKey ( jwtToken : string , publicKey : string | null ) : Promise < DecodedIdToken > {
272
+ private mapJwtErrorToAuthError ( error : JwtError ) : Error {
253
273
const verifyJwtTokenDocsMessage = ` See ${ this . tokenInfo . url } ` +
254
274
`for details on how to retrieve ${ this . shortNameArticle } ${ this . tokenInfo . shortName } .` ;
255
- 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 ) ;
277
- }
278
- } ) ;
279
- } ) ;
280
- }
281
-
282
- /**
283
- * Fetches the public keys for the Google certs.
284
- *
285
- * @return {Promise<object> } A promise fulfilled with public keys for the Google certs.
286
- */
287
- private fetchPublicKeys ( ) : Promise < { [ key : string ] : string } > {
288
- const publicKeysExist = ( typeof this . publicKeys !== 'undefined' ) ;
289
- const publicKeysExpiredExists = ( typeof this . publicKeysExpireAt !== 'undefined' ) ;
290
- const publicKeysStillValid = ( publicKeysExpiredExists && Date . now ( ) < this . publicKeysExpireAt ) ;
291
- if ( publicKeysExist && publicKeysStillValid ) {
292
- return Promise . resolve ( this . publicKeys ) ;
275
+ if ( error . code === JwtErrorCode . TOKEN_EXPIRED ) {
276
+ const errorMessage = `${ this . tokenInfo . jwtName } has expired. Get a fresh ${ this . tokenInfo . shortName } ` +
277
+ ` from your client app and try again (auth/${ this . tokenInfo . expiredErrorCode . code } ).` +
278
+ verifyJwtTokenDocsMessage ;
279
+ return new FirebaseAuthError ( this . tokenInfo . expiredErrorCode , errorMessage ) ;
280
+ } else if ( error . code === JwtErrorCode . INVALID_SIGNATURE ) {
281
+ const errorMessage = `${ this . tokenInfo . jwtName } has invalid signature.` + verifyJwtTokenDocsMessage ;
282
+ return new FirebaseAuthError ( AuthClientErrorCode . INVALID_ARGUMENT , errorMessage ) ;
283
+ } else if ( error . code === JwtErrorCode . NO_MATCHING_KID ) {
284
+ const errorMessage = `${ this . tokenInfo . jwtName } has "kid" claim which does not ` +
285
+ `correspond to a known public key. Most likely the ${ this . tokenInfo . shortName } ` +
286
+ 'is expired, so get a fresh token from your client app and try again.' ;
287
+ return new FirebaseAuthError ( AuthClientErrorCode . INVALID_ARGUMENT , errorMessage ) ;
293
288
}
294
-
295
- const client = new HttpClient ( ) ;
296
- const request : HttpRequestConfig = {
297
- method : 'GET' ,
298
- url : this . clientCertUrl ,
299
- httpAgent : this . app . options . httpAgent ,
300
- } ;
301
- return client . send ( request ) . then ( ( resp ) => {
302
- if ( ! resp . isJson ( ) || resp . data . error ) {
303
- // Treat all non-json messages and messages with an 'error' field as
304
- // error responses.
305
- throw new HttpError ( resp ) ;
306
- }
307
- if ( Object . prototype . hasOwnProperty . call ( resp . headers , 'cache-control' ) ) {
308
- const cacheControlHeader : string = resp . headers [ 'cache-control' ] ;
309
- const parts = cacheControlHeader . split ( ',' ) ;
310
- parts . forEach ( ( part ) => {
311
- const subParts = part . trim ( ) . split ( '=' ) ;
312
- if ( subParts [ 0 ] === 'max-age' ) {
313
- const maxAge : number = + subParts [ 1 ] ;
314
- this . publicKeysExpireAt = Date . now ( ) + ( maxAge * 1000 ) ;
315
- }
316
- } ) ;
317
- }
318
- this . publicKeys = resp . data ;
319
- return resp . data ;
320
- } ) . catch ( ( err ) => {
321
- if ( err instanceof HttpError ) {
322
- let errorMessage = 'Error fetching public keys for Google certs: ' ;
323
- const resp = err . response ;
324
- if ( resp . isJson ( ) && resp . data . error ) {
325
- errorMessage += `${ resp . data . error } ` ;
326
- if ( resp . data . error_description ) {
327
- errorMessage += ' (' + resp . data . error_description + ')' ;
328
- }
329
- } else {
330
- errorMessage += `${ resp . text } ` ;
331
- }
332
- throw new FirebaseAuthError ( AuthClientErrorCode . INTERNAL_ERROR , errorMessage ) ;
333
- }
334
- throw err ;
335
- } ) ;
289
+ return new FirebaseAuthError ( AuthClientErrorCode . INVALID_ARGUMENT , error . message ) ;
336
290
}
337
291
}
338
292
339
293
/**
340
294
* Creates a new FirebaseTokenVerifier to verify Firebase ID tokens.
341
295
*
342
- * @param { FirebaseApp } app Firebase app instance.
343
- * @return { FirebaseTokenVerifier }
296
+ * @param app Firebase app instance.
297
+ * @return FirebaseTokenVerifier
344
298
*/
345
299
export function createIdTokenVerifier ( app : FirebaseApp ) : FirebaseTokenVerifier {
346
300
return new FirebaseTokenVerifier (
347
301
CLIENT_CERT_URL ,
348
- ALGORITHM_RS256 ,
349
302
'https://securetoken.google.com/' ,
350
303
ID_TOKEN_INFO ,
351
304
app
@@ -355,13 +308,12 @@ export function createIdTokenVerifier(app: FirebaseApp): FirebaseTokenVerifier {
355
308
/**
356
309
* Creates a new FirebaseTokenVerifier to verify Firebase session cookies.
357
310
*
358
- * @param { FirebaseApp } app Firebase app instance.
359
- * @return { FirebaseTokenVerifier }
311
+ * @param app Firebase app instance.
312
+ * @return FirebaseTokenVerifier
360
313
*/
361
314
export function createSessionCookieVerifier ( app : FirebaseApp ) : FirebaseTokenVerifier {
362
315
return new FirebaseTokenVerifier (
363
316
SESSION_COOKIE_CERT_URL ,
364
- ALGORITHM_RS256 ,
365
317
'https://session.firebase.google.com/' ,
366
318
SESSION_COOKIE_INFO ,
367
319
app
0 commit comments