Skip to content

Commit 8848ac6

Browse files
committed
PR fixes
1 parent e5b62f3 commit 8848ac6

File tree

3 files changed

+59
-120
lines changed

3 files changed

+59
-120
lines changed

src/auth/token-verifier.ts

Lines changed: 24 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,9 @@
1717
import { AuthClientErrorCode, FirebaseAuthError, ErrorInfo } from '../utils/error';
1818
import * as util from '../utils/index';
1919
import * as validator from '../utils/validator';
20-
import * as jwt from 'jsonwebtoken';
2120
import {
2221
DecodedToken, decodeJwt, JwtError, JwtErrorCode,
23-
EmulatorSignatureVerifier, PublicKeySignatureVerifier, UrlKeyFetcher,
22+
EmulatorSignatureVerifier, PublicKeySignatureVerifier,
2423
} from '../utils/jwt';
2524
import { FirebaseApp } from '../firebase-app';
2625
import { auth } from './index';
@@ -39,6 +38,8 @@ const CLIENT_CERT_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/secur
3938
// URL containing the public keys for Firebase session cookies. This will be updated to a different URL soon.
4039
const SESSION_COOKIE_CERT_URL = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys';
4140

41+
const EMULATOR_VERIFIER = new EmulatorSignatureVerifier();
42+
4243
/** User facing token information related to the Firebase ID token. */
4344
export const ID_TOKEN_INFO: FirebaseTokenInfo = {
4445
url: 'https://firebase.google.com/docs/auth/admin/verify-id-tokens',
@@ -78,20 +79,14 @@ export class FirebaseTokenVerifier {
7879
private readonly shortNameArticle: string;
7980
private readonly signatureVerifier: PublicKeySignatureVerifier;
8081

81-
constructor(clientCertUrl: string, private algorithm: jwt.Algorithm,
82-
private issuer: string, private tokenInfo: FirebaseTokenInfo,
82+
constructor(clientCertUrl: string, private issuer: string, private tokenInfo: FirebaseTokenInfo,
8383
private readonly app: FirebaseApp) {
8484

8585
if (!validator.isURL(clientCertUrl)) {
8686
throw new FirebaseAuthError(
8787
AuthClientErrorCode.INVALID_ARGUMENT,
8888
'The provided public client certificate URL is an invalid URL.',
8989
);
90-
} else if (!validator.isNonEmptyString(algorithm)) {
91-
throw new FirebaseAuthError(
92-
AuthClientErrorCode.INVALID_ARGUMENT,
93-
'The provided JWT algorithm is an empty string.',
94-
);
9590
} else if (!validator.isURL(issuer)) {
9691
throw new FirebaseAuthError(
9792
AuthClientErrorCode.INVALID_ARGUMENT,
@@ -130,19 +125,18 @@ export class FirebaseTokenVerifier {
130125
}
131126
this.shortNameArticle = tokenInfo.shortName.charAt(0).match(/[aeiou]/i) ? 'an' : 'a';
132127

133-
this.signatureVerifier = new PublicKeySignatureVerifier(
134-
new UrlKeyFetcher(clientCertUrl, app.options.httpAgent));
128+
this.signatureVerifier =
129+
PublicKeySignatureVerifier.withCertificateUrl(clientCertUrl, app.options.httpAgent);
135130

136131
// For backward compatibility, the project ID is validated in the verification call.
137132
}
138133

139134
/**
140135
* Verifies the format and signature of a Firebase Auth JWT token.
141136
*
142-
* @param {string} jwtToken The Firebase Auth JWT token to verify.
143-
* @param {boolean=} isEmulator Whether to accept Auth Emulator tokens.
144-
* @return {Promise<DecodedIdToken>} A promise fulfilled with the decoded claims of the Firebase Auth ID
145-
* token.
137+
* @param jwtToken The Firebase Auth JWT token to verify.
138+
* @param isEmulator Whether to accept Auth Emulator tokens.
139+
* @return A promise fulfilled with the decoded claims of the Firebase Auth ID token.
146140
*/
147141
public verifyJWT(jwtToken: string, isEmulator = false): Promise<DecodedIdToken> {
148142
if (!validator.isString(jwtToken)) {
@@ -178,26 +172,17 @@ export class FirebaseTokenVerifier {
178172
}
179173

180174
private decodeAndVerify(token: string, projectId: string, isEmulator: boolean): Promise<DecodedToken> {
181-
return this.verifyContent(token, projectId, isEmulator)
182-
.then((decoded) => {
175+
return this.safeDecode(token)
176+
.then((decodedToken) => {
177+
this.verifyContent(decodedToken, projectId, isEmulator);
183178
return this.verifySignature(token, isEmulator)
184-
.then(() => decoded);
179+
.then(() => decodedToken);
185180
});
186181
}
187182

188-
private verifyContent(token: string, projectId: string, isEmulator: boolean): Promise<DecodedToken> {
189-
return this.safeDecode(token).then((decodedToken) => {
190-
this.validateTokenContent(decodedToken, projectId, isEmulator);
191-
return Promise.resolve(decodedToken);
192-
});
193-
}
194-
195183
private safeDecode(jwtToken: string): Promise<DecodedToken> {
196184
return decodeJwt(jwtToken)
197-
.catch((err) => {
198-
if (!(err instanceof JwtError)) {
199-
throw err;
200-
}
185+
.catch((err: JwtError) => {
201186
if (err.code == JwtErrorCode.INVALID_ARGUMENT) {
202187
const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` +
203188
`for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`;
@@ -211,7 +196,7 @@ export class FirebaseTokenVerifier {
211196
});
212197
}
213198

214-
private validateTokenContent(
199+
private verifyContent(
215200
fullDecodedToken: DecodedToken,
216201
projectId: string | null,
217202
isEmulator: boolean): void {
@@ -240,8 +225,8 @@ export class FirebaseTokenVerifier {
240225
}
241226

242227
errorMessage += verifyJwtTokenDocsMessage;
243-
} else if (!isEmulator && header.alg !== this.algorithm) {
244-
errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected "` + this.algorithm + '" but got ' +
228+
} else if (!isEmulator && header.alg !== ALGORITHM_RS256) {
229+
errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected "` + ALGORITHM_RS256 + '" but got ' +
245230
'"' + header.alg + '".' + verifyJwtTokenDocsMessage;
246231
} else if (payload.aud !== projectId) {
247232
errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` +
@@ -266,7 +251,7 @@ export class FirebaseTokenVerifier {
266251

267252
private verifySignature(jwtToken: string, isEmulator: boolean):
268253
Promise<void> {
269-
const verifier = isEmulator ? new EmulatorSignatureVerifier() : this.signatureVerifier;
254+
const verifier = isEmulator ? EMULATOR_VERIFIER : this.signatureVerifier;
270255
return verifier.verify(jwtToken)
271256
.catch((error) => {
272257
throw this.mapJwtErrorToAuthError(error);
@@ -285,11 +270,11 @@ export class FirebaseTokenVerifier {
285270
verifyJwtTokenDocsMessage;
286271
return new FirebaseAuthError(this.tokenInfo.expiredErrorCode, errorMessage);
287272
}
288-
else if (error.code === JwtErrorCode.INVALID_TOKEN) {
273+
else if (error.code === JwtErrorCode.INVALID_SIGNATURE) {
289274
const errorMessage = `${this.tokenInfo.jwtName} has invalid signature.` + verifyJwtTokenDocsMessage;
290275
return new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage);
291276
}
292-
else if (error.code === JwtErrorCode.NO_MATCHING_KID) {
277+
else if (error.code === JwtErrorCode.KEY_FETCH_ERROR) {
293278
const errorMessage = `${this.tokenInfo.jwtName} has "kid" claim which does not ` +
294279
`correspond to a known public key. Most likely the ${this.tokenInfo.shortName} ` +
295280
'is expired, so get a fresh token from your client app and try again.';
@@ -302,13 +287,12 @@ export class FirebaseTokenVerifier {
302287
/**
303288
* Creates a new FirebaseTokenVerifier to verify Firebase ID tokens.
304289
*
305-
* @param {FirebaseApp} app Firebase app instance.
306-
* @return {FirebaseTokenVerifier}
290+
* @param app Firebase app instance.
291+
* @return FirebaseTokenVerifier
307292
*/
308293
export function createIdTokenVerifier(app: FirebaseApp): FirebaseTokenVerifier {
309294
return new FirebaseTokenVerifier(
310295
CLIENT_CERT_URL,
311-
ALGORITHM_RS256,
312296
'https://securetoken.google.com/',
313297
ID_TOKEN_INFO,
314298
app
@@ -318,13 +302,12 @@ export function createIdTokenVerifier(app: FirebaseApp): FirebaseTokenVerifier {
318302
/**
319303
* Creates a new FirebaseTokenVerifier to verify Firebase session cookies.
320304
*
321-
* @param {FirebaseApp} app Firebase app instance.
322-
* @return {FirebaseTokenVerifier}
305+
* @param app Firebase app instance.
306+
* @return FirebaseTokenVerifier
323307
*/
324308
export function createSessionCookieVerifier(app: FirebaseApp): FirebaseTokenVerifier {
325309
return new FirebaseTokenVerifier(
326310
SESSION_COOKIE_CERT_URL,
327-
ALGORITHM_RS256,
328311
'https://session.firebase.google.com/',
329312
SESSION_COOKIE_INFO,
330313
app

src/utils/jwt.ts

Lines changed: 34 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ import * as jwt from 'jsonwebtoken';
1919
import { HttpClient, HttpRequestConfig, HttpError } from '../utils/api-request';
2020
import { Agent } from 'http';
2121

22+
const ALGORITHM_RS256: jwt.Algorithm = 'RS256' as const;
23+
24+
// `jsonwebtoken` converts errors from the `getKey` callback to its own `JsonWebTokenError` type
25+
// and prefixes the error message with the following. Use the prefix to identify errors thrown
26+
// from the key provider callback.
27+
// https://github.com/auth0/node-jsonwebtoken/blob/d71e383862fc735991fd2e759181480f066bf138/verify.js#L96
28+
const JWT_CALLBACK_ERROR_PREFIX = 'error in secret or public key callback: ';
29+
30+
const NO_MATCHING_KID_ERROR_MESSAGE = 'no-matching-kid-error';
31+
2232
export type Dictionary = {[key: string]: any}
2333

2434
export type DecodedToken = {
@@ -34,9 +44,9 @@ interface KeyFetcher {
3444
fetchPublicKeys(): Promise<{ [key: string]: string }>;
3545
}
3646

37-
export class UrlKeyFetcher implements KeyFetcher {
47+
class UrlKeyFetcher implements KeyFetcher {
3848
private publicKeys: { [key: string]: string };
39-
private publicKeysExpireAt: number;
49+
private publicKeysExpireAt = 0;
4050

4151
constructor(private clientCertUrl: string, private readonly httpAgent?: Agent) {
4252
if (!validator.isURL(clientCertUrl)) {
@@ -59,10 +69,7 @@ export class UrlKeyFetcher implements KeyFetcher {
5969
}
6070

6171
private shouldRefresh(): boolean {
62-
const publicKeysExist = (typeof this.publicKeys !== 'undefined');
63-
const publicKeysExpiredExists = (typeof this.publicKeysExpireAt !== 'undefined');
64-
const publicKeysStillValid = (publicKeysExpiredExists && Date.now() < this.publicKeysExpireAt);
65-
return !(publicKeysExist && publicKeysStillValid);
72+
return !this.publicKeys || this.publicKeysExpireAt <= Date.now();
6673
}
6774

6875
private refresh(): Promise<{ [key: string]: string }> {
@@ -78,6 +85,8 @@ export class UrlKeyFetcher implements KeyFetcher {
7885
// error responses.
7986
throw new HttpError(resp);
8087
}
88+
// reset expire at from previous set of keys.
89+
this.publicKeysExpireAt = 0;
8190
if (Object.prototype.hasOwnProperty.call(resp.headers, 'cache-control')) {
8291
const cacheControlHeader: string = resp.headers['cache-control'];
8392
const parts = cacheControlHeader.split(',');
@@ -103,7 +112,7 @@ export class UrlKeyFetcher implements KeyFetcher {
103112
} else {
104113
errorMessage += `${resp.text}`;
105114
}
106-
throw new JwtError(JwtErrorCode.INTERNAL_ERROR, errorMessage);
115+
throw new Error(errorMessage);
107116
}
108117
throw err;
109118
});
@@ -114,21 +123,18 @@ export class UrlKeyFetcher implements KeyFetcher {
114123
* Verifies JWT signature with a public key.
115124
*/
116125
export class PublicKeySignatureVerifier implements SignatureVerifier {
117-
constructor(private keyFetcher: UrlKeyFetcher) {
126+
constructor(private keyFetcher: KeyFetcher) {
118127
if (!validator.isNonNullObject(keyFetcher)) {
119128
throw new Error('The provided key fetcher is not an object or null.');
120129
}
121130
}
122131

132+
public static withCertificateUrl(clientCertUrl: string, httpAgent?: Agent): PublicKeySignatureVerifier {
133+
return new PublicKeySignatureVerifier(new UrlKeyFetcher(clientCertUrl, httpAgent));
134+
}
135+
123136
public verify(token: string): Promise<void> {
124-
const ALGORITHM_RS256: jwt.Algorithm = 'RS256' as const;
125-
const error = new JwtError(JwtErrorCode.INVALID_TOKEN,
126-
'The provided token has invalid signature.');
127-
return isSignatureValid(token, getKeyCallback(this.keyFetcher),
128-
{ algorithms: [ALGORITHM_RS256] })
129-
.then(isValid => {
130-
return isValid ? Promise.resolve() : Promise.reject(error);
131-
});
137+
return verifyJwtSignature(token, getKeyCallback(this.keyFetcher), { algorithms: [ALGORITHM_RS256] });
132138
}
133139
}
134140

@@ -150,40 +156,34 @@ function getKeyCallback(fetcher: KeyFetcher): jwt.GetPublicKeyOrSecret {
150156

151157
export class EmulatorSignatureVerifier implements SignatureVerifier {
152158
public verify(token: string): Promise<void> {
153-
const error = new JwtError(JwtErrorCode.INVALID_TOKEN,
154-
'The provided token has invalid signature.');
155-
156159
// Signature checks skipped for emulator; no need to fetch public keys.
157-
return isSignatureValid(token, '')
158-
.then(isValid => {
159-
return isValid ? Promise.resolve() : Promise.reject(error);
160-
});
160+
return verifyJwtSignature(token, '');
161161
}
162162
}
163163

164-
function isSignatureValid(token: string, secretOrPublicKey: jwt.Secret | jwt.GetPublicKeyOrSecret,
165-
options?: jwt.VerifyOptions): Promise<boolean> {
164+
function verifyJwtSignature(token: string, secretOrPublicKey: jwt.Secret | jwt.GetPublicKeyOrSecret,
165+
options?: jwt.VerifyOptions): Promise<void> {
166166
return new Promise((resolve, reject) => {
167167
jwt.verify(token, secretOrPublicKey, options,
168168
(error: jwt.VerifyErrors | null) => {
169169
if (!error) {
170-
return resolve(true);
170+
return resolve();
171171
}
172172
if (error.name === 'TokenExpiredError') {
173173
return reject(new JwtError(JwtErrorCode.TOKEN_EXPIRED,
174174
'The provided token has expired. Get a fresh token from your ' +
175175
'client app and try again.'));
176176
} else if (error.name === 'JsonWebTokenError') {
177-
const prefix = 'error in secret or public key callback: ';
178-
if (error.message && error.message.includes(prefix)) {
179-
const message = error.message.split(prefix).pop() || 'Error fetching public keys.';
180-
const code = (message === NO_MATCHING_KID_ERROR_MESSAGE) ? JwtErrorCode.NO_MATCHING_KID :
177+
if (error.message && error.message.includes(JWT_CALLBACK_ERROR_PREFIX)) {
178+
const message = error.message.split(JWT_CALLBACK_ERROR_PREFIX).pop() || 'Error fetching public keys.';
179+
const code = (message === NO_MATCHING_KID_ERROR_MESSAGE) ? JwtErrorCode.KEY_FETCH_ERROR :
181180
JwtErrorCode.INVALID_ARGUMENT;
182181
return reject(new JwtError(code, message));
183182
}
184-
return resolve(false);
183+
return reject(new JwtError(JwtErrorCode.INVALID_SIGNATURE,
184+
'The provided token has invalid signature.'));
185185
}
186-
return reject(new JwtError(JwtErrorCode.INVALID_TOKEN, error.message));
186+
return reject(new JwtError(JwtErrorCode.INVALID_SIGNATURE, error.message));
187187
});
188188
});
189189
}
@@ -232,9 +232,6 @@ export enum JwtErrorCode {
232232
INVALID_ARGUMENT = 'invalid-argument',
233233
INVALID_CREDENTIAL = 'invalid-credential',
234234
TOKEN_EXPIRED = 'token-expired',
235-
INVALID_TOKEN = 'invalid-token',
236-
NO_MATCHING_KID = 'no-matching-kid-error',
237-
INTERNAL_ERROR = 'internal-error',
235+
INVALID_SIGNATURE = 'invalid-token',
236+
KEY_FETCH_ERROR = 'no-matching-kid-error',
238237
}
239-
240-
const NO_MATCHING_KID_ERROR_MESSAGE = 'no-matching-kid-error';

0 commit comments

Comments
 (0)