From 8044f7dacec1603f0feb252a6ba07054d2a80438 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Mon, 8 Mar 2021 15:39:57 -0500 Subject: [PATCH 1/7] chore: Move Token verification logic to util --- src/auth/auth.ts | 2 +- src/utils/error.ts | 20 ++++++- src/{auth => utils}/token-verifier.ts | 77 +++++++++++++++------------ test/unit/auth/auth.spec.ts | 2 +- test/unit/auth/token-verifier.spec.ts | 40 +++++++++++++- 5 files changed, 102 insertions(+), 39 deletions(-) rename src/{auth => utils}/token-verifier.ts (85%) diff --git a/src/auth/auth.ts b/src/auth/auth.ts index aa5d7b11ef..0251a7d346 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -31,7 +31,7 @@ import * as validator from '../utils/validator'; import { auth } from './index'; import { FirebaseTokenVerifier, createSessionCookieVerifier, createIdTokenVerifier -} from './token-verifier'; +} from '../utils/token-verifier'; import { SAMLConfig, OIDCConfig, OIDCConfigServerResponse, SAMLConfigServerResponse, } from './auth-config'; diff --git a/src/utils/error.ts b/src/utils/error.ts index 5809a294b6..40dd26eab0 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -338,10 +338,28 @@ export class AppErrorCodes { public static UNABLE_TO_PARSE_RESPONSE = 'unable-to-parse-response'; } +/** + * Base class for client error codes and their default messages. + */ +export class BaseClientErrorCode { + public static INVALID_ARGUMENT = { + code: 'argument-error', + message: 'Invalid argument provided.', + }; + public static INVALID_CREDENTIAL = { + code: 'invalid-credential', + message: 'Invalid credential object provided.', + }; + public static INTERNAL_ERROR = { + code: 'internal-error', + message: 'An internal error has occurred.', + }; +} + /** * Auth client error codes and their default messages. */ -export class AuthClientErrorCode { +export class AuthClientErrorCode extends BaseClientErrorCode { public static BILLING_NOT_ENABLED = { code: 'billing-not-enabled', message: 'Feature requires billing to be enabled.', diff --git a/src/auth/token-verifier.ts b/src/utils/token-verifier.ts similarity index 85% rename from src/auth/token-verifier.ts rename to src/utils/token-verifier.ts index cbb9991f4c..72f2075966 100644 --- a/src/auth/token-verifier.ts +++ b/src/utils/token-verifier.ts @@ -14,13 +14,14 @@ * limitations under the License. */ -import { AuthClientErrorCode, FirebaseAuthError, ErrorInfo } from '../utils/error'; -import * as util from '../utils/index'; -import * as validator from '../utils/validator'; +import * as util from './index'; +import * as validator from './validator'; import * as jwt from 'jsonwebtoken'; -import { HttpClient, HttpRequestConfig, HttpError } from '../utils/api-request'; +import { HttpClient, HttpRequestConfig, HttpError } from './api-request'; import { FirebaseApp } from '../firebase-app'; -import { auth } from './index'; +import { ErrorInfo, PrefixedFirebaseError, + BaseClientErrorCode, AuthClientErrorCode, FirebaseAuthError } from './error'; +import { auth } from '../auth/index'; import DecodedIdToken = auth.DecodedIdToken; @@ -43,6 +44,8 @@ export const ID_TOKEN_INFO: FirebaseTokenInfo = { jwtName: 'Firebase ID token', shortName: 'ID token', expiredErrorCode: AuthClientErrorCode.ID_TOKEN_EXPIRED, + errorCodeType: AuthClientErrorCode, + errorType: FirebaseAuthError, }; /** User facing token information related to the Firebase session cookie. */ @@ -52,6 +55,8 @@ export const SESSION_COOKIE_INFO: FirebaseTokenInfo = { jwtName: 'Firebase session cookie', shortName: 'session cookie', expiredErrorCode: AuthClientErrorCode.SESSION_COOKIE_EXPIRED, + errorCodeType: AuthClientErrorCode, + errorType: FirebaseAuthError, }; /** Interface that defines token related user facing information. */ @@ -66,6 +71,10 @@ export interface FirebaseTokenInfo { shortName: string; /** JWT Expiration error code. */ expiredErrorCode: ErrorInfo; + /** Generic error code type. */ + errorCodeType: typeof BaseClientErrorCode; + /** Error type. */ + errorType: new (info: ErrorInfo, message?: string) => PrefixedFirebaseError; } /** @@ -81,48 +90,48 @@ export class FirebaseTokenVerifier { private readonly app: FirebaseApp) { if (!validator.isURL(clientCertUrl)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + throw new this.tokenInfo.errorType(this.tokenInfo.errorCodeType.INVALID_ARGUMENT, 'The provided public client certificate URL is an invalid URL.', ); + } else if (!validator.isNonEmptyString(algorithm)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + throw new this.tokenInfo.errorType( + this.tokenInfo.errorCodeType.INVALID_ARGUMENT, 'The provided JWT algorithm is an empty string.', ); } else if (!validator.isURL(issuer)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + throw new this.tokenInfo.errorType( + this.tokenInfo.errorCodeType.INVALID_ARGUMENT, 'The provided JWT issuer is an invalid URL.', ); } else if (!validator.isNonNullObject(tokenInfo)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + throw new this.tokenInfo.errorType( + this.tokenInfo.errorCodeType.INVALID_ARGUMENT, 'The provided JWT information is not an object or null.', ); } else if (!validator.isURL(tokenInfo.url)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + throw new this.tokenInfo.errorType( + this.tokenInfo.errorCodeType.INVALID_ARGUMENT, 'The provided JWT verification documentation URL is invalid.', ); } else if (!validator.isNonEmptyString(tokenInfo.verifyApiName)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + throw new this.tokenInfo.errorType( + this.tokenInfo.errorCodeType.INVALID_ARGUMENT, 'The JWT verify API name must be a non-empty string.', ); } else if (!validator.isNonEmptyString(tokenInfo.jwtName)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + throw new this.tokenInfo.errorType( + this.tokenInfo.errorCodeType.INVALID_ARGUMENT, 'The JWT public full name must be a non-empty string.', ); } else if (!validator.isNonEmptyString(tokenInfo.shortName)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + throw new this.tokenInfo.errorType( + this.tokenInfo.errorCodeType.INVALID_ARGUMENT, 'The JWT public short name must be a non-empty string.', ); } else if (!validator.isNonNullObject(tokenInfo.expiredErrorCode) || !('code' in tokenInfo.expiredErrorCode)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + throw new this.tokenInfo.errorType( + this.tokenInfo.errorCodeType.INVALID_ARGUMENT, 'The JWT expiration error code must be a non-null ErrorInfo object.', ); } @@ -141,8 +150,8 @@ export class FirebaseTokenVerifier { */ public verifyJWT(jwtToken: string, isEmulator = false): Promise { if (!validator.isString(jwtToken)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + throw new this.tokenInfo.errorType( + this.tokenInfo.errorCodeType.INVALID_ARGUMENT, `First argument to ${this.tokenInfo.verifyApiName} must be a ${this.tokenInfo.jwtName} string.`, ); } @@ -159,8 +168,8 @@ export class FirebaseTokenVerifier { isEmulator: boolean ): Promise { if (!validator.isNonEmptyString(projectId)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CREDENTIAL, + throw new this.tokenInfo.errorType( + this.tokenInfo.errorCodeType.INVALID_CREDENTIAL, 'Must initialize app with a cert credential or set your Firebase project ID as the ' + `GOOGLE_CLOUD_PROJECT environment variable to call ${this.tokenInfo.verifyApiName}.`, ); @@ -217,7 +226,7 @@ export class FirebaseTokenVerifier { verifyJwtTokenDocsMessage; } if (errorMessage) { - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage)); + return Promise.reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeType.INVALID_ARGUMENT, errorMessage)); } if (isEmulator) { @@ -228,8 +237,8 @@ export class FirebaseTokenVerifier { return this.fetchPublicKeys().then((publicKeys) => { if (!Object.prototype.hasOwnProperty.call(publicKeys, header.kid)) { return Promise.reject( - new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + new this.tokenInfo.errorType( + this.tokenInfo.errorCodeType.INVALID_ARGUMENT, `${this.tokenInfo.jwtName} has "kid" claim which does not correspond to a known public key. ` + `Most likely the ${this.tokenInfo.shortName} is expired, so get a fresh token from your ` + 'client app and try again.', @@ -264,12 +273,12 @@ export class FirebaseTokenVerifier { const errorMessage = `${this.tokenInfo.jwtName} has expired. Get a fresh ${this.tokenInfo.shortName}` + ` from your client app and try again (auth/${this.tokenInfo.expiredErrorCode.code}).` + verifyJwtTokenDocsMessage; - return reject(new FirebaseAuthError(this.tokenInfo.expiredErrorCode, errorMessage)); + return reject(new this.tokenInfo.errorType(this.tokenInfo.expiredErrorCode, errorMessage)); } else if (error.name === 'JsonWebTokenError') { const errorMessage = `${this.tokenInfo.jwtName} has invalid signature.` + verifyJwtTokenDocsMessage; - return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage)); + return reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeType.INVALID_ARGUMENT, errorMessage)); } - return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, error.message)); + return reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeType.INVALID_ARGUMENT, error.message)); } else { const decodedIdToken = (decodedToken as DecodedIdToken); decodedIdToken.uid = decodedIdToken.sub; @@ -329,7 +338,7 @@ export class FirebaseTokenVerifier { } else { errorMessage += `${resp.text}`; } - throw new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR, errorMessage); + throw new this.tokenInfo.errorType(this.tokenInfo.errorCodeType.INTERNAL_ERROR, errorMessage); } throw err; }); diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 71957bc4b4..b6e570c8d9 100644 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -36,7 +36,7 @@ import { import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; import * as validator from '../../../src/utils/validator'; -import { FirebaseTokenVerifier } from '../../../src/auth/token-verifier'; +import { FirebaseTokenVerifier } from '../../../src/utils/token-verifier'; import { OIDCConfig, SAMLConfig, OIDCConfigServerResponse, SAMLConfigServerResponse, } from '../../../src/auth/auth-config'; diff --git a/test/unit/auth/token-verifier.spec.ts b/test/unit/auth/token-verifier.spec.ts index d863a0e849..5661e07ada 100644 --- a/test/unit/auth/token-verifier.spec.ts +++ b/test/unit/auth/token-verifier.spec.ts @@ -29,10 +29,10 @@ import LegacyFirebaseTokenGenerator = require('firebase-token-generator'); import * as mocks from '../../resources/mocks'; import { FirebaseTokenGenerator, ServiceAccountSigner } from '../../../src/auth/token-generator'; -import * as verifier from '../../../src/auth/token-verifier'; +import * as verifier from '../../../src/utils/token-verifier'; import { ServiceAccountCredential } from '../../../src/credential/credential-internal'; -import { AuthClientErrorCode } from '../../../src/utils/error'; +import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; import { FirebaseApp } from '../../../src/firebase-app'; import { Algorithm } from 'jsonwebtoken'; @@ -160,6 +160,8 @@ describe('FirebaseTokenVerifier', () => { jwtName: 'Important Token', shortName: 'token', expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT, + errorCodeType: AuthClientErrorCode, + errorType: FirebaseAuthError, }, app, ); @@ -224,6 +226,8 @@ describe('FirebaseTokenVerifier', () => { jwtName: 'Important Token', shortName: 'token', expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT, + errorCodeType: AuthClientErrorCode, + errorType: FirebaseAuthError, }, app, ); @@ -245,6 +249,8 @@ describe('FirebaseTokenVerifier', () => { jwtName: invalidJwtName as any, shortName: 'token', expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT, + errorCodeType: AuthClientErrorCode, + errorType: FirebaseAuthError, }, app, ); @@ -266,6 +272,8 @@ describe('FirebaseTokenVerifier', () => { jwtName: 'Important Token', shortName: invalidShortName as any, expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT, + errorCodeType: AuthClientErrorCode, + errorType: FirebaseAuthError, }, app, ); @@ -287,6 +295,8 @@ describe('FirebaseTokenVerifier', () => { jwtName: 'Important Token', shortName: 'token', expiredErrorCode: invalidExpiredErrorCode as any, + errorCodeType: AuthClientErrorCode, + errorType: FirebaseAuthError, }, app, ); @@ -295,6 +305,32 @@ describe('FirebaseTokenVerifier', () => { }); }); + const errorTypes = [ + { type: FirebaseAuthError, code: AuthClientErrorCode }, + ]; + errorTypes.forEach((errorType) => { + it('should throw with the correct error type set in token info', () => { + expect(() => { + new verifier.FirebaseTokenVerifier( + 'https://www.example.com/publicKeys', + 'RS256', + 'https://www.example.com/issuer/', + { + url: 'https://docs.example.com/verify-tokens', + verifyApiName: 'verifyToken()', + jwtName: 'Important Token', + shortName: '', + expiredErrorCode: errorType.code.INVALID_ARGUMENT, + errorCodeType: errorType.code, + errorType: errorType.type, + }, + app, + ); + }).to.throw(errorType.type) + .with.property('code').and.match(/(auth|messaging)\/argument-error/); + }); + }); + describe('verifyJWT()', () => { let mockedRequests: nock.Scope[] = []; From 6719cfae202076bfe33062887c77bb6a5fb29009 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Tue, 9 Mar 2021 12:20:11 -0500 Subject: [PATCH 2/7] Moved Auth related items to token-verifier-util --- src/auth/auth.ts | 5 +- src/auth/token-verifier-util.ts | 89 ++++++++++++++++++++++ src/utils/error.ts | 29 +++---- src/utils/token-verifier.ts | 105 ++++++-------------------- test/unit/auth/token-verifier.spec.ts | 48 ++++++------ 5 files changed, 149 insertions(+), 127 deletions(-) create mode 100644 src/auth/token-verifier-util.ts diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 0251a7d346..89dea773bf 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -29,9 +29,8 @@ import { AuthClientErrorCode, FirebaseAuthError, ErrorInfo } from '../utils/erro import * as utils from '../utils/index'; import * as validator from '../utils/validator'; import { auth } from './index'; -import { - FirebaseTokenVerifier, createSessionCookieVerifier, createIdTokenVerifier -} from '../utils/token-verifier'; +import { FirebaseTokenVerifier } from '../utils/token-verifier'; +import { createSessionCookieVerifier, createIdTokenVerifier } from './token-verifier-util'; import { SAMLConfig, OIDCConfig, OIDCConfigServerResponse, SAMLConfigServerResponse, } from './auth-config'; diff --git a/src/auth/token-verifier-util.ts b/src/auth/token-verifier-util.ts new file mode 100644 index 0000000000..c274a42892 --- /dev/null +++ b/src/auth/token-verifier-util.ts @@ -0,0 +1,89 @@ +/*! + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FirebaseApp } from '../firebase-app'; +import { AuthClientErrorCode, ErrorCodeConfig, FirebaseAuthError } from '../utils/error'; +import { FirebaseTokenInfo, FirebaseTokenVerifier } from '../utils/token-verifier'; + +const ALGORITHM_RS256 = 'RS256'; + +// URL containing the public keys for the Google certs (whose private keys are used to sign Firebase +// Auth ID tokens) +const CLIENT_CERT_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'; + +// URL containing the public keys for Firebase session cookies. This will be updated to a different URL soon. +const SESSION_COOKIE_CERT_URL = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys'; + +/** Error codes that matches the FirebaseAuthError type */ +const AUTH_ERROR_CODE_CONFIG: ErrorCodeConfig = { + invalidArg: AuthClientErrorCode.INVALID_ARGUMENT, + invalidCredential: AuthClientErrorCode.INVALID_CREDENTIAL, + internalError: AuthClientErrorCode.INTERNAL_ERROR, +} + +/** User facing token information related to the Firebase ID token. */ +export const ID_TOKEN_INFO: FirebaseTokenInfo = { + url: 'https://firebase.google.com/docs/auth/admin/verify-id-tokens', + verifyApiName: 'verifyIdToken()', + jwtName: 'Firebase ID token', + shortName: 'ID token', + expiredErrorCode: AuthClientErrorCode.ID_TOKEN_EXPIRED, + errorCodeConfig: AUTH_ERROR_CODE_CONFIG, + errorType: FirebaseAuthError, +}; + +/** User facing token information related to the Firebase session cookie. */ +export const SESSION_COOKIE_INFO: FirebaseTokenInfo = { + url: 'https://firebase.google.com/docs/auth/admin/manage-cookies', + verifyApiName: 'verifySessionCookie()', + jwtName: 'Firebase session cookie', + shortName: 'session cookie', + expiredErrorCode: AuthClientErrorCode.SESSION_COOKIE_EXPIRED, + errorCodeConfig: AUTH_ERROR_CODE_CONFIG, + errorType: FirebaseAuthError, +}; + +/** + * Creates a new FirebaseTokenVerifier to verify Firebase ID tokens. + * + * @param {FirebaseApp} app Firebase app instance. + * @return {FirebaseTokenVerifier} + */ +export function createIdTokenVerifier(app: FirebaseApp): FirebaseTokenVerifier { + return new FirebaseTokenVerifier( + CLIENT_CERT_URL, + ALGORITHM_RS256, + 'https://securetoken.google.com/', + ID_TOKEN_INFO, + app + ); +} + +/** + * Creates a new FirebaseTokenVerifier to verify Firebase session cookies. + * + * @param {FirebaseApp} app Firebase app instance. + * @return {FirebaseTokenVerifier} + */ +export function createSessionCookieVerifier(app: FirebaseApp): FirebaseTokenVerifier { + return new FirebaseTokenVerifier( + SESSION_COOKIE_CERT_URL, + ALGORITHM_RS256, + 'https://session.firebase.google.com/', + SESSION_COOKIE_INFO, + app + ); +} diff --git a/src/utils/error.ts b/src/utils/error.ts index 40dd26eab0..47c25b1fbb 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -33,6 +33,15 @@ interface ServerToClientCode { [code: string]: string; } +/** + * Defines a type that stores commonly used error codes. + */ +export interface ErrorCodeConfig { + invalidArg: ErrorInfo; + invalidCredential: ErrorInfo; + internalError: ErrorInfo; +} + /** * Firebase error code structure. This extends Error. * @@ -338,28 +347,10 @@ export class AppErrorCodes { public static UNABLE_TO_PARSE_RESPONSE = 'unable-to-parse-response'; } -/** - * Base class for client error codes and their default messages. - */ -export class BaseClientErrorCode { - public static INVALID_ARGUMENT = { - code: 'argument-error', - message: 'Invalid argument provided.', - }; - public static INVALID_CREDENTIAL = { - code: 'invalid-credential', - message: 'Invalid credential object provided.', - }; - public static INTERNAL_ERROR = { - code: 'internal-error', - message: 'An internal error has occurred.', - }; -} - /** * Auth client error codes and their default messages. */ -export class AuthClientErrorCode extends BaseClientErrorCode { +export class AuthClientErrorCode { public static BILLING_NOT_ENABLED = { code: 'billing-not-enabled', message: 'Feature requires billing to be enabled.', diff --git a/src/utils/token-verifier.ts b/src/utils/token-verifier.ts index 72f2075966..930dc32141 100644 --- a/src/utils/token-verifier.ts +++ b/src/utils/token-verifier.ts @@ -19,8 +19,7 @@ import * as validator from './validator'; import * as jwt from 'jsonwebtoken'; import { HttpClient, HttpRequestConfig, HttpError } from './api-request'; import { FirebaseApp } from '../firebase-app'; -import { ErrorInfo, PrefixedFirebaseError, - BaseClientErrorCode, AuthClientErrorCode, FirebaseAuthError } from './error'; +import { ErrorCodeConfig, ErrorInfo, PrefixedFirebaseError } from './error'; import { auth } from '../auth/index'; import DecodedIdToken = auth.DecodedIdToken; @@ -28,37 +27,6 @@ import DecodedIdToken = auth.DecodedIdToken; // Audience to use for Firebase Auth Custom tokens const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit'; -export const ALGORITHM_RS256 = 'RS256'; - -// URL containing the public keys for the Google certs (whose private keys are used to sign Firebase -// Auth ID tokens) -const CLIENT_CERT_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'; - -// URL containing the public keys for Firebase session cookies. This will be updated to a different URL soon. -const SESSION_COOKIE_CERT_URL = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys'; - -/** User facing token information related to the Firebase ID token. */ -export const ID_TOKEN_INFO: FirebaseTokenInfo = { - url: 'https://firebase.google.com/docs/auth/admin/verify-id-tokens', - verifyApiName: 'verifyIdToken()', - jwtName: 'Firebase ID token', - shortName: 'ID token', - expiredErrorCode: AuthClientErrorCode.ID_TOKEN_EXPIRED, - errorCodeType: AuthClientErrorCode, - errorType: FirebaseAuthError, -}; - -/** User facing token information related to the Firebase session cookie. */ -export const SESSION_COOKIE_INFO: FirebaseTokenInfo = { - url: 'https://firebase.google.com/docs/auth/admin/manage-cookies', - verifyApiName: 'verifySessionCookie()', - jwtName: 'Firebase session cookie', - shortName: 'session cookie', - expiredErrorCode: AuthClientErrorCode.SESSION_COOKIE_EXPIRED, - errorCodeType: AuthClientErrorCode, - errorType: FirebaseAuthError, -}; - /** Interface that defines token related user facing information. */ export interface FirebaseTokenInfo { /** Documentation URL. */ @@ -71,9 +39,9 @@ export interface FirebaseTokenInfo { shortName: string; /** JWT Expiration error code. */ expiredErrorCode: ErrorInfo; - /** Generic error code type. */ - errorCodeType: typeof BaseClientErrorCode; - /** Error type. */ + /** Error code config of the public error type. */ + errorCodeConfig: ErrorCodeConfig; + /** Public error type. */ errorType: new (info: ErrorInfo, message?: string) => PrefixedFirebaseError; } @@ -90,48 +58,49 @@ export class FirebaseTokenVerifier { private readonly app: FirebaseApp) { if (!validator.isURL(clientCertUrl)) { - throw new this.tokenInfo.errorType(this.tokenInfo.errorCodeType.INVALID_ARGUMENT, + throw new this.tokenInfo.errorType( + this.tokenInfo.errorCodeConfig.invalidArg, 'The provided public client certificate URL is an invalid URL.', ); } else if (!validator.isNonEmptyString(algorithm)) { throw new this.tokenInfo.errorType( - this.tokenInfo.errorCodeType.INVALID_ARGUMENT, + this.tokenInfo.errorCodeConfig.invalidArg, 'The provided JWT algorithm is an empty string.', ); } else if (!validator.isURL(issuer)) { throw new this.tokenInfo.errorType( - this.tokenInfo.errorCodeType.INVALID_ARGUMENT, + this.tokenInfo.errorCodeConfig.invalidArg, 'The provided JWT issuer is an invalid URL.', ); } else if (!validator.isNonNullObject(tokenInfo)) { throw new this.tokenInfo.errorType( - this.tokenInfo.errorCodeType.INVALID_ARGUMENT, + this.tokenInfo.errorCodeConfig.invalidArg, 'The provided JWT information is not an object or null.', ); } else if (!validator.isURL(tokenInfo.url)) { throw new this.tokenInfo.errorType( - this.tokenInfo.errorCodeType.INVALID_ARGUMENT, + this.tokenInfo.errorCodeConfig.invalidArg, 'The provided JWT verification documentation URL is invalid.', ); } else if (!validator.isNonEmptyString(tokenInfo.verifyApiName)) { throw new this.tokenInfo.errorType( - this.tokenInfo.errorCodeType.INVALID_ARGUMENT, + this.tokenInfo.errorCodeConfig.invalidArg, 'The JWT verify API name must be a non-empty string.', ); } else if (!validator.isNonEmptyString(tokenInfo.jwtName)) { throw new this.tokenInfo.errorType( - this.tokenInfo.errorCodeType.INVALID_ARGUMENT, + this.tokenInfo.errorCodeConfig.invalidArg, 'The JWT public full name must be a non-empty string.', ); } else if (!validator.isNonEmptyString(tokenInfo.shortName)) { throw new this.tokenInfo.errorType( - this.tokenInfo.errorCodeType.INVALID_ARGUMENT, + this.tokenInfo.errorCodeConfig.invalidArg, 'The JWT public short name must be a non-empty string.', ); } else if (!validator.isNonNullObject(tokenInfo.expiredErrorCode) || !('code' in tokenInfo.expiredErrorCode)) { throw new this.tokenInfo.errorType( - this.tokenInfo.errorCodeType.INVALID_ARGUMENT, + this.tokenInfo.errorCodeConfig.invalidArg, 'The JWT expiration error code must be a non-null ErrorInfo object.', ); } @@ -151,7 +120,7 @@ export class FirebaseTokenVerifier { public verifyJWT(jwtToken: string, isEmulator = false): Promise { if (!validator.isString(jwtToken)) { throw new this.tokenInfo.errorType( - this.tokenInfo.errorCodeType.INVALID_ARGUMENT, + this.tokenInfo.errorCodeConfig.invalidArg, `First argument to ${this.tokenInfo.verifyApiName} must be a ${this.tokenInfo.jwtName} string.`, ); } @@ -169,7 +138,7 @@ export class FirebaseTokenVerifier { ): Promise { if (!validator.isNonEmptyString(projectId)) { throw new this.tokenInfo.errorType( - this.tokenInfo.errorCodeType.INVALID_CREDENTIAL, + this.tokenInfo.errorCodeConfig.invalidCredential, 'Must initialize app with a cert credential or set your Firebase project ID as the ' + `GOOGLE_CLOUD_PROJECT environment variable to call ${this.tokenInfo.verifyApiName}.`, ); @@ -226,7 +195,7 @@ export class FirebaseTokenVerifier { verifyJwtTokenDocsMessage; } if (errorMessage) { - return Promise.reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeType.INVALID_ARGUMENT, errorMessage)); + return Promise.reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeConfig.invalidArg, errorMessage)); } if (isEmulator) { @@ -238,7 +207,7 @@ export class FirebaseTokenVerifier { if (!Object.prototype.hasOwnProperty.call(publicKeys, header.kid)) { return Promise.reject( new this.tokenInfo.errorType( - this.tokenInfo.errorCodeType.INVALID_ARGUMENT, + this.tokenInfo.errorCodeConfig.invalidArg, `${this.tokenInfo.jwtName} has "kid" claim which does not correspond to a known public key. ` + `Most likely the ${this.tokenInfo.shortName} is expired, so get a fresh token from your ` + 'client app and try again.', @@ -276,9 +245,9 @@ export class FirebaseTokenVerifier { return reject(new this.tokenInfo.errorType(this.tokenInfo.expiredErrorCode, errorMessage)); } else if (error.name === 'JsonWebTokenError') { const errorMessage = `${this.tokenInfo.jwtName} has invalid signature.` + verifyJwtTokenDocsMessage; - return reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeType.INVALID_ARGUMENT, errorMessage)); + return reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeConfig.invalidArg, errorMessage)); } - return reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeType.INVALID_ARGUMENT, error.message)); + return reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeConfig.invalidArg, error.message)); } else { const decodedIdToken = (decodedToken as DecodedIdToken); decodedIdToken.uid = decodedIdToken.sub; @@ -338,41 +307,9 @@ export class FirebaseTokenVerifier { } else { errorMessage += `${resp.text}`; } - throw new this.tokenInfo.errorType(this.tokenInfo.errorCodeType.INTERNAL_ERROR, errorMessage); + throw new this.tokenInfo.errorType(this.tokenInfo.errorCodeConfig.internalError, errorMessage); } throw err; }); } } - -/** - * Creates a new FirebaseTokenVerifier to verify Firebase ID tokens. - * - * @param {FirebaseApp} app Firebase app instance. - * @return {FirebaseTokenVerifier} - */ -export function createIdTokenVerifier(app: FirebaseApp): FirebaseTokenVerifier { - return new FirebaseTokenVerifier( - CLIENT_CERT_URL, - ALGORITHM_RS256, - 'https://securetoken.google.com/', - ID_TOKEN_INFO, - app - ); -} - -/** - * Creates a new FirebaseTokenVerifier to verify Firebase session cookies. - * - * @param {FirebaseApp} app Firebase app instance. - * @return {FirebaseTokenVerifier} - */ -export function createSessionCookieVerifier(app: FirebaseApp): FirebaseTokenVerifier { - return new FirebaseTokenVerifier( - SESSION_COOKIE_CERT_URL, - ALGORITHM_RS256, - 'https://session.firebase.google.com/', - SESSION_COOKIE_INFO, - app - ); -} diff --git a/test/unit/auth/token-verifier.spec.ts b/test/unit/auth/token-verifier.spec.ts index 5661e07ada..d4cd1b168b 100644 --- a/test/unit/auth/token-verifier.spec.ts +++ b/test/unit/auth/token-verifier.spec.ts @@ -30,9 +30,10 @@ import LegacyFirebaseTokenGenerator = require('firebase-token-generator'); import * as mocks from '../../resources/mocks'; import { FirebaseTokenGenerator, ServiceAccountSigner } from '../../../src/auth/token-generator'; import * as verifier from '../../../src/utils/token-verifier'; +import * as verifierUtil from '../../../src/auth/token-verifier-util'; import { ServiceAccountCredential } from '../../../src/credential/credential-internal'; -import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; +import { AuthClientErrorCode, ErrorCodeConfig, FirebaseAuthError } from '../../../src/utils/error'; import { FirebaseApp } from '../../../src/firebase-app'; import { Algorithm } from 'jsonwebtoken'; @@ -45,6 +46,12 @@ const expect = chai.expect; const ONE_HOUR_IN_SECONDS = 60 * 60; const idTokenPublicCertPath = '/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'; +const AUTH_ERROR_CODE_CONFIG: ErrorCodeConfig = { + invalidArg: AuthClientErrorCode.INVALID_ARGUMENT, + invalidCredential: AuthClientErrorCode.INVALID_CREDENTIAL, + internalError: AuthClientErrorCode.INTERNAL_ERROR, +} + /** * Returns a mocked out success response from the URL containing the public keys for the Google certs. * @@ -114,7 +121,7 @@ function createTokenVerifier( 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', algorithm, 'https://securetoken.google.com/', - verifier.ID_TOKEN_INFO, + verifierUtil.ID_TOKEN_INFO, app ); } @@ -160,7 +167,7 @@ describe('FirebaseTokenVerifier', () => { jwtName: 'Important Token', shortName: 'token', expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT, - errorCodeType: AuthClientErrorCode, + errorCodeConfig: AUTH_ERROR_CODE_CONFIG, errorType: FirebaseAuthError, }, app, @@ -176,7 +183,7 @@ describe('FirebaseTokenVerifier', () => { invalidCertUrl as any, 'RS256', 'https://www.example.com/issuer/', - verifier.ID_TOKEN_INFO, + verifierUtil.ID_TOKEN_INFO, app, ); }).to.throw('The provided public client certificate URL is an invalid URL.'); @@ -191,7 +198,7 @@ describe('FirebaseTokenVerifier', () => { 'https://www.example.com/publicKeys', invalidAlgorithm as any, 'https://www.example.com/issuer/', - verifier.ID_TOKEN_INFO, + verifierUtil.ID_TOKEN_INFO, app); }).to.throw('The provided JWT algorithm is an empty string.'); }); @@ -205,7 +212,7 @@ describe('FirebaseTokenVerifier', () => { 'https://www.example.com/publicKeys', 'RS256', invalidIssuer as any, - verifier.ID_TOKEN_INFO, + verifierUtil.ID_TOKEN_INFO, app, ); }).to.throw('The provided JWT issuer is an invalid URL.'); @@ -226,7 +233,7 @@ describe('FirebaseTokenVerifier', () => { jwtName: 'Important Token', shortName: 'token', expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT, - errorCodeType: AuthClientErrorCode, + errorCodeConfig: AUTH_ERROR_CODE_CONFIG, errorType: FirebaseAuthError, }, app, @@ -249,7 +256,7 @@ describe('FirebaseTokenVerifier', () => { jwtName: invalidJwtName as any, shortName: 'token', expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT, - errorCodeType: AuthClientErrorCode, + errorCodeConfig: AUTH_ERROR_CODE_CONFIG, errorType: FirebaseAuthError, }, app, @@ -272,7 +279,7 @@ describe('FirebaseTokenVerifier', () => { jwtName: 'Important Token', shortName: invalidShortName as any, expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT, - errorCodeType: AuthClientErrorCode, + errorCodeConfig: AUTH_ERROR_CODE_CONFIG, errorType: FirebaseAuthError, }, app, @@ -295,7 +302,7 @@ describe('FirebaseTokenVerifier', () => { jwtName: 'Important Token', shortName: 'token', expiredErrorCode: invalidExpiredErrorCode as any, - errorCodeType: AuthClientErrorCode, + errorCodeConfig: AUTH_ERROR_CODE_CONFIG, errorType: FirebaseAuthError, }, app, @@ -306,7 +313,7 @@ describe('FirebaseTokenVerifier', () => { }); const errorTypes = [ - { type: FirebaseAuthError, code: AuthClientErrorCode }, + { type: FirebaseAuthError, code: AUTH_ERROR_CODE_CONFIG }, ]; errorTypes.forEach((errorType) => { it('should throw with the correct error type set in token info', () => { @@ -320,14 +327,13 @@ describe('FirebaseTokenVerifier', () => { verifyApiName: 'verifyToken()', jwtName: 'Important Token', shortName: '', - expiredErrorCode: errorType.code.INVALID_ARGUMENT, - errorCodeType: errorType.code, + expiredErrorCode: errorType.code.invalidArg, + errorCodeConfig: errorType.code, errorType: errorType.type, }, app, ); - }).to.throw(errorType.type) - .with.property('code').and.match(/(auth|messaging)\/argument-error/); + }).to.throw(errorType.type).with.property('code', 'auth/argument-error'); }); }); @@ -369,7 +375,7 @@ describe('FirebaseTokenVerifier', () => { 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', 'RS256', 'https://securetoken.google.com/', - verifier.ID_TOKEN_INFO, + verifierUtil.ID_TOKEN_INFO, mocks.mockCredentialApp(), ); const mockIdToken = mocks.generateIdToken(); @@ -476,7 +482,7 @@ describe('FirebaseTokenVerifier', () => { 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys', 'RS256', 'https://session.firebase.google.com/', - verifier.SESSION_COOKIE_INFO, + verifierUtil.SESSION_COOKIE_INFO, app, ); mockedRequests.push(mockFetchPublicKeys('/identitytoolkit/v3/relyingparty/publicKeys')); @@ -521,7 +527,7 @@ describe('FirebaseTokenVerifier', () => { 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys', 'RS256', 'https://session.firebase.google.com/', - verifier.SESSION_COOKIE_INFO, + verifierUtil.SESSION_COOKIE_INFO, app, ); return tokenGenerator.createCustomToken(mocks.uid) @@ -547,7 +553,7 @@ describe('FirebaseTokenVerifier', () => { 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys', 'RS256', 'https://session.firebase.google.com/', - verifier.SESSION_COOKIE_INFO, + verifierUtil.SESSION_COOKIE_INFO, app, ); const legacyTokenGenerator = new LegacyFirebaseTokenGenerator('foo'); @@ -640,7 +646,7 @@ describe('FirebaseTokenVerifier', () => { 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', 'RS256', 'https://securetoken.google.com/', - verifier.ID_TOKEN_INFO, + verifierUtil.ID_TOKEN_INFO, appWithAgent, ); mockedRequests.push(mockFetchPublicKeys()); @@ -663,7 +669,7 @@ describe('FirebaseTokenVerifier', () => { 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', 'RS256', 'https://securetoken.google.com/', - verifier.ID_TOKEN_INFO, + verifierUtil.ID_TOKEN_INFO, app, ); expect(https.request).not.to.have.been.called; From 913ce37bb768c1d1ea52c85c49f8161495bf44c3 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Tue, 9 Mar 2021 13:50:26 -0500 Subject: [PATCH 3/7] Change the value errors in FirebaseTokenVerifier constructor to Error types. --- src/utils/token-verifier.ts | 49 ++++++--------------------- test/unit/auth/token-verifier.spec.ts | 37 ++++++++++---------- 2 files changed, 31 insertions(+), 55 deletions(-) diff --git a/src/utils/token-verifier.ts b/src/utils/token-verifier.ts index 930dc32141..6080d229e2 100644 --- a/src/utils/token-verifier.ts +++ b/src/utils/token-verifier.ts @@ -58,51 +58,24 @@ export class FirebaseTokenVerifier { private readonly app: FirebaseApp) { if (!validator.isURL(clientCertUrl)) { - throw new this.tokenInfo.errorType( - this.tokenInfo.errorCodeConfig.invalidArg, - 'The provided public client certificate URL is an invalid URL.', - ); - + throw new Error('The provided public client certificate URL is an invalid URL.'); } else if (!validator.isNonEmptyString(algorithm)) { - throw new this.tokenInfo.errorType( - this.tokenInfo.errorCodeConfig.invalidArg, - 'The provided JWT algorithm is an empty string.', - ); + throw new Error('The provided JWT algorithm is an empty string.'); } else if (!validator.isURL(issuer)) { - throw new this.tokenInfo.errorType( - this.tokenInfo.errorCodeConfig.invalidArg, - 'The provided JWT issuer is an invalid URL.', - ); + throw new Error('The provided JWT issuer is an invalid URL.'); } else if (!validator.isNonNullObject(tokenInfo)) { - throw new this.tokenInfo.errorType( - this.tokenInfo.errorCodeConfig.invalidArg, - 'The provided JWT information is not an object or null.', - ); + throw new Error('The provided JWT information is not an object or null.'); } else if (!validator.isURL(tokenInfo.url)) { - throw new this.tokenInfo.errorType( - this.tokenInfo.errorCodeConfig.invalidArg, - 'The provided JWT verification documentation URL is invalid.', - ); + throw new Error('The provided JWT verification documentation URL is invalid.'); } else if (!validator.isNonEmptyString(tokenInfo.verifyApiName)) { - throw new this.tokenInfo.errorType( - this.tokenInfo.errorCodeConfig.invalidArg, - 'The JWT verify API name must be a non-empty string.', - ); + throw new Error('The JWT verify API name must be a non-empty string.'); } else if (!validator.isNonEmptyString(tokenInfo.jwtName)) { - throw new this.tokenInfo.errorType( - this.tokenInfo.errorCodeConfig.invalidArg, - 'The JWT public full name must be a non-empty string.', - ); + throw new Error('The JWT public full name must be a non-empty string.'); } else if (!validator.isNonEmptyString(tokenInfo.shortName)) { - throw new this.tokenInfo.errorType( - this.tokenInfo.errorCodeConfig.invalidArg, - 'The JWT public short name must be a non-empty string.', - ); - } else if (!validator.isNonNullObject(tokenInfo.expiredErrorCode) || !('code' in tokenInfo.expiredErrorCode)) { - throw new this.tokenInfo.errorType( - this.tokenInfo.errorCodeConfig.invalidArg, - 'The JWT expiration error code must be a non-null ErrorInfo object.', - ); + throw new Error('The JWT public short name must be a non-empty string.'); + } else if (!validator.isNonNullObject(tokenInfo.expiredErrorCode) || + !('code' in tokenInfo.expiredErrorCode)) { + throw new Error('The JWT expiration error code must be a non-null ErrorInfo object.'); } this.shortNameArticle = tokenInfo.shortName.charAt(0).match(/[aeiou]/i) ? 'an' : 'a'; diff --git a/test/unit/auth/token-verifier.spec.ts b/test/unit/auth/token-verifier.spec.ts index d4cd1b168b..1806cd5284 100644 --- a/test/unit/auth/token-verifier.spec.ts +++ b/test/unit/auth/token-verifier.spec.ts @@ -316,24 +316,27 @@ describe('FirebaseTokenVerifier', () => { { type: FirebaseAuthError, code: AUTH_ERROR_CODE_CONFIG }, ]; errorTypes.forEach((errorType) => { - it('should throw with the correct error type set in token info', () => { + const tokenVerifier = new verifier.FirebaseTokenVerifier( + 'https://www.example.com/publicKeys', + 'RS256', + 'https://www.example.com/issuer/', + { + url: 'https://docs.example.com/verify-tokens', + verifyApiName: 'verifyToken()', + jwtName: 'Important Token', + shortName: 'token', + expiredErrorCode: errorType.code.invalidArg, + errorCodeConfig: errorType.code, + errorType: errorType.type, + }, + app, + ); + it('should throw with the correct error type and code set in token info', () => { expect(() => { - new verifier.FirebaseTokenVerifier( - 'https://www.example.com/publicKeys', - 'RS256', - 'https://www.example.com/issuer/', - { - url: 'https://docs.example.com/verify-tokens', - verifyApiName: 'verifyToken()', - jwtName: 'Important Token', - shortName: '', - expiredErrorCode: errorType.code.invalidArg, - errorCodeConfig: errorType.code, - errorType: errorType.type, - }, - app, - ); - }).to.throw(errorType.type).with.property('code', 'auth/argument-error'); + (tokenVerifier as any).verifyJWT(); + }).to.throw(errorType.type).with.property('code').match( + new RegExp(`(.*)/${errorType.code.invalidArg.code}`) + ); }); }); From bba0b022694e2fa665694a2f1f4f20562bdea0a3 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Tue, 9 Mar 2021 13:56:59 -0500 Subject: [PATCH 4/7] Move the error type test into verifyJWT() tests --- test/unit/auth/token-verifier.spec.ts | 56 +++++++++++++-------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/test/unit/auth/token-verifier.spec.ts b/test/unit/auth/token-verifier.spec.ts index 1806cd5284..70d3876070 100644 --- a/test/unit/auth/token-verifier.spec.ts +++ b/test/unit/auth/token-verifier.spec.ts @@ -312,34 +312,6 @@ describe('FirebaseTokenVerifier', () => { }); }); - const errorTypes = [ - { type: FirebaseAuthError, code: AUTH_ERROR_CODE_CONFIG }, - ]; - errorTypes.forEach((errorType) => { - const tokenVerifier = new verifier.FirebaseTokenVerifier( - 'https://www.example.com/publicKeys', - 'RS256', - 'https://www.example.com/issuer/', - { - url: 'https://docs.example.com/verify-tokens', - verifyApiName: 'verifyToken()', - jwtName: 'Important Token', - shortName: 'token', - expiredErrorCode: errorType.code.invalidArg, - errorCodeConfig: errorType.code, - errorType: errorType.type, - }, - app, - ); - it('should throw with the correct error type and code set in token info', () => { - expect(() => { - (tokenVerifier as any).verifyJWT(); - }).to.throw(errorType.type).with.property('code').match( - new RegExp(`(.*)/${errorType.code.invalidArg.code}`) - ); - }); - }); - describe('verifyJWT()', () => { let mockedRequests: nock.Scope[] = []; @@ -354,6 +326,34 @@ describe('FirebaseTokenVerifier', () => { }).to.throw('First argument to verifyIdToken() must be a Firebase ID token'); }); + const errorTypes = [ + { type: FirebaseAuthError, config: AUTH_ERROR_CODE_CONFIG }, + ]; + errorTypes.forEach((errorType) => { + const tokenVerifier = new verifier.FirebaseTokenVerifier( + 'https://www.example.com/publicKeys', + 'RS256', + 'https://www.example.com/issuer/', + { + url: 'https://docs.example.com/verify-tokens', + verifyApiName: 'verifyToken()', + jwtName: 'Important Token', + shortName: 'token', + expiredErrorCode: errorType.config.invalidArg, + errorCodeConfig: errorType.config, + errorType: errorType.type, + }, + app, + ); + it('should throw with the correct error type and code set in token info', () => { + expect(() => { + (tokenVerifier as any).verifyJWT(); + }).to.throw(errorType.type).with.property('code').match( + new RegExp(`(.*)/${errorType.config.invalidArg.code}`) + ); + }); + }); + const invalidIdTokens = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; invalidIdTokens.forEach((invalidIdToken) => { it('should throw given a non-string Firebase JWT token: ' + JSON.stringify(invalidIdToken), () => { From 27e0e6fe95c28d00ebe43c17c1aa38e4eb7f66c4 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 10 Mar 2021 17:08:12 -0500 Subject: [PATCH 5/7] Merge expired error code to erro code config --- src/auth/auth.ts | 2 +- ...ken-verifier-util.ts => token-verifier.ts} | 19 +++-- src/utils/error.ts | 1 + src/utils/token-verifier.ts | 17 ++-- test/unit/auth/token-verifier.spec.ts | 77 ++++++++++--------- 5 files changed, 66 insertions(+), 50 deletions(-) rename src/auth/{token-verifier-util.ts => token-verifier.ts} (82%) diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 89dea773bf..71738fa0b0 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -30,7 +30,7 @@ import * as utils from '../utils/index'; import * as validator from '../utils/validator'; import { auth } from './index'; import { FirebaseTokenVerifier } from '../utils/token-verifier'; -import { createSessionCookieVerifier, createIdTokenVerifier } from './token-verifier-util'; +import { createSessionCookieVerifier, createIdTokenVerifier } from './token-verifier'; import { SAMLConfig, OIDCConfig, OIDCConfigServerResponse, SAMLConfigServerResponse, } from './auth-config'; diff --git a/src/auth/token-verifier-util.ts b/src/auth/token-verifier.ts similarity index 82% rename from src/auth/token-verifier-util.ts rename to src/auth/token-verifier.ts index c274a42892..c1ce098a06 100644 --- a/src/auth/token-verifier-util.ts +++ b/src/auth/token-verifier.ts @@ -27,11 +27,20 @@ const CLIENT_CERT_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/secur // URL containing the public keys for Firebase session cookies. This will be updated to a different URL soon. const SESSION_COOKIE_CERT_URL = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys'; -/** Error codes that matches the FirebaseAuthError type */ -const AUTH_ERROR_CODE_CONFIG: ErrorCodeConfig = { +/** Matching Auth error code config for ID token */ +export const ID_TOKEN_ERROR_CODE_CONFIG: ErrorCodeConfig = { invalidArg: AuthClientErrorCode.INVALID_ARGUMENT, invalidCredential: AuthClientErrorCode.INVALID_CREDENTIAL, internalError: AuthClientErrorCode.INTERNAL_ERROR, + expiredError: AuthClientErrorCode.ID_TOKEN_EXPIRED, +} + +/** Matching Auth error code config for session cookie */ +export const SESSION_COOKIE_ERROR_CODE_CONFIG: ErrorCodeConfig = { + invalidArg: AuthClientErrorCode.INVALID_ARGUMENT, + invalidCredential: AuthClientErrorCode.INVALID_CREDENTIAL, + internalError: AuthClientErrorCode.INTERNAL_ERROR, + expiredError: AuthClientErrorCode.SESSION_COOKIE_EXPIRED, } /** User facing token information related to the Firebase ID token. */ @@ -40,8 +49,7 @@ export const ID_TOKEN_INFO: FirebaseTokenInfo = { verifyApiName: 'verifyIdToken()', jwtName: 'Firebase ID token', shortName: 'ID token', - expiredErrorCode: AuthClientErrorCode.ID_TOKEN_EXPIRED, - errorCodeConfig: AUTH_ERROR_CODE_CONFIG, + errorCodeConfig: ID_TOKEN_ERROR_CODE_CONFIG, errorType: FirebaseAuthError, }; @@ -51,8 +59,7 @@ export const SESSION_COOKIE_INFO: FirebaseTokenInfo = { verifyApiName: 'verifySessionCookie()', jwtName: 'Firebase session cookie', shortName: 'session cookie', - expiredErrorCode: AuthClientErrorCode.SESSION_COOKIE_EXPIRED, - errorCodeConfig: AUTH_ERROR_CODE_CONFIG, + errorCodeConfig: SESSION_COOKIE_ERROR_CODE_CONFIG, errorType: FirebaseAuthError, }; diff --git a/src/utils/error.ts b/src/utils/error.ts index 47c25b1fbb..6df690398a 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -40,6 +40,7 @@ export interface ErrorCodeConfig { invalidArg: ErrorInfo; invalidCredential: ErrorInfo; internalError: ErrorInfo; + expiredError: ErrorInfo; } /** diff --git a/src/utils/token-verifier.ts b/src/utils/token-verifier.ts index 6080d229e2..6e9593b188 100644 --- a/src/utils/token-verifier.ts +++ b/src/utils/token-verifier.ts @@ -37,8 +37,6 @@ export interface FirebaseTokenInfo { jwtName: string; /** The JWT short name. */ shortName: string; - /** JWT Expiration error code. */ - expiredErrorCode: ErrorInfo; /** Error code config of the public error type. */ errorCodeConfig: ErrorCodeConfig; /** Public error type. */ @@ -73,9 +71,14 @@ export class FirebaseTokenVerifier { throw new Error('The JWT public full name must be a non-empty string.'); } else if (!validator.isNonEmptyString(tokenInfo.shortName)) { throw new Error('The JWT public short name must be a non-empty string.'); - } else if (!validator.isNonNullObject(tokenInfo.expiredErrorCode) || - !('code' in tokenInfo.expiredErrorCode)) { - throw new Error('The JWT expiration error code must be a non-null ErrorInfo object.'); + } else if (!(typeof tokenInfo.errorType === 'function' && tokenInfo.errorType !== null)) { + throw new Error('The provided error type must be a non-null PrefixedFirebaseError type.'); + } else if (!validator.isNonNullObject(tokenInfo.errorCodeConfig) || + !('invalidArg' in tokenInfo.errorCodeConfig || + 'invalidCredential' in tokenInfo.errorCodeConfig || + 'internalError' in tokenInfo.errorCodeConfig || + 'expiredError' in tokenInfo.errorCodeConfig)) { + throw new Error('The provided error code config must be a non-null ErrorCodeInfo object.'); } this.shortNameArticle = tokenInfo.shortName.charAt(0).match(/[aeiou]/i) ? 'an' : 'a'; @@ -213,9 +216,9 @@ export class FirebaseTokenVerifier { if (error) { if (error.name === 'TokenExpiredError') { const errorMessage = `${this.tokenInfo.jwtName} has expired. Get a fresh ${this.tokenInfo.shortName}` + - ` from your client app and try again (auth/${this.tokenInfo.expiredErrorCode.code}).` + + ` from your client app and try again (auth/${this.tokenInfo.errorCodeConfig.expiredError.code}).` + verifyJwtTokenDocsMessage; - return reject(new this.tokenInfo.errorType(this.tokenInfo.expiredErrorCode, errorMessage)); + return reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeConfig.expiredError, errorMessage)); } else if (error.name === 'JsonWebTokenError') { const errorMessage = `${this.tokenInfo.jwtName} has invalid signature.` + verifyJwtTokenDocsMessage; return reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeConfig.invalidArg, errorMessage)); diff --git a/test/unit/auth/token-verifier.spec.ts b/test/unit/auth/token-verifier.spec.ts index 70d3876070..1adf7ceabb 100644 --- a/test/unit/auth/token-verifier.spec.ts +++ b/test/unit/auth/token-verifier.spec.ts @@ -30,10 +30,10 @@ import LegacyFirebaseTokenGenerator = require('firebase-token-generator'); import * as mocks from '../../resources/mocks'; import { FirebaseTokenGenerator, ServiceAccountSigner } from '../../../src/auth/token-generator'; import * as verifier from '../../../src/utils/token-verifier'; -import * as verifierUtil from '../../../src/auth/token-verifier-util'; +import * as verifierUtil from '../../../src/auth/token-verifier'; import { ServiceAccountCredential } from '../../../src/credential/credential-internal'; -import { AuthClientErrorCode, ErrorCodeConfig, FirebaseAuthError } from '../../../src/utils/error'; +import { FirebaseAuthError } from '../../../src/utils/error'; import { FirebaseApp } from '../../../src/firebase-app'; import { Algorithm } from 'jsonwebtoken'; @@ -46,12 +46,6 @@ const expect = chai.expect; const ONE_HOUR_IN_SECONDS = 60 * 60; const idTokenPublicCertPath = '/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'; -const AUTH_ERROR_CODE_CONFIG: ErrorCodeConfig = { - invalidArg: AuthClientErrorCode.INVALID_ARGUMENT, - invalidCredential: AuthClientErrorCode.INVALID_CREDENTIAL, - internalError: AuthClientErrorCode.INTERNAL_ERROR, -} - /** * Returns a mocked out success response from the URL containing the public keys for the Google certs. * @@ -166,8 +160,7 @@ describe('FirebaseTokenVerifier', () => { verifyApiName: 'verifyToken()', jwtName: 'Important Token', shortName: 'token', - expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT, - errorCodeConfig: AUTH_ERROR_CODE_CONFIG, + errorCodeConfig: verifierUtil.ID_TOKEN_ERROR_CODE_CONFIG, errorType: FirebaseAuthError, }, app, @@ -232,8 +225,7 @@ describe('FirebaseTokenVerifier', () => { verifyApiName: invalidVerifyApiName as any, jwtName: 'Important Token', shortName: 'token', - expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT, - errorCodeConfig: AUTH_ERROR_CODE_CONFIG, + errorCodeConfig: verifierUtil.ID_TOKEN_ERROR_CODE_CONFIG, errorType: FirebaseAuthError, }, app, @@ -255,8 +247,7 @@ describe('FirebaseTokenVerifier', () => { verifyApiName: 'verifyToken()', jwtName: invalidJwtName as any, shortName: 'token', - expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT, - errorCodeConfig: AUTH_ERROR_CODE_CONFIG, + errorCodeConfig: verifierUtil.ID_TOKEN_ERROR_CODE_CONFIG, errorType: FirebaseAuthError, }, app, @@ -278,8 +269,7 @@ describe('FirebaseTokenVerifier', () => { verifyApiName: 'verifyToken()', jwtName: 'Important Token', shortName: invalidShortName as any, - expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT, - errorCodeConfig: AUTH_ERROR_CODE_CONFIG, + errorCodeConfig: verifierUtil.ID_TOKEN_ERROR_CODE_CONFIG, errorType: FirebaseAuthError, }, app, @@ -288,9 +278,9 @@ describe('FirebaseTokenVerifier', () => { }); }); - const invalidExpiredErrorCodes = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop, '', 'test']; - invalidExpiredErrorCodes.forEach((invalidExpiredErrorCode) => { - it('should throw given an invalid expiration error code: ' + JSON.stringify(invalidExpiredErrorCode), () => { + const invalidErrorCodeTypes = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop, '', 'test']; + invalidErrorCodeTypes.forEach((invalidErrorCodeType) => { + it('should throw given an invalid error code config: ' + JSON.stringify(invalidErrorCodeType), () => { expect(() => { new verifier.FirebaseTokenVerifier( 'https://www.example.com/publicKeys', @@ -301,13 +291,34 @@ describe('FirebaseTokenVerifier', () => { verifyApiName: 'verifyToken()', jwtName: 'Important Token', shortName: 'token', - expiredErrorCode: invalidExpiredErrorCode as any, - errorCodeConfig: AUTH_ERROR_CODE_CONFIG, + errorCodeConfig: verifierUtil.ID_TOKEN_ERROR_CODE_CONFIG, + errorType: invalidErrorCodeTypes as any, + }, + app, + ); + }).to.throw('The provided error type must be a non-null PrefixedFirebaseError type.'); + }); + }); + + const invalidErrorCodeConfigs = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop, '', 'test']; + invalidErrorCodeConfigs.forEach((invalidErrorCodeConfig) => { + it('should throw given an invalid error code config: ' + JSON.stringify(invalidErrorCodeConfig), () => { + expect(() => { + new verifier.FirebaseTokenVerifier( + 'https://www.example.com/publicKeys', + 'RS256', + 'https://www.example.com/issuer/', + { + url: 'https://docs.example.com/verify-tokens', + verifyApiName: 'verifyToken()', + jwtName: 'Important Token', + shortName: 'token', + errorCodeConfig: invalidErrorCodeConfig as any, errorType: FirebaseAuthError, }, app, ); - }).to.throw('The JWT expiration error code must be a non-null ErrorInfo object.'); + }).to.throw('The provided error code config must be a non-null ErrorCodeInfo object.'); }); }); }); @@ -326,10 +337,9 @@ describe('FirebaseTokenVerifier', () => { }).to.throw('First argument to verifyIdToken() must be a Firebase ID token'); }); - const errorTypes = [ - { type: FirebaseAuthError, config: AUTH_ERROR_CODE_CONFIG }, - ]; - errorTypes.forEach((errorType) => { + it('should throw with the correct error type and code set in token info', () => { + const errorType = FirebaseAuthError; + const errorCodeConfig = verifierUtil.ID_TOKEN_ERROR_CODE_CONFIG; const tokenVerifier = new verifier.FirebaseTokenVerifier( 'https://www.example.com/publicKeys', 'RS256', @@ -339,19 +349,14 @@ describe('FirebaseTokenVerifier', () => { verifyApiName: 'verifyToken()', jwtName: 'Important Token', shortName: 'token', - expiredErrorCode: errorType.config.invalidArg, - errorCodeConfig: errorType.config, - errorType: errorType.type, + errorCodeConfig: errorCodeConfig, + errorType, }, app, ); - it('should throw with the correct error type and code set in token info', () => { - expect(() => { - (tokenVerifier as any).verifyJWT(); - }).to.throw(errorType.type).with.property('code').match( - new RegExp(`(.*)/${errorType.config.invalidArg.code}`) - ); - }); + expect(() => { + (tokenVerifier as any).verifyJWT(); + }).to.throw(errorType).with.property('code').equal(`auth/${errorCodeConfig.invalidArg.code}`); }); const invalidIdTokens = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; From 5d3597a3d2c988cd4618ee56100b7ad9c9f2fb0d Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 10 Mar 2021 17:22:49 -0500 Subject: [PATCH 6/7] Fixed copyright years --- src/auth/token-verifier.ts | 2 +- src/utils/token-verifier.ts | 2 +- test/unit/auth/token-verifier.spec.ts | 34 +++++++++++++-------------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/auth/token-verifier.ts b/src/auth/token-verifier.ts index c1ce098a06..be49f04b2b 100644 --- a/src/auth/token-verifier.ts +++ b/src/auth/token-verifier.ts @@ -1,5 +1,5 @@ /*! - * Copyright 2021 Google Inc. + * Copyright 2018 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/utils/token-verifier.ts b/src/utils/token-verifier.ts index 6e9593b188..d2b9268180 100644 --- a/src/utils/token-verifier.ts +++ b/src/utils/token-verifier.ts @@ -1,5 +1,5 @@ /*! - * Copyright 2018 Google Inc. + * Copyright 2021 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/test/unit/auth/token-verifier.spec.ts b/test/unit/auth/token-verifier.spec.ts index 1adf7ceabb..9494b9736a 100644 --- a/test/unit/auth/token-verifier.spec.ts +++ b/test/unit/auth/token-verifier.spec.ts @@ -30,7 +30,7 @@ import LegacyFirebaseTokenGenerator = require('firebase-token-generator'); import * as mocks from '../../resources/mocks'; import { FirebaseTokenGenerator, ServiceAccountSigner } from '../../../src/auth/token-generator'; import * as verifier from '../../../src/utils/token-verifier'; -import * as verifierUtil from '../../../src/auth/token-verifier'; +import * as verifierAuth from '../../../src/auth/token-verifier'; import { ServiceAccountCredential } from '../../../src/credential/credential-internal'; import { FirebaseAuthError } from '../../../src/utils/error'; @@ -115,7 +115,7 @@ function createTokenVerifier( 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', algorithm, 'https://securetoken.google.com/', - verifierUtil.ID_TOKEN_INFO, + verifierAuth.ID_TOKEN_INFO, app ); } @@ -160,7 +160,7 @@ describe('FirebaseTokenVerifier', () => { verifyApiName: 'verifyToken()', jwtName: 'Important Token', shortName: 'token', - errorCodeConfig: verifierUtil.ID_TOKEN_ERROR_CODE_CONFIG, + errorCodeConfig: verifierAuth.ID_TOKEN_ERROR_CODE_CONFIG, errorType: FirebaseAuthError, }, app, @@ -176,7 +176,7 @@ describe('FirebaseTokenVerifier', () => { invalidCertUrl as any, 'RS256', 'https://www.example.com/issuer/', - verifierUtil.ID_TOKEN_INFO, + verifierAuth.ID_TOKEN_INFO, app, ); }).to.throw('The provided public client certificate URL is an invalid URL.'); @@ -191,7 +191,7 @@ describe('FirebaseTokenVerifier', () => { 'https://www.example.com/publicKeys', invalidAlgorithm as any, 'https://www.example.com/issuer/', - verifierUtil.ID_TOKEN_INFO, + verifierAuth.ID_TOKEN_INFO, app); }).to.throw('The provided JWT algorithm is an empty string.'); }); @@ -205,7 +205,7 @@ describe('FirebaseTokenVerifier', () => { 'https://www.example.com/publicKeys', 'RS256', invalidIssuer as any, - verifierUtil.ID_TOKEN_INFO, + verifierAuth.ID_TOKEN_INFO, app, ); }).to.throw('The provided JWT issuer is an invalid URL.'); @@ -225,7 +225,7 @@ describe('FirebaseTokenVerifier', () => { verifyApiName: invalidVerifyApiName as any, jwtName: 'Important Token', shortName: 'token', - errorCodeConfig: verifierUtil.ID_TOKEN_ERROR_CODE_CONFIG, + errorCodeConfig: verifierAuth.ID_TOKEN_ERROR_CODE_CONFIG, errorType: FirebaseAuthError, }, app, @@ -247,7 +247,7 @@ describe('FirebaseTokenVerifier', () => { verifyApiName: 'verifyToken()', jwtName: invalidJwtName as any, shortName: 'token', - errorCodeConfig: verifierUtil.ID_TOKEN_ERROR_CODE_CONFIG, + errorCodeConfig: verifierAuth.ID_TOKEN_ERROR_CODE_CONFIG, errorType: FirebaseAuthError, }, app, @@ -269,7 +269,7 @@ describe('FirebaseTokenVerifier', () => { verifyApiName: 'verifyToken()', jwtName: 'Important Token', shortName: invalidShortName as any, - errorCodeConfig: verifierUtil.ID_TOKEN_ERROR_CODE_CONFIG, + errorCodeConfig: verifierAuth.ID_TOKEN_ERROR_CODE_CONFIG, errorType: FirebaseAuthError, }, app, @@ -291,7 +291,7 @@ describe('FirebaseTokenVerifier', () => { verifyApiName: 'verifyToken()', jwtName: 'Important Token', shortName: 'token', - errorCodeConfig: verifierUtil.ID_TOKEN_ERROR_CODE_CONFIG, + errorCodeConfig: verifierAuth.ID_TOKEN_ERROR_CODE_CONFIG, errorType: invalidErrorCodeTypes as any, }, app, @@ -339,7 +339,7 @@ describe('FirebaseTokenVerifier', () => { it('should throw with the correct error type and code set in token info', () => { const errorType = FirebaseAuthError; - const errorCodeConfig = verifierUtil.ID_TOKEN_ERROR_CODE_CONFIG; + const errorCodeConfig = verifierAuth.ID_TOKEN_ERROR_CODE_CONFIG; const tokenVerifier = new verifier.FirebaseTokenVerifier( 'https://www.example.com/publicKeys', 'RS256', @@ -383,7 +383,7 @@ describe('FirebaseTokenVerifier', () => { 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', 'RS256', 'https://securetoken.google.com/', - verifierUtil.ID_TOKEN_INFO, + verifierAuth.ID_TOKEN_INFO, mocks.mockCredentialApp(), ); const mockIdToken = mocks.generateIdToken(); @@ -490,7 +490,7 @@ describe('FirebaseTokenVerifier', () => { 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys', 'RS256', 'https://session.firebase.google.com/', - verifierUtil.SESSION_COOKIE_INFO, + verifierAuth.SESSION_COOKIE_INFO, app, ); mockedRequests.push(mockFetchPublicKeys('/identitytoolkit/v3/relyingparty/publicKeys')); @@ -535,7 +535,7 @@ describe('FirebaseTokenVerifier', () => { 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys', 'RS256', 'https://session.firebase.google.com/', - verifierUtil.SESSION_COOKIE_INFO, + verifierAuth.SESSION_COOKIE_INFO, app, ); return tokenGenerator.createCustomToken(mocks.uid) @@ -561,7 +561,7 @@ describe('FirebaseTokenVerifier', () => { 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys', 'RS256', 'https://session.firebase.google.com/', - verifierUtil.SESSION_COOKIE_INFO, + verifierAuth.SESSION_COOKIE_INFO, app, ); const legacyTokenGenerator = new LegacyFirebaseTokenGenerator('foo'); @@ -654,7 +654,7 @@ describe('FirebaseTokenVerifier', () => { 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', 'RS256', 'https://securetoken.google.com/', - verifierUtil.ID_TOKEN_INFO, + verifierAuth.ID_TOKEN_INFO, appWithAgent, ); mockedRequests.push(mockFetchPublicKeys()); @@ -677,7 +677,7 @@ describe('FirebaseTokenVerifier', () => { 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', 'RS256', 'https://securetoken.google.com/', - verifierUtil.ID_TOKEN_INFO, + verifierAuth.ID_TOKEN_INFO, app, ); expect(https.request).not.to.have.been.called; From ac28a5568a820daea7486448a1c49c1d726dca85 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 11 Mar 2021 16:13:57 -0500 Subject: [PATCH 7/7] Remove all Auth dependencies from the token verifier --- src/auth/auth.ts | 6 ++- src/utils/token-verifier.ts | 37 +++++++------- test/unit/index.spec.ts | 2 +- .../{auth => utils}/token-verifier.spec.ts | 50 +++++++++---------- 4 files changed, 48 insertions(+), 47 deletions(-) rename test/unit/{auth => utils}/token-verifier.spec.ts (95%) diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 71738fa0b0..9e79b302e6 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -116,8 +116,9 @@ export class BaseAuth implements BaseAuthI */ public verifyIdToken(idToken: string, checkRevoked = false): Promise { const isEmulator = useEmulator(); - return this.idTokenVerifier.verifyJWT(idToken, isEmulator) + return this.idTokenVerifier.verifyJWT(idToken, isEmulator) .then((decodedIdToken: DecodedIdToken) => { + decodedIdToken.uid = decodedIdToken.sub; // Whether to check if the token was revoked. if (checkRevoked || isEmulator) { return this.verifyDecodedJWTNotRevoked( @@ -519,8 +520,9 @@ export class BaseAuth implements BaseAuthI public verifySessionCookie( sessionCookie: string, checkRevoked = false): Promise { const isEmulator = useEmulator(); - return this.sessionCookieVerifier.verifyJWT(sessionCookie, isEmulator) + return this.sessionCookieVerifier.verifyJWT(sessionCookie, isEmulator) .then((decodedIdToken: DecodedIdToken) => { + decodedIdToken.uid = decodedIdToken.sub; // Whether to check if the token was revoked. if (checkRevoked || isEmulator) { return this.verifyDecodedJWTNotRevoked( diff --git a/src/utils/token-verifier.ts b/src/utils/token-verifier.ts index d2b9268180..b028a0460c 100644 --- a/src/utils/token-verifier.ts +++ b/src/utils/token-verifier.ts @@ -20,9 +20,6 @@ import * as jwt from 'jsonwebtoken'; import { HttpClient, HttpRequestConfig, HttpError } from './api-request'; import { FirebaseApp } from '../firebase-app'; import { ErrorCodeConfig, ErrorInfo, PrefixedFirebaseError } from './error'; -import { auth } from '../auth/index'; - -import DecodedIdToken = auth.DecodedIdToken; // Audience to use for Firebase Auth Custom tokens const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit'; @@ -86,14 +83,15 @@ export class FirebaseTokenVerifier { } /** - * Verifies the format and signature of a Firebase Auth JWT token. + * Verifies the format and signature of a Firebase JWT token. * - * @param {string} jwtToken The Firebase Auth JWT token to verify. - * @param {boolean=} isEmulator Whether to accept Auth Emulator tokens. - * @return {Promise} A promise fulfilled with the decoded claims of the Firebase Auth ID + * @template T The returned type of the decoded token. + * @param {string} jwtToken The Firebase JWT token to verify. + * @param {boolean=} isEmulator Whether to accept Emulator tokens. + * @return {Promise} A promise fulfilled with the decoded claims of the Firebase Auth ID * token. */ - public verifyJWT(jwtToken: string, isEmulator = false): Promise { + public verifyJWT(jwtToken: string, isEmulator = false): Promise { if (!validator.isString(jwtToken)) { throw new this.tokenInfo.errorType( this.tokenInfo.errorCodeConfig.invalidArg, @@ -103,15 +101,15 @@ export class FirebaseTokenVerifier { return util.findProjectId(this.app) .then((projectId) => { - return this.verifyJWTWithProjectId(jwtToken, projectId, isEmulator); + return this.verifyJWTWithProjectId(jwtToken, projectId, isEmulator); }); } - private verifyJWTWithProjectId( + private verifyJWTWithProjectId( jwtToken: string, projectId: string | null, isEmulator: boolean - ): Promise { + ): Promise { if (!validator.isNonEmptyString(projectId)) { throw new this.tokenInfo.errorType( this.tokenInfo.errorCodeConfig.invalidCredential, @@ -176,7 +174,7 @@ export class FirebaseTokenVerifier { if (isEmulator) { // Signature checks skipped for emulator; no need to fetch public keys. - return this.verifyJwtSignatureWithKey(jwtToken, null); + return this.verifyJwtSignatureWithKey(jwtToken, null); } return this.fetchPublicKeys().then((publicKeys) => { @@ -190,7 +188,7 @@ export class FirebaseTokenVerifier { ), ); } else { - return this.verifyJwtSignatureWithKey(jwtToken, publicKeys[header.kid]); + return this.verifyJwtSignatureWithKey(jwtToken, publicKeys[header.kid]); } }); @@ -198,12 +196,15 @@ export class FirebaseTokenVerifier { /** * Verifies the JWT signature using the provided public key. + * + * @template T The returned type of the decoded token. * @param {string} jwtToken The JWT token to verify. * @param {string} publicKey The public key certificate. - * @return {Promise} A promise that resolves with the decoded JWT claims on successful + * @return {Promise} A promise that resolves with the decoded JWT claims on successful * verification. */ - private verifyJwtSignatureWithKey(jwtToken: string, publicKey: string | null): Promise { + private verifyJwtSignatureWithKey(jwtToken: string, + publicKey: string | null): Promise { const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` + `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`; return new Promise((resolve, reject) => { @@ -216,7 +217,7 @@ export class FirebaseTokenVerifier { if (error) { if (error.name === 'TokenExpiredError') { const errorMessage = `${this.tokenInfo.jwtName} has expired. Get a fresh ${this.tokenInfo.shortName}` + - ` from your client app and try again (auth/${this.tokenInfo.errorCodeConfig.expiredError.code}).` + + ` from your client app and try again (${this.tokenInfo.errorCodeConfig.expiredError.code}).` + verifyJwtTokenDocsMessage; return reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeConfig.expiredError, errorMessage)); } else if (error.name === 'JsonWebTokenError') { @@ -225,9 +226,7 @@ export class FirebaseTokenVerifier { } return reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeConfig.invalidArg, error.message)); } else { - const decodedIdToken = (decodedToken as DecodedIdToken); - decodedIdToken.uid = decodedIdToken.sub; - resolve(decodedIdToken); + resolve(decodedToken as T); } }); }); diff --git a/test/unit/index.spec.ts b/test/unit/index.spec.ts index efbe059e96..ab26c1e4e4 100644 --- a/test/unit/index.spec.ts +++ b/test/unit/index.spec.ts @@ -25,12 +25,12 @@ import './utils/index.spec'; import './utils/error.spec'; import './utils/validator.spec'; import './utils/api-request.spec'; +import './utils/token-verifier.spec'; // Auth import './auth/auth.spec'; import './auth/user-record.spec'; import './auth/token-generator.spec'; -import './auth/token-verifier.spec'; import './auth/auth-api-request.spec'; import './auth/user-import-builder.spec'; import './auth/action-code-settings-builder.spec'; diff --git a/test/unit/auth/token-verifier.spec.ts b/test/unit/utils/token-verifier.spec.ts similarity index 95% rename from test/unit/auth/token-verifier.spec.ts rename to test/unit/utils/token-verifier.spec.ts index 9494b9736a..ae58e26c2f 100644 --- a/test/unit/auth/token-verifier.spec.ts +++ b/test/unit/utils/token-verifier.spec.ts @@ -30,7 +30,9 @@ import LegacyFirebaseTokenGenerator = require('firebase-token-generator'); import * as mocks from '../../resources/mocks'; import { FirebaseTokenGenerator, ServiceAccountSigner } from '../../../src/auth/token-generator'; import * as verifier from '../../../src/utils/token-verifier'; -import * as verifierAuth from '../../../src/auth/token-verifier'; +import { + ID_TOKEN_ERROR_CODE_CONFIG, ID_TOKEN_INFO, SESSION_COOKIE_INFO +} from '../../../src/auth/token-verifier'; import { ServiceAccountCredential } from '../../../src/credential/credential-internal'; import { FirebaseAuthError } from '../../../src/utils/error'; @@ -115,7 +117,7 @@ function createTokenVerifier( 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', algorithm, 'https://securetoken.google.com/', - verifierAuth.ID_TOKEN_INFO, + ID_TOKEN_INFO, app ); } @@ -160,7 +162,7 @@ describe('FirebaseTokenVerifier', () => { verifyApiName: 'verifyToken()', jwtName: 'Important Token', shortName: 'token', - errorCodeConfig: verifierAuth.ID_TOKEN_ERROR_CODE_CONFIG, + errorCodeConfig: ID_TOKEN_ERROR_CODE_CONFIG, errorType: FirebaseAuthError, }, app, @@ -176,7 +178,7 @@ describe('FirebaseTokenVerifier', () => { invalidCertUrl as any, 'RS256', 'https://www.example.com/issuer/', - verifierAuth.ID_TOKEN_INFO, + ID_TOKEN_INFO, app, ); }).to.throw('The provided public client certificate URL is an invalid URL.'); @@ -191,7 +193,7 @@ describe('FirebaseTokenVerifier', () => { 'https://www.example.com/publicKeys', invalidAlgorithm as any, 'https://www.example.com/issuer/', - verifierAuth.ID_TOKEN_INFO, + ID_TOKEN_INFO, app); }).to.throw('The provided JWT algorithm is an empty string.'); }); @@ -205,7 +207,7 @@ describe('FirebaseTokenVerifier', () => { 'https://www.example.com/publicKeys', 'RS256', invalidIssuer as any, - verifierAuth.ID_TOKEN_INFO, + ID_TOKEN_INFO, app, ); }).to.throw('The provided JWT issuer is an invalid URL.'); @@ -225,7 +227,7 @@ describe('FirebaseTokenVerifier', () => { verifyApiName: invalidVerifyApiName as any, jwtName: 'Important Token', shortName: 'token', - errorCodeConfig: verifierAuth.ID_TOKEN_ERROR_CODE_CONFIG, + errorCodeConfig: ID_TOKEN_ERROR_CODE_CONFIG, errorType: FirebaseAuthError, }, app, @@ -247,7 +249,7 @@ describe('FirebaseTokenVerifier', () => { verifyApiName: 'verifyToken()', jwtName: invalidJwtName as any, shortName: 'token', - errorCodeConfig: verifierAuth.ID_TOKEN_ERROR_CODE_CONFIG, + errorCodeConfig: ID_TOKEN_ERROR_CODE_CONFIG, errorType: FirebaseAuthError, }, app, @@ -269,7 +271,7 @@ describe('FirebaseTokenVerifier', () => { verifyApiName: 'verifyToken()', jwtName: 'Important Token', shortName: invalidShortName as any, - errorCodeConfig: verifierAuth.ID_TOKEN_ERROR_CODE_CONFIG, + errorCodeConfig: ID_TOKEN_ERROR_CODE_CONFIG, errorType: FirebaseAuthError, }, app, @@ -278,9 +280,9 @@ describe('FirebaseTokenVerifier', () => { }); }); - const invalidErrorCodeTypes = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop, '', 'test']; - invalidErrorCodeTypes.forEach((invalidErrorCodeType) => { - it('should throw given an invalid error code config: ' + JSON.stringify(invalidErrorCodeType), () => { + const invalidErrorTypes = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, undefined, '', 'test']; + invalidErrorTypes.forEach((invalidErrorType) => { + it('should throw given an invalid error type: ' + JSON.stringify(invalidErrorType), () => { expect(() => { new verifier.FirebaseTokenVerifier( 'https://www.example.com/publicKeys', @@ -291,8 +293,8 @@ describe('FirebaseTokenVerifier', () => { verifyApiName: 'verifyToken()', jwtName: 'Important Token', shortName: 'token', - errorCodeConfig: verifierAuth.ID_TOKEN_ERROR_CODE_CONFIG, - errorType: invalidErrorCodeTypes as any, + errorCodeConfig: ID_TOKEN_ERROR_CODE_CONFIG, + errorType: invalidErrorType as any, }, app, ); @@ -339,7 +341,7 @@ describe('FirebaseTokenVerifier', () => { it('should throw with the correct error type and code set in token info', () => { const errorType = FirebaseAuthError; - const errorCodeConfig = verifierAuth.ID_TOKEN_ERROR_CODE_CONFIG; + const errorCodeConfig = ID_TOKEN_ERROR_CODE_CONFIG; const tokenVerifier = new verifier.FirebaseTokenVerifier( 'https://www.example.com/publicKeys', 'RS256', @@ -383,7 +385,7 @@ describe('FirebaseTokenVerifier', () => { 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', 'RS256', 'https://securetoken.google.com/', - verifierAuth.ID_TOKEN_INFO, + ID_TOKEN_INFO, mocks.mockCredentialApp(), ); const mockIdToken = mocks.generateIdToken(); @@ -480,7 +482,7 @@ describe('FirebaseTokenVerifier', () => { // Token should now be invalid return tokenVerifier.verifyJWT(mockIdToken) .should.eventually.be.rejectedWith('Firebase ID token has expired. Get a fresh ID token from your client ' + - 'app and try again (auth/id-token-expired)') + 'app and try again (id-token-expired)') .and.have.property('code', 'auth/id-token-expired'); }); }); @@ -490,7 +492,7 @@ describe('FirebaseTokenVerifier', () => { 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys', 'RS256', 'https://session.firebase.google.com/', - verifierAuth.SESSION_COOKIE_INFO, + SESSION_COOKIE_INFO, app, ); mockedRequests.push(mockFetchPublicKeys('/identitytoolkit/v3/relyingparty/publicKeys')); @@ -508,7 +510,7 @@ describe('FirebaseTokenVerifier', () => { // Cookie should now be invalid return tokenVerifierSessionCookie.verifyJWT(mockSessionCookie) .should.eventually.be.rejectedWith('Firebase session cookie has expired. Get a fresh session cookie from ' + - 'your client app and try again (auth/session-cookie-expired).') + 'your client app and try again (session-cookie-expired).') .and.have.property('code', 'auth/session-cookie-expired'); }); }); @@ -535,7 +537,7 @@ describe('FirebaseTokenVerifier', () => { 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys', 'RS256', 'https://session.firebase.google.com/', - verifierAuth.SESSION_COOKIE_INFO, + SESSION_COOKIE_INFO, app, ); return tokenGenerator.createCustomToken(mocks.uid) @@ -561,7 +563,7 @@ describe('FirebaseTokenVerifier', () => { 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys', 'RS256', 'https://session.firebase.google.com/', - verifierAuth.SESSION_COOKIE_INFO, + SESSION_COOKIE_INFO, app, ); const legacyTokenGenerator = new LegacyFirebaseTokenGenerator('foo'); @@ -590,7 +592,6 @@ describe('FirebaseTokenVerifier', () => { aud: mocks.projectId, iss: 'https://securetoken.google.com/' + mocks.projectId, sub: mocks.uid, - uid: mocks.uid, }); }); @@ -613,7 +614,6 @@ describe('FirebaseTokenVerifier', () => { aud: mocks.projectId, iss: 'https://securetoken.google.com/' + mocks.projectId, sub: mocks.uid, - uid: mocks.uid, }); }); @@ -654,7 +654,7 @@ describe('FirebaseTokenVerifier', () => { 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', 'RS256', 'https://securetoken.google.com/', - verifierAuth.ID_TOKEN_INFO, + ID_TOKEN_INFO, appWithAgent, ); mockedRequests.push(mockFetchPublicKeys()); @@ -677,7 +677,7 @@ describe('FirebaseTokenVerifier', () => { 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', 'RS256', 'https://securetoken.google.com/', - verifierAuth.ID_TOKEN_INFO, + ID_TOKEN_INFO, app, ); expect(https.request).not.to.have.been.called;