Skip to content

Commit 83852f5

Browse files
committed
Move the signature verifier to utils/jwt and other PR fixes
1 parent e118e6a commit 83852f5

File tree

3 files changed

+138
-215
lines changed

3 files changed

+138
-215
lines changed

src/auth/token-verifier.ts

Lines changed: 54 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,9 @@ import * as util from '../utils/index';
1919
import * as validator from '../utils/validator';
2020
import * as jwt from 'jsonwebtoken';
2121
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';
22+
DecodedToken, decodeJwt, JwtError, JwtErrorCode,
23+
EmulatorSignatureVerifier, PublicKeySignatureVerifier, UrlKeyFetcher,
24+
} from '../utils/jwt';
2825
import { FirebaseApp } from '../firebase-app';
2926
import { auth } from './index';
3027

@@ -33,7 +30,7 @@ import DecodedIdToken = auth.DecodedIdToken;
3330
// Audience to use for Firebase Auth Custom tokens
3431
const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit';
3532

36-
export const ALGORITHM_RS256 = 'RS256';
33+
const ALGORITHM_RS256 = 'RS256' as const;
3734

3835
// URL containing the public keys for the Google certs (whose private keys are used to sign Firebase
3936
// Auth ID tokens)
@@ -80,7 +77,6 @@ export interface FirebaseTokenInfo {
8077
export class FirebaseTokenVerifier {
8178
private readonly shortNameArticle: string;
8279
private readonly signatureVerifier: PublicKeySignatureVerifier;
83-
private readonly emulatorSignatureVerifier: EmulatorSignatureVerifier;
8480

8581
constructor(clientCertUrl: string, private algorithm: jwt.Algorithm,
8682
private issuer: string, private tokenInfo: FirebaseTokenInfo,
@@ -134,8 +130,8 @@ export class FirebaseTokenVerifier {
134130
}
135131
this.shortNameArticle = tokenInfo.shortName.charAt(0).match(/[aeiou]/i) ? 'an' : 'a';
136132

137-
this.signatureVerifier = new PublicKeySignatureVerifier(clientCertUrl, algorithm, app);
138-
this.emulatorSignatureVerifier = new EmulatorSignatureVerifier();
133+
this.signatureVerifier = new PublicKeySignatureVerifier(
134+
new UrlKeyFetcher(clientCertUrl, app.options.httpAgent));
139135

140136
// For backward compatibility, the project ID is validated in the verification call.
141137
}
@@ -156,55 +152,69 @@ export class FirebaseTokenVerifier {
156152
);
157153
}
158154

159-
return util.findProjectId(this.app)
155+
return this.ensureProjectId()
160156
.then((projectId) => {
161-
return Promise.all([this.safeDecode(jwtToken), projectId]);
162-
})
163-
.then(([fullDecodedToken, projectId]) => {
164-
this.validateToken(fullDecodedToken, projectId, isEmulator);
165-
return Promise.all([
166-
fullDecodedToken,
167-
this.verifySignature(jwtToken, isEmulator)
168-
]);
157+
return this.decodeAndVerify(jwtToken, projectId, isEmulator);
169158
})
170-
.then(([fullDecodedToken]) => {
171-
const decodedIdToken = fullDecodedToken.payload as DecodedIdToken;
159+
.then((decoded) => {
160+
const decodedIdToken = decoded.payload as DecodedIdToken;
172161
decodedIdToken.uid = decodedIdToken.sub;
173162
return decodedIdToken;
174163
});
175164
}
176165

166+
private ensureProjectId(): Promise<string> {
167+
return util.findProjectId(this.app)
168+
.then((projectId) => {
169+
if (!validator.isNonEmptyString(projectId)) {
170+
throw new FirebaseAuthError(
171+
AuthClientErrorCode.INVALID_CREDENTIAL,
172+
'Must initialize app with a cert credential or set your Firebase project ID as the ' +
173+
`GOOGLE_CLOUD_PROJECT environment variable to call ${this.tokenInfo.verifyApiName}.`,
174+
);
175+
}
176+
return Promise.resolve(projectId);
177+
})
178+
}
179+
180+
private decodeAndVerify(token: string, projectId: string, isEmulator: boolean): Promise<DecodedToken> {
181+
return this.verifyContent(token, projectId, isEmulator)
182+
.then((decoded) => {
183+
return this.verifySignature(token, isEmulator)
184+
.then(() => decoded);
185+
});
186+
}
187+
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+
177195
private safeDecode(jwtToken: string): Promise<DecodedToken> {
178196
return decodeJwt(jwtToken)
179197
.catch((err) => {
180-
if (!(err instanceof JwtDecoderError)) {
181-
return Promise.reject(err);
198+
if (!(err instanceof JwtError)) {
199+
throw err;
182200
}
183-
if (err.code == JwtDecoderErrorCode.INVALID_ARGUMENT) {
201+
if (err.code == JwtErrorCode.INVALID_ARGUMENT) {
184202
const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` +
185203
`for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`;
186204
const errorMessage = `Decoding ${this.tokenInfo.jwtName} failed. Make sure you passed ` +
187205
`the entire string JWT which represents ${this.shortNameArticle} ` +
188206
`${this.tokenInfo.shortName}.` + verifyJwtTokenDocsMessage;
189-
return Promise.reject(
190-
new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage));
207+
throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT,
208+
errorMessage);
191209
}
192-
return Promise.reject(
193-
new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR, err.message));
210+
throw new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR, err.message);
194211
});
195212
}
196213

197-
private validateToken(
214+
private validateTokenContent(
198215
fullDecodedToken: DecodedToken,
199216
projectId: string | null,
200217
isEmulator: boolean): void {
201-
if (!validator.isNonEmptyString(projectId)) {
202-
throw new FirebaseAuthError(
203-
AuthClientErrorCode.INVALID_CREDENTIAL,
204-
'Must initialize app with a cert credential or set your Firebase project ID as the ' +
205-
`GOOGLE_CLOUD_PROJECT environment variable to call ${this.tokenInfo.verifyApiName}.`,
206-
);
207-
}
208218

209219
const header = fullDecodedToken && fullDecodedToken.header;
210220
const payload = fullDecodedToken && fullDecodedToken.payload;
@@ -256,37 +266,30 @@ export class FirebaseTokenVerifier {
256266

257267
private verifySignature(jwtToken: string, isEmulator: boolean):
258268
Promise<void> {
259-
if (isEmulator) {
260-
return this.emulatorSignatureVerifier.verify(jwtToken)
261-
.catch((error) => {
262-
return Promise.reject(this.mapSignatureVerifierErrorToAuthError(error));
263-
});
264-
}
265-
266-
return this.signatureVerifier.verify(jwtToken)
269+
const verifier = isEmulator ? new EmulatorSignatureVerifier() : this.signatureVerifier;
270+
return verifier.verify(jwtToken)
267271
.catch((error) => {
268-
return Promise.reject(this.mapSignatureVerifierErrorToAuthError(error));
272+
throw this.mapSignatureVerifierErrorToAuthError(error);
269273
});
270274
}
271275

272-
private mapSignatureVerifierErrorToAuthError(error: SignatureVerifierError): Error {
276+
private mapSignatureVerifierErrorToAuthError(error: JwtError): Error {
273277
const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` +
274278
`for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`;
275-
if (!(error instanceof SignatureVerifierError)) {
279+
if (!(error instanceof JwtError)) {
276280
return (error);
277281
}
278-
if (error.code === SignatureVerifierErrorCode.TOKEN_EXPIRED) {
282+
if (error.code === JwtErrorCode.TOKEN_EXPIRED) {
279283
const errorMessage = `${this.tokenInfo.jwtName} has expired. Get a fresh ${this.tokenInfo.shortName}` +
280284
` from your client app and try again (auth/${this.tokenInfo.expiredErrorCode.code}).` +
281285
verifyJwtTokenDocsMessage;
282286
return new FirebaseAuthError(this.tokenInfo.expiredErrorCode, errorMessage);
283287
}
284-
else if (error.code === SignatureVerifierErrorCode.INVALID_TOKEN) {
288+
else if (error.code === JwtErrorCode.INVALID_TOKEN) {
285289
const errorMessage = `${this.tokenInfo.jwtName} has invalid signature.` + verifyJwtTokenDocsMessage;
286290
return new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage);
287291
}
288-
else if (error.code === SignatureVerifierErrorCode.INVALID_ARGUMENT &&
289-
error.message === NO_MATCHING_KID_ERROR_MESSAGE) {
292+
else if (error.code === JwtErrorCode.NO_MATCHING_KID) {
290293
const errorMessage = `${this.tokenInfo.jwtName} has "kid" claim which does not ` +
291294
`correspond to a known public key. Most likely the ${this.tokenInfo.shortName} ` +
292295
'is expired, so get a fresh token from your client app and try again.';

src/utils/jwt-decoder.ts

Lines changed: 0 additions & 86 deletions
This file was deleted.

0 commit comments

Comments
 (0)