Skip to content

Commit 60193c7

Browse files
committed
added token-verifier test
1 parent 0ef3325 commit 60193c7

File tree

6 files changed

+193
-60
lines changed

6 files changed

+193
-60
lines changed

src/auth.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useEmulator } from "./emulator";
12
import {
23
createIdTokenVerifier,
34
FirebaseIdToken,
@@ -8,9 +9,14 @@ export class Auth {
89
/** @internal */
910
protected readonly idTokenVerifier: FirebaseTokenVerifier;
1011

11-
constructor(projectId: string, cfPublicKeyCacheNamespace: KVNamespace) {
12+
constructor(
13+
projectId: string,
14+
cacheKey: string,
15+
cfPublicKeyCacheNamespace: KVNamespace
16+
) {
1217
this.idTokenVerifier = createIdTokenVerifier(
1318
projectId,
19+
cacheKey,
1420
cfPublicKeyCacheNamespace
1521
);
1622
}
@@ -27,10 +33,8 @@ export class Auth {
2733
* token's decoded claims if the ID token is valid; otherwise, a rejected
2834
* promise.
2935
*/
30-
public verifyIdToken(
31-
idToken: string,
32-
isEmulator = false
33-
): Promise<FirebaseIdToken> {
36+
public verifyIdToken(idToken: string): Promise<FirebaseIdToken> {
37+
const isEmulator = useEmulator();
3438
return this.idTokenVerifier.verifyJWT(idToken, isEmulator);
3539
}
3640
}

src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
const TOKEN_EXPIRY_THRESHOLD_MILLIS = 5 * 60 * 1000;
1+
export {
2+
Auth
3+
} from "./auth"

src/jws-verifier.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,12 @@ export class PublicKeySignatureVerifier implements SignatureVerifier {
2828

2929
public static withCertificateUrl(
3030
clientCertUrl: string,
31+
cacheKey: string,
3132
cfKVNamespace: KVNamespace
3233
): PublicKeySignatureVerifier {
3334
const fetcher = new HTTPFetcher(clientCertUrl)
3435
return new PublicKeySignatureVerifier(
35-
new UrlKeyFetcher(fetcher, cfKVNamespace)
36+
new UrlKeyFetcher(fetcher, cacheKey, cfKVNamespace)
3637
);
3738
}
3839

@@ -104,3 +105,12 @@ export class PublicKeySignatureVerifier implements SignatureVerifier {
104105
}
105106
}
106107
}
108+
109+
/**
110+
* Class for verifying unsigned (emulator) JWTs.
111+
*/
112+
export class EmulatorSignatureVerifier implements SignatureVerifier {
113+
public async verify(_token: RS256Token): Promise<void> {
114+
// Signature checks skipped for emulator; no need to fetch public keys.
115+
}
116+
}

src/jwt-decoder.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,19 @@ export class RS256Token {
3636
*
3737
* @param token - The JWT to verify.
3838
* @param currentTimestamp - Current timestamp in seconds since the Unix epoch.
39+
* @param skipVerifyHeader - skip verification header content if true.
3940
* @throw Error if the token is invalid.
4041
* @returns
4142
*/
42-
public static decode(token: string, currentTimestamp: number): RS256Token {
43+
public static decode(token: string, currentTimestamp: number, skipVerifyHeader: boolean = false): RS256Token {
4344
const tokenParts = token.split(".");
4445
if (tokenParts.length !== 3) {
4546
throw new JwtError(
4647
JwtErrorCode.INVALID_ARGUMENT,
4748
"token must consist of 3 parts"
4849
);
4950
}
50-
const header = decodeHeader(tokenParts[0]);
51+
const header = decodeHeader(tokenParts[0], skipVerifyHeader);
5152
const payload = decodePayload(tokenParts[1], currentTimestamp);
5253

5354
return new RS256Token(token, {
@@ -66,8 +67,11 @@ export class RS256Token {
6667
}
6768
}
6869

69-
const decodeHeader = (headerPart: string): DecodedHeader => {
70+
const decodeHeader = (headerPart: string, skipVerifyHeader: boolean): DecodedHeader => {
7071
const header = decodeBase64JSON(headerPart);
72+
if (skipVerifyHeader) {
73+
return header
74+
}
7175
const kid = header.kid;
7276
if (!isString(kid)) {
7377
throw new JwtError(

src/token-verifier.ts

Lines changed: 49 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ import {
55
JwtError,
66
JwtErrorCode,
77
} from "./errors";
8-
import { PublicKeySignatureVerifier, SignatureVerifier } from "./jws-verifier";
8+
import { EmulatorSignatureVerifier, PublicKeySignatureVerifier, SignatureVerifier } from "./jws-verifier";
99
import { DecodedPayload, RS256Token } from "./jwt-decoder";
10-
import * as validator from "./validator";
10+
import { isNonEmptyString, isNonNullObject, isString, isURL } from "./validator";
1111

1212
// Audience to use for Firebase Auth Custom tokens
13-
const FIREBASE_AUDIENCE =
13+
export const FIREBASE_AUDIENCE =
1414
"https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit";
1515

16+
const EMULATOR_VERIFIER = new EmulatorSignatureVerifier()
17+
1618
/**
1719
* Interface representing a decoded Firebase ID token, returned from the
1820
* {@link BaseAuth.verifyIdToken} method.
@@ -174,57 +176,50 @@ const makeExpectedbutGotMsg = (want: any, got: any) =>
174176
*/
175177
export class FirebaseTokenVerifier {
176178
private readonly shortNameArticle: string;
177-
private readonly signatureVerifier: SignatureVerifier;
178179

179180
constructor(
180-
clientCertUrl: string,
181-
cfKVNamespace: KVNamespace,
181+
private readonly signatureVerifier: SignatureVerifier,
182182
private projectId: string,
183183
private issuer: string,
184184
private tokenInfo: FirebaseTokenInfo
185185
) {
186-
if (!validator.isURL(clientCertUrl)) {
187-
throw new FirebaseAuthError(
188-
AuthClientErrorCode.INVALID_ARGUMENT,
189-
"The provided public client certificate URL is an invalid URL."
190-
);
191-
} else if (!validator.isNonEmptyString(projectId)) {
186+
if (!isNonEmptyString(projectId)) {
192187
throw new FirebaseAuthError(
193188
AuthClientErrorCode.INVALID_ARGUMENT,
194189
"Your Firebase project ID must be a non-empty string"
195190
);
196-
} else if (!validator.isURL(issuer)) {
191+
} else if (!isURL(issuer)) {
197192
throw new FirebaseAuthError(
198193
AuthClientErrorCode.INVALID_ARGUMENT,
199194
"The provided JWT issuer is an invalid URL."
200195
);
201-
} else if (!validator.isNonNullObject(tokenInfo)) {
196+
} else if (!isNonNullObject(tokenInfo)) {
202197
throw new FirebaseAuthError(
203198
AuthClientErrorCode.INVALID_ARGUMENT,
204199
"The provided JWT information is not an object or null."
205200
);
206-
} else if (!validator.isURL(tokenInfo.url)) {
201+
} else if (!isURL(tokenInfo.url)) {
207202
throw new FirebaseAuthError(
208203
AuthClientErrorCode.INVALID_ARGUMENT,
209204
"The provided JWT verification documentation URL is invalid."
210205
);
211-
} else if (!validator.isNonEmptyString(tokenInfo.verifyApiName)) {
206+
} else if (!isNonEmptyString(tokenInfo.verifyApiName)) {
212207
throw new FirebaseAuthError(
213208
AuthClientErrorCode.INVALID_ARGUMENT,
214209
"The JWT verify API name must be a non-empty string."
215210
);
216-
} else if (!validator.isNonEmptyString(tokenInfo.jwtName)) {
211+
} else if (!isNonEmptyString(tokenInfo.jwtName)) {
217212
throw new FirebaseAuthError(
218213
AuthClientErrorCode.INVALID_ARGUMENT,
219214
"The JWT public full name must be a non-empty string."
220215
);
221-
} else if (!validator.isNonEmptyString(tokenInfo.shortName)) {
216+
} else if (!isNonEmptyString(tokenInfo.shortName)) {
222217
throw new FirebaseAuthError(
223218
AuthClientErrorCode.INVALID_ARGUMENT,
224219
"The JWT public short name must be a non-empty string."
225220
);
226221
} else if (
227-
!validator.isNonNullObject(tokenInfo.expiredErrorCode) ||
222+
!isNonNullObject(tokenInfo.expiredErrorCode) ||
228223
!("code" in tokenInfo.expiredErrorCode)
229224
) {
230225
throw new FirebaseAuthError(
@@ -235,11 +230,6 @@ export class FirebaseTokenVerifier {
235230
this.shortNameArticle = tokenInfo.shortName.charAt(0).match(/[aeiou]/i)
236231
? "an"
237232
: "a";
238-
239-
this.signatureVerifier = PublicKeySignatureVerifier.withCertificateUrl(
240-
clientCertUrl,
241-
cfKVNamespace
242-
);
243233
}
244234

245235
/**
@@ -253,13 +243,13 @@ export class FirebaseTokenVerifier {
253243
jwtToken: string,
254244
isEmulator = false
255245
): Promise<FirebaseIdToken> {
256-
if (!validator.isString(jwtToken)) {
246+
if (!isString(jwtToken)) {
257247
throw new FirebaseAuthError(
258248
AuthClientErrorCode.INVALID_ARGUMENT,
259249
`First argument to ${this.tokenInfo.verifyApiName} must be a ${this.tokenInfo.jwtName} string.`
260250
);
261251
}
262-
return this.decodeAndVerify(jwtToken, this.projectId, isEmulator).then(
252+
return this.decodeAndVerify(jwtToken, isEmulator).then(
263253
(payload) => {
264254
payload.uid = payload.sub;
265255
return payload;
@@ -269,15 +259,14 @@ export class FirebaseTokenVerifier {
269259

270260
private async decodeAndVerify(
271261
token: string,
272-
projectId: string,
273262
isEmulator: boolean
274263
): Promise<FirebaseIdToken> {
275264
const currentTimestamp = Math.floor(Date.now() / 1000);
276265
try {
277266
const rs256Token = this.safeDecode(token, isEmulator, currentTimestamp);
278267
const { payload } = rs256Token.decodedToken;
279268

280-
this.verifyPayload(payload, projectId, currentTimestamp);
269+
this.verifyPayload(payload, currentTimestamp);
281270
await this.verifySignature(rs256Token, isEmulator);
282271

283272
return payload;
@@ -295,9 +284,7 @@ export class FirebaseTokenVerifier {
295284
currentTimestamp: number
296285
): RS256Token {
297286
try {
298-
return isEmulator
299-
? RS256Token.decode(jwtToken, currentTimestamp)
300-
: RS256Token.decode(jwtToken, currentTimestamp);
287+
return RS256Token.decode(jwtToken, currentTimestamp, isEmulator)
301288
} catch (err) {
302289
const verifyJwtTokenDocsMessage =
303290
` See ${this.tokenInfo.url} ` +
@@ -316,7 +303,6 @@ export class FirebaseTokenVerifier {
316303

317304
private verifyPayload(
318305
tokenPayload: DecodedPayload,
319-
projectId: string,
320306
currentTimestamp: number
321307
): asserts tokenPayload is FirebaseIdToken {
322308
const payload = tokenPayload;
@@ -331,43 +317,43 @@ export class FirebaseTokenVerifier {
331317
const createInvalidArgument = (errorMessage: string) =>
332318
new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage);
333319

334-
if (payload.aud !== projectId && payload.aud !== FIREBASE_AUDIENCE) {
320+
if (payload.aud !== this.projectId && payload.aud !== FIREBASE_AUDIENCE) {
335321
throw createInvalidArgument(
336322
`${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. ` +
337-
makeExpectedbutGotMsg(projectId, payload.aud) +
338-
projectIdMatchMessage +
339-
verifyJwtTokenDocsMessage
323+
makeExpectedbutGotMsg(this.projectId, payload.aud) +
324+
projectIdMatchMessage +
325+
verifyJwtTokenDocsMessage
340326
);
341327
}
342328

343-
if (payload.iss !== this.issuer + projectId) {
329+
if (payload.iss !== this.issuer + this.projectId) {
344330
throw createInvalidArgument(
345331
`${this.tokenInfo.jwtName} has incorrect "iss" (issuer) claim. ` +
346-
makeExpectedbutGotMsg(this.issuer, payload.iss) +
347-
projectIdMatchMessage +
348-
verifyJwtTokenDocsMessage
332+
makeExpectedbutGotMsg(this.issuer, payload.iss) +
333+
projectIdMatchMessage +
334+
verifyJwtTokenDocsMessage
349335
);
350336
}
351337

352338
if (payload.sub.length > 128) {
353339
throw createInvalidArgument(
354340
`${this.tokenInfo.jwtName} has "sub" (subject) claim longer than 128 characters.` +
355-
verifyJwtTokenDocsMessage
341+
verifyJwtTokenDocsMessage
356342
);
357343
}
358344

359345
// check auth_time claim
360346
if (typeof payload.auth_time !== "number") {
361347
throw createInvalidArgument(
362348
`${this.tokenInfo.jwtName} has no "auth_time" claim. ` +
363-
verifyJwtTokenDocsMessage
349+
verifyJwtTokenDocsMessage
364350
);
365351
}
366352

367353
if (currentTimestamp < payload.auth_time) {
368354
throw createInvalidArgument(
369355
`${this.tokenInfo.jwtName} has incorrect "auth_time" claim. ` +
370-
verifyJwtTokenDocsMessage
356+
verifyJwtTokenDocsMessage
371357
);
372358
}
373359
}
@@ -376,9 +362,7 @@ export class FirebaseTokenVerifier {
376362
token: RS256Token,
377363
isEmulator: boolean
378364
): Promise<void> {
379-
const verifier = isEmulator
380-
? /* EMULATOR_VERIFIER */ this.signatureVerifier
381-
: this.signatureVerifier;
365+
const verifier = isEmulator ? EMULATOR_VERIFIER : this.signatureVerifier
382366
return await verifier.verify(token);
383367
}
384368

@@ -466,18 +450,33 @@ export const ID_TOKEN_INFO: FirebaseTokenInfo = {
466450
* Creates a new FirebaseTokenVerifier to verify Firebase ID tokens.
467451
*
468452
* @internal
469-
* @param app - Firebase app instance.
470453
* @returns FirebaseTokenVerifier
471454
*/
472455
export function createIdTokenVerifier(
473456
projectID: string,
457+
cacheKey: string,
474458
cfPublicKeyCacheNamespace: KVNamespace
475459
): FirebaseTokenVerifier {
476-
return new FirebaseTokenVerifier(
460+
const signatureVerifier = PublicKeySignatureVerifier.withCertificateUrl(
477461
CLIENT_JWK_URL,
478-
cfPublicKeyCacheNamespace,
462+
cacheKey,
463+
cfPublicKeyCacheNamespace
464+
);
465+
return createFirebaseTokenVerifier(signatureVerifier, projectID)
466+
}
467+
468+
/**
469+
* @internal
470+
* @returns FirebaseTokenVerifier
471+
*/
472+
export function createFirebaseTokenVerifier(
473+
signatureVerifier: SignatureVerifier,
474+
projectID: string
475+
): FirebaseTokenVerifier {
476+
return new FirebaseTokenVerifier(
477+
signatureVerifier,
479478
projectID,
480479
"https://securetoken.google.com/",
481480
ID_TOKEN_INFO
482481
);
483-
}
482+
}

0 commit comments

Comments
 (0)