From f603e659bfc0a77d988d7fd332e11ae3d72b2ab6 Mon Sep 17 00:00:00 2001 From: Andrii Boiko Date: Tue, 9 Dec 2025 06:38:06 +0100 Subject: [PATCH 1/9] feat: add basic fpnv --- entrypoints.json | 4 + package.json | 8 ++ src/firebase-namespace-api.ts | 3 + src/fpnv/base-fpnv.ts | 23 ++++ src/fpnv/fpnv-namespace.ts | 39 ++++++ src/fpnv/fpnv.ts | 41 ++++++ src/fpnv/index.ts | 74 ++++++++++ src/fpnv/token-verifier.ts | 246 ++++++++++++++++++++++++++++++++++ src/utils/error.ts | 32 +++++ src/utils/jwt.ts | 1 + 10 files changed, 471 insertions(+) create mode 100644 src/fpnv/base-fpnv.ts create mode 100644 src/fpnv/fpnv-namespace.ts create mode 100644 src/fpnv/fpnv.ts create mode 100644 src/fpnv/index.ts create mode 100644 src/fpnv/token-verifier.ts diff --git a/entrypoints.json b/entrypoints.json index caa92a7604..96c3f52e73 100644 --- a/entrypoints.json +++ b/entrypoints.json @@ -16,6 +16,10 @@ "typings": "./lib/auth/index.d.ts", "dist": "./lib/auth/index.js" }, + "firebase-admin/fpnv": { + "typings": "./lib/fpnv/index.d.ts", + "dist": "./lib/fpnv/index.js" + }, "firebase-admin/database": { "typings": "./lib/database/index.d.ts", "dist": "./lib/database/index.js" diff --git a/package.json b/package.json index a17a1e291f..7a33f881be 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,9 @@ "auth": [ "lib/auth" ], + "fpnv": [ + "lib/fpnv" + ], "eventarc": [ "lib/eventarc" ], @@ -134,6 +137,11 @@ "require": "./lib/auth/index.js", "import": "./lib/esm/auth/index.js" }, + "./fpnv": { + "types": "./lib/fpnv/index.d.ts", + "require": "./lib/fpnv/index.js", + "import": "./lib/esm/fpnv/index.js" + }, "./database": { "types": "./lib/database/index.d.ts", "require": "./lib/database/index.js", diff --git a/src/firebase-namespace-api.ts b/src/firebase-namespace-api.ts index 3808876910..e024fa7b75 100644 --- a/src/firebase-namespace-api.ts +++ b/src/firebase-namespace-api.ts @@ -16,6 +16,7 @@ import { appCheck } from './app-check/app-check-namespace'; import { auth } from './auth/auth-namespace'; +import { fpnv } from './fpnv/fpnv-namespace'; import { database } from './database/database-namespace'; import { firestore } from './firestore/firestore-namespace'; import { instanceId } from './instance-id/instance-id-namespace'; @@ -43,6 +44,7 @@ export namespace app { export interface App extends AppCore { appCheck(): appCheck.AppCheck; auth(): auth.Auth; + fpnv(): fpnv.Fpnv; database(url?: string): database.Database; firestore(): firestore.Firestore; installations(): installations.Installations; @@ -81,6 +83,7 @@ export namespace app { export * from './credential/index'; export { appCheck } from './app-check/app-check-namespace'; export { auth } from './auth/auth-namespace'; +export { fpnv } from './fpnv/fpnv-namespace'; export { database } from './database/database-namespace'; export { firestore } from './firestore/firestore-namespace'; export { instanceId } from './instance-id/instance-id-namespace'; diff --git a/src/fpnv/base-fpnv.ts b/src/fpnv/base-fpnv.ts new file mode 100644 index 0000000000..ca4f013b6c --- /dev/null +++ b/src/fpnv/base-fpnv.ts @@ -0,0 +1,23 @@ +import { App } from '../app'; + +import { + FirebasePhoneNumberTokenVerifier, + FpnvToken, + createFPNTVerifier, +} from './token-verifier'; + + +export abstract class BaseFpnv { + protected readonly fPNTVerifier: FirebasePhoneNumberTokenVerifier; + + protected constructor( + app: App, + ) { + this.fPNTVerifier = createFPNTVerifier(app); + } + + + public async verifyToken(idToken: string): Promise { + return await this.fPNTVerifier.verifyJWT(idToken); + } +} diff --git a/src/fpnv/fpnv-namespace.ts b/src/fpnv/fpnv-namespace.ts new file mode 100644 index 0000000000..2c8be84101 --- /dev/null +++ b/src/fpnv/fpnv-namespace.ts @@ -0,0 +1,39 @@ +/*! + * Copyright 2021 Google LLC + * + * 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 { App } from '../app'; + +// Import all public types with aliases, and re-export from the auth namespace. + +import { Fpnv as TFpnv } from './fpnv'; + +import { + BaseFpnv as TBaseFpnv, +} from './base-fpnv'; + +import { + FpnvToken as TFpnvToken, +} from './token-verifier'; + + +export declare function fpnv(app?: App): fpnv.Fpnv; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace fpnv { + export type BaseFpnv = TBaseFpnv; + export type Fpnv = TFpnv; + export type FpnvToken = TFpnvToken; +} diff --git a/src/fpnv/fpnv.ts b/src/fpnv/fpnv.ts new file mode 100644 index 0000000000..c0c8c87f60 --- /dev/null +++ b/src/fpnv/fpnv.ts @@ -0,0 +1,41 @@ +/*! + * @license + * Copyright 2017 Google LLC + * + * 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 { App } from '../app'; +import { BaseFpnv } from './base-fpnv'; + +/** + * Fpnv service bound to the provided app. + */ +export class Fpnv extends BaseFpnv { + private readonly app_: App; + + constructor(app: App) { + super(app); + + this.app_ = app; + } + + /** + * Returns the app associated with this Fpnv instance. + * + * @returns The app associated with this Fpnv instance. + */ + get app(): App { + return this.app_; + } +} diff --git a/src/fpnv/index.ts b/src/fpnv/index.ts new file mode 100644 index 0000000000..8de3872b40 --- /dev/null +++ b/src/fpnv/index.ts @@ -0,0 +1,74 @@ +/*! + * Copyright 2020 Google LLC + * + * 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. + */ + +/** + * Firebase Phone Number Verification. + * + * @packageDocumentation + */ + +import { App, getApp } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import { Fpnv } from './fpnv'; + +/** + * Gets the {@link Fpnv} service for the default app or a + * given app. + * + * `getFirebasePnv()` can be called with no arguments to access the default app's + * {@link Fpnv} service or as `getFirebasePnv(app)` to access the + * {@link Fpnv} service associated with a specific app. + * + * @example + * ```javascript + * // Get the Fpnv service for the default app + * const defaultFpnv = getFirebasePnv(); + * ``` + * + * @example + * ```javascript + * // Get the Fpnv service for a given app + * const otherFpnv = getFirebasePnv(otherApp); + * ``` + * + */ +export function getFirebasePnv(app?: App): Fpnv { + if (typeof app === 'undefined') { + app = getApp(); + } + + const firebaseApp: FirebaseApp = app as FirebaseApp; + return firebaseApp.getOrInitService('fpnv', (app) => new Fpnv(app)); +} + +export { + Fpnv, +} from './fpnv'; + +export { + BaseFpnv, +} from './base-fpnv'; + + +export { + FpnvToken, +} from './token-verifier'; + + +export { + FirebasePnvError, + FpnvErrorCode, +} from '../utils/error'; diff --git a/src/fpnv/token-verifier.ts b/src/fpnv/token-verifier.ts new file mode 100644 index 0000000000..cc3cf27a4e --- /dev/null +++ b/src/fpnv/token-verifier.ts @@ -0,0 +1,246 @@ +import { App } from '../app'; +import { FpnvErrorCode, ErrorInfo, FirebasePnvError } from '../utils/error'; +import * as util from '../utils/index'; +import * as validator from '../utils/validator'; +import { + DecodedToken, decodeJwt, JwtError, JwtErrorCode, + PublicKeySignatureVerifier, ALGORITHM_ES256, SignatureVerifier, +} from '../utils/jwt'; + +export interface FpnvToken { + aud: string; + auth_time: number; + exp: number; + iat: number; + iss: string; + sub: string; + + getPhoneNumber(): string; + + /** + * Other arbitrary claims included in the ID token. + */ + [key: string]: any; +} + +const CLIENT_CERT_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'; + +export const PN_TOKEN_INFO: FirebasePhoneNumberTokenInfo = { + url: 'https://firebase.google.com/docs/phone-number-verification', + verifyApiName: 'verifyToken()', + jwtName: 'Firebase Phone Verification token', + shortName: 'FPNV token', + typ: 'JWT', + expiredErrorCode: FpnvErrorCode.COMMON_ISSUE, +}; + +export interface FirebasePhoneNumberTokenInfo { + /** Documentation URL. */ + url: string; + /** verify API name. */ + verifyApiName: string; + /** The JWT full name. */ + jwtName: string; + /** The JWT short name. */ + shortName: string; + /** JWT Expiration error code. */ + expiredErrorCode: ErrorInfo; + /** The JWT typ" (Type) */ + typ: string; +} + +export class FirebasePhoneNumberTokenVerifier { + + private readonly shortNameArticle: string; + private readonly signatureVerifier: SignatureVerifier; + + constructor( + clientCertUrl: string, + private issuer: string, + private tokenInfo: FirebasePhoneNumberTokenInfo, + private readonly app: App + ) { + + if (!validator.isURL(clientCertUrl)) { + throw new FirebasePnvError( + FpnvErrorCode.COMMON_ISSUE, + 'The provided public client certificate URL is an invalid URL.', + ); + } else if (!validator.isURL(issuer)) { + throw new FirebasePnvError( + FpnvErrorCode.COMMON_ISSUE, + 'The provided JWT issuer is an invalid URL.', + ); + } else if (!validator.isNonNullObject(tokenInfo)) { + throw new FirebasePnvError( + FpnvErrorCode.COMMON_ISSUE, + 'The provided JWT information is not an object or null.', + ); + } else if (!validator.isURL(tokenInfo.url)) { + throw new FirebasePnvError( + FpnvErrorCode.COMMON_ISSUE, + 'The provided JWT verification documentation URL is invalid.', + ); + } else if (!validator.isNonEmptyString(tokenInfo.verifyApiName)) { + throw new FirebasePnvError( + FpnvErrorCode.COMMON_ISSUE, + 'The JWT verify API name must be a non-empty string.', + ); + } else if (!validator.isNonEmptyString(tokenInfo.jwtName)) { + throw new FirebasePnvError( + FpnvErrorCode.COMMON_ISSUE, + 'The JWT public full name must be a non-empty string.', + ); + } else if (!validator.isNonEmptyString(tokenInfo.shortName)) { + throw new FirebasePnvError( + FpnvErrorCode.COMMON_ISSUE, + 'The JWT public short name must be a non-empty string.', + ); + } else if (!validator.isNonNullObject(tokenInfo.expiredErrorCode) || !('code' in tokenInfo.expiredErrorCode)) { + throw new FirebasePnvError( + FpnvErrorCode.COMMON_ISSUE, + 'The JWT expiration error code must be a non-null ErrorInfo object.', + ); + } + this.shortNameArticle = tokenInfo.shortName.charAt(0).match(/[aeiou]/i) ? 'an' : 'a'; + + this.signatureVerifier = + PublicKeySignatureVerifier.withCertificateUrl(clientCertUrl, app.options.httpAgent); + + // For backward compatibility, the project ID is validated in the verification call. + } + + public async verifyJWT(jwtToken: string): Promise { + if (!validator.isString(jwtToken)) { + throw new FirebasePnvError( + FpnvErrorCode.COMMON_ISSUE, + `First argument to ${this.tokenInfo.verifyApiName} must be a ${this.tokenInfo.jwtName} string.`, + ); + } + + const projectId = await this.ensureProjectId(); + const decoded = await this.decodeAndVerify(jwtToken, projectId); + const decodedIdToken = decoded.payload as FpnvToken; + decodedIdToken.getPhoneNumber = () => decodedIdToken.sub; + return decodedIdToken; + } + + private async ensureProjectId(): Promise { + const projectId = await util.findProjectId(this.app); + if (!validator.isNonEmptyString(projectId)) { + throw new FirebasePnvError( + FpnvErrorCode.COMMON_ISSUE, + '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}.`); + } + return projectId; + } + + private async decodeAndVerify( + token: string, + projectId: string, + ): Promise { + const decodedToken = await this.safeDecode(token); + this.verifyContent(decodedToken, projectId); + await this.verifySignature(token); + return decodedToken; + } + + private async safeDecode(jwtToken: string): Promise { + try { + return await decodeJwt(jwtToken); + } catch (err) { + if (err.code === JwtErrorCode.INVALID_ARGUMENT) { + const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` + + `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`; + const errorMessage = `Decoding ${this.tokenInfo.jwtName} failed. Make sure you passed ` + + `the entire string JWT which represents ${this.shortNameArticle} ` + + `${this.tokenInfo.shortName}.` + verifyJwtTokenDocsMessage; + throw new FirebasePnvError(FpnvErrorCode.COMMON_ISSUE, + errorMessage); + } + throw new FirebasePnvError(FpnvErrorCode.COMMON_ISSUE, err.message); + } + } + + + private verifyContent( + fullDecodedToken: DecodedToken, + projectId: string | null, + ): void { + const header = fullDecodedToken && fullDecodedToken.header; + const payload = fullDecodedToken && fullDecodedToken.payload; + + const projectIdMatchMessage = ` Make sure the ${this.tokenInfo.shortName} comes from the same ` + + 'Firebase project as the service account used to authenticate this SDK.'; + const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` + + `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`; + + let errorMessage: string | undefined; + + // JWT Header + if (typeof header.kid === 'undefined') { + errorMessage = `${this.tokenInfo.jwtName} has no "kid" claim.`; + errorMessage += verifyJwtTokenDocsMessage; + } else if (header.alg !== ALGORITHM_ES256) { + errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected "` + ALGORITHM_ES256 + '" but got ' + + '"' + header.alg + '".' + verifyJwtTokenDocsMessage; + } else if (header.typ !== this.tokenInfo.typ) { + errorMessage = `${this.tokenInfo.jwtName} has incorrect typ. Expected "${this.tokenInfo.typ}" but got ` + + '"' + header.typ + '".' + verifyJwtTokenDocsMessage; + } + // FPNV Token + else if (!((payload.aud as string[]).some(item => item === this.issuer + projectId))) { + errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` + + this.issuer + projectId + '" to be one of "' + payload.aud + '".' + projectIdMatchMessage + + verifyJwtTokenDocsMessage; + } else if (typeof payload.sub !== 'string') { + errorMessage = `${this.tokenInfo.jwtName} has no "sub" (subject) claim.` + verifyJwtTokenDocsMessage; + } else if (payload.sub === '') { + errorMessage = `${this.tokenInfo.jwtName} has an empty "sub" (subject) claim.` + + verifyJwtTokenDocsMessage; + } + + if (errorMessage) { + throw new FirebasePnvError(FpnvErrorCode.COMMON_ISSUE, errorMessage); + } + } + + private async verifySignature(jwtToken: string): Promise { + try { + return await this.signatureVerifier.verify(jwtToken); + } catch (error) { + throw this.mapJwtErrorToAuthError(error); + } + } + + private mapJwtErrorToAuthError(error: JwtError): Error { + const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` + + `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`; + if (error.code === JwtErrorCode.TOKEN_EXPIRED) { + 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 new FirebasePnvError(this.tokenInfo.expiredErrorCode, errorMessage); + } else if (error.code === JwtErrorCode.INVALID_SIGNATURE) { + const errorMessage = `${this.tokenInfo.jwtName} has invalid signature.` + verifyJwtTokenDocsMessage; + return new FirebasePnvError(FpnvErrorCode.COMMON_ISSUE, errorMessage); + } else if (error.code === JwtErrorCode.NO_MATCHING_KID) { + const errorMessage = `${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.'; + return new FirebasePnvError(FpnvErrorCode.COMMON_ISSUE, errorMessage); + } + return new FirebasePnvError(FpnvErrorCode.COMMON_ISSUE, error.message); + } + +} + +export function createFPNTVerifier(app: App): FirebasePhoneNumberTokenVerifier { + return new FirebasePhoneNumberTokenVerifier( + CLIENT_CERT_URL, + 'https://fpnv.googleapis.com/projects/', + PN_TOKEN_INFO, + app + ); +} diff --git a/src/utils/error.ts b/src/utils/error.ts index db013b8967..1887b6c799 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -1145,3 +1145,35 @@ const TOPIC_MGT_SERVER_TO_CLIENT_CODE: ServerToClientCode = { INTERNAL: 'INTERNAL_ERROR', UNKNOWN: 'UNKNOWN_ERROR', }; + +/** + * Firebase Phone Number Verification error code structure. This extends PrefixedFirebaseError. + */ +export class FirebasePnvError extends PrefixedFirebaseError { + /** + * @param info - The error code info. + * @param message - The error message. This will override the default message if provided. + * @constructor + * @internal + */ + constructor(info: ErrorInfo, message?: string) { + // Override default message if custom message provided. + super('fpnv', info.code, message || info.message); + + /* tslint:disable:max-line-length */ + // Set the prototype explicitly. See the following link for more details: + // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work + /* tslint:enable:max-line-length */ + (this as any).__proto__ = FirebasePnvError.prototype; + } +} + +export class FpnvErrorCode { + static INVALID_TOKEN = 'invalid_token'; + static EXPIRED_TOKEN = 'expired_token'; + static COMMON_ISSUE = { + code: "code", + message: "message" + }; + // TODO: need to update codes properly +} \ No newline at end of file diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts index 9a66494482..f3e4ba0744 100644 --- a/src/utils/jwt.ts +++ b/src/utils/jwt.ts @@ -21,6 +21,7 @@ import { HttpClient, HttpRequestConfig, RequestResponseError } from '../utils/ap import { Agent } from 'http'; export const ALGORITHM_RS256: jwt.Algorithm = 'RS256' as const; +export const ALGORITHM_ES256: jwt.Algorithm = 'ES256' as const; // `jsonwebtoken` converts errors from the `getKey` callback to its own `JsonWebTokenError` type // and prefixes the error message with the following. Use the prefix to identify errors thrown From 23868e8f6e3a13f88f193a58b22a7cc09aa9c9b9 Mon Sep 17 00:00:00 2001 From: Andrii Boiko Date: Wed, 10 Dec 2025 15:26:37 +0100 Subject: [PATCH 2/9] chore: resolve comments --- src/firebase-namespace-api.ts | 3 - src/fpnv/base-fpnv.ts | 23 ------ src/fpnv/{fpnv-namespace.ts => fpnv-api.ts} | 42 +++++------ src/fpnv/fpnv.ts | 26 +++++-- src/fpnv/index.ts | 22 +----- src/fpnv/token-verifier.ts | 81 +++++++++++---------- src/utils/error.ts | 22 ++++-- 7 files changed, 99 insertions(+), 120 deletions(-) delete mode 100644 src/fpnv/base-fpnv.ts rename src/fpnv/{fpnv-namespace.ts => fpnv-api.ts} (51%) diff --git a/src/firebase-namespace-api.ts b/src/firebase-namespace-api.ts index e024fa7b75..3808876910 100644 --- a/src/firebase-namespace-api.ts +++ b/src/firebase-namespace-api.ts @@ -16,7 +16,6 @@ import { appCheck } from './app-check/app-check-namespace'; import { auth } from './auth/auth-namespace'; -import { fpnv } from './fpnv/fpnv-namespace'; import { database } from './database/database-namespace'; import { firestore } from './firestore/firestore-namespace'; import { instanceId } from './instance-id/instance-id-namespace'; @@ -44,7 +43,6 @@ export namespace app { export interface App extends AppCore { appCheck(): appCheck.AppCheck; auth(): auth.Auth; - fpnv(): fpnv.Fpnv; database(url?: string): database.Database; firestore(): firestore.Firestore; installations(): installations.Installations; @@ -83,7 +81,6 @@ export namespace app { export * from './credential/index'; export { appCheck } from './app-check/app-check-namespace'; export { auth } from './auth/auth-namespace'; -export { fpnv } from './fpnv/fpnv-namespace'; export { database } from './database/database-namespace'; export { firestore } from './firestore/firestore-namespace'; export { instanceId } from './instance-id/instance-id-namespace'; diff --git a/src/fpnv/base-fpnv.ts b/src/fpnv/base-fpnv.ts deleted file mode 100644 index ca4f013b6c..0000000000 --- a/src/fpnv/base-fpnv.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { App } from '../app'; - -import { - FirebasePhoneNumberTokenVerifier, - FpnvToken, - createFPNTVerifier, -} from './token-verifier'; - - -export abstract class BaseFpnv { - protected readonly fPNTVerifier: FirebasePhoneNumberTokenVerifier; - - protected constructor( - app: App, - ) { - this.fPNTVerifier = createFPNTVerifier(app); - } - - - public async verifyToken(idToken: string): Promise { - return await this.fPNTVerifier.verifyJWT(idToken); - } -} diff --git a/src/fpnv/fpnv-namespace.ts b/src/fpnv/fpnv-api.ts similarity index 51% rename from src/fpnv/fpnv-namespace.ts rename to src/fpnv/fpnv-api.ts index 2c8be84101..ca3e46d22d 100644 --- a/src/fpnv/fpnv-namespace.ts +++ b/src/fpnv/fpnv-api.ts @@ -1,5 +1,6 @@ /*! - * Copyright 2021 Google LLC + * @license + * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,26 +15,25 @@ * limitations under the License. */ -import { App } from '../app'; - -// Import all public types with aliases, and re-export from the auth namespace. - -import { Fpnv as TFpnv } from './fpnv'; - -import { - BaseFpnv as TBaseFpnv, -} from './base-fpnv'; - -import { - FpnvToken as TFpnvToken, -} from './token-verifier'; +/** + * Interface representing a Fpnv token. + */ +export interface FpnvToken { + aud: string; + auth_time: number; + exp: number; + iat: number; + iss: string; + sub: string; + + getPhoneNumber(): string; + + /** + * Other arbitrary claims included in the ID token. + */ + [key: string]: any; +} -export declare function fpnv(app?: App): fpnv.Fpnv; +export {FpnvErrorCode, FirebasePnvError, ErrorInfo} from '../utils/error'; -/* eslint-disable @typescript-eslint/no-namespace */ -export namespace fpnv { - export type BaseFpnv = TBaseFpnv; - export type Fpnv = TFpnv; - export type FpnvToken = TFpnvToken; -} diff --git a/src/fpnv/fpnv.ts b/src/fpnv/fpnv.ts index c0c8c87f60..dc64a4cd6e 100644 --- a/src/fpnv/fpnv.ts +++ b/src/fpnv/fpnv.ts @@ -1,6 +1,6 @@ /*! * @license - * Copyright 2017 Google LLC + * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,26 +16,36 @@ */ import { App } from '../app'; -import { BaseFpnv } from './base-fpnv'; +import { FpnvToken } from './fpnv-api'; +import { + FirebasePhoneNumberTokenVerifier, + createFPNTVerifier, +} from './token-verifier'; /** * Fpnv service bound to the provided app. */ -export class Fpnv extends BaseFpnv { +export class Fpnv { private readonly app_: App; + protected readonly fpnvVerifier: FirebasePhoneNumberTokenVerifier; + constructor(app: App) { - super(app); this.app_ = app; + this.fpnvVerifier = createFPNTVerifier(app); } /** - * Returns the app associated with this Fpnv instance. - * - * @returns The app associated with this Fpnv instance. - */ + * Returns the app associated with this Auth instance. + * + * @returns The app associated with this Auth instance. + */ get app(): App { return this.app_; } + + public async verifyToken(idToken: string): Promise { + return await this.fpnvVerifier.verifyJWT(idToken); + } } diff --git a/src/fpnv/index.ts b/src/fpnv/index.ts index 8de3872b40..a295136b6b 100644 --- a/src/fpnv/index.ts +++ b/src/fpnv/index.ts @@ -1,5 +1,6 @@ /*! - * Copyright 2020 Google LLC + * @license + * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,22 +54,3 @@ export function getFirebasePnv(app?: App): Fpnv { const firebaseApp: FirebaseApp = app as FirebaseApp; return firebaseApp.getOrInitService('fpnv', (app) => new Fpnv(app)); } - -export { - Fpnv, -} from './fpnv'; - -export { - BaseFpnv, -} from './base-fpnv'; - - -export { - FpnvToken, -} from './token-verifier'; - - -export { - FirebasePnvError, - FpnvErrorCode, -} from '../utils/error'; diff --git a/src/fpnv/token-verifier.ts b/src/fpnv/token-verifier.ts index cc3cf27a4e..ae3c1bf357 100644 --- a/src/fpnv/token-verifier.ts +++ b/src/fpnv/token-verifier.ts @@ -1,5 +1,23 @@ +/*! + * @license + * Copyright 2025 Google LLC + * + * 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 { App } from '../app'; -import { FpnvErrorCode, ErrorInfo, FirebasePnvError } from '../utils/error'; +import { FpnvErrorCode, FirebasePnvError, ErrorInfo } from '../utils/error'; +import {FirebasePhoneNumberTokenInfo, FpnvToken} from './fpnv-api'; import * as util from '../utils/index'; import * as validator from '../utils/validator'; import { @@ -7,34 +25,19 @@ import { PublicKeySignatureVerifier, ALGORITHM_ES256, SignatureVerifier, } from '../utils/jwt'; -export interface FpnvToken { - aud: string; - auth_time: number; - exp: number; - iat: number; - iss: string; - sub: string; - - getPhoneNumber(): string; - - /** - * Other arbitrary claims included in the ID token. - */ - [key: string]: any; -} - const CLIENT_CERT_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'; -export const PN_TOKEN_INFO: FirebasePhoneNumberTokenInfo = { + +const PN_TOKEN_INFO: FirebasePhoneNumberTokenInfo = { url: 'https://firebase.google.com/docs/phone-number-verification', verifyApiName: 'verifyToken()', jwtName: 'Firebase Phone Verification token', shortName: 'FPNV token', typ: 'JWT', - expiredErrorCode: FpnvErrorCode.COMMON_ISSUE, + expiredErrorCode: FpnvErrorCode.EXPIRED_TOKEN, }; -export interface FirebasePhoneNumberTokenInfo { +interface FirebasePhoneNumberTokenInfo { /** Documentation URL. */ url: string; /** verify API name. */ @@ -63,42 +66,42 @@ export class FirebasePhoneNumberTokenVerifier { if (!validator.isURL(clientCertUrl)) { throw new FirebasePnvError( - FpnvErrorCode.COMMON_ISSUE, + FpnvErrorCode.INVALID_ARGUMENT, 'The provided public client certificate URL is an invalid URL.', ); } else if (!validator.isURL(issuer)) { throw new FirebasePnvError( - FpnvErrorCode.COMMON_ISSUE, + FpnvErrorCode.INVALID_ARGUMENT, 'The provided JWT issuer is an invalid URL.', ); } else if (!validator.isNonNullObject(tokenInfo)) { throw new FirebasePnvError( - FpnvErrorCode.COMMON_ISSUE, + FpnvErrorCode.INVALID_ARGUMENT, 'The provided JWT information is not an object or null.', ); } else if (!validator.isURL(tokenInfo.url)) { throw new FirebasePnvError( - FpnvErrorCode.COMMON_ISSUE, + FpnvErrorCode.INVALID_ARGUMENT, 'The provided JWT verification documentation URL is invalid.', ); } else if (!validator.isNonEmptyString(tokenInfo.verifyApiName)) { throw new FirebasePnvError( - FpnvErrorCode.COMMON_ISSUE, + FpnvErrorCode.INVALID_ARGUMENT, 'The JWT verify API name must be a non-empty string.', ); } else if (!validator.isNonEmptyString(tokenInfo.jwtName)) { throw new FirebasePnvError( - FpnvErrorCode.COMMON_ISSUE, + FpnvErrorCode.INVALID_ARGUMENT, 'The JWT public full name must be a non-empty string.', ); } else if (!validator.isNonEmptyString(tokenInfo.shortName)) { throw new FirebasePnvError( - FpnvErrorCode.COMMON_ISSUE, + FpnvErrorCode.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 FirebasePnvError( - FpnvErrorCode.COMMON_ISSUE, + FpnvErrorCode.INVALID_ARGUMENT, 'The JWT expiration error code must be a non-null ErrorInfo object.', ); } @@ -113,7 +116,7 @@ export class FirebasePhoneNumberTokenVerifier { public async verifyJWT(jwtToken: string): Promise { if (!validator.isString(jwtToken)) { throw new FirebasePnvError( - FpnvErrorCode.COMMON_ISSUE, + FpnvErrorCode.PROJECT_NOT_FOUND, `First argument to ${this.tokenInfo.verifyApiName} must be a ${this.tokenInfo.jwtName} string.`, ); } @@ -129,7 +132,7 @@ export class FirebasePhoneNumberTokenVerifier { const projectId = await util.findProjectId(this.app); if (!validator.isNonEmptyString(projectId)) { throw new FirebasePnvError( - FpnvErrorCode.COMMON_ISSUE, + FpnvErrorCode.PROJECT_NOT_FOUND, '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}.`); } @@ -156,10 +159,10 @@ export class FirebasePhoneNumberTokenVerifier { const errorMessage = `Decoding ${this.tokenInfo.jwtName} failed. Make sure you passed ` + `the entire string JWT which represents ${this.shortNameArticle} ` + `${this.tokenInfo.shortName}.` + verifyJwtTokenDocsMessage; - throw new FirebasePnvError(FpnvErrorCode.COMMON_ISSUE, + throw new FirebasePnvError(FpnvErrorCode.INVALID_ARGUMENT, errorMessage); } - throw new FirebasePnvError(FpnvErrorCode.COMMON_ISSUE, err.message); + throw new FirebasePnvError(FpnvErrorCode.INVALID_ARGUMENT, err.message); } } @@ -183,14 +186,14 @@ export class FirebasePhoneNumberTokenVerifier { errorMessage = `${this.tokenInfo.jwtName} has no "kid" claim.`; errorMessage += verifyJwtTokenDocsMessage; } else if (header.alg !== ALGORITHM_ES256) { - errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected "` + ALGORITHM_ES256 + '" but got ' + - '"' + header.alg + '".' + verifyJwtTokenDocsMessage; + errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected ` + + `"${ALGORITHM_ES256}" but got "${header.alg}". ${verifyJwtTokenDocsMessage}`; } else if (header.typ !== this.tokenInfo.typ) { errorMessage = `${this.tokenInfo.jwtName} has incorrect typ. Expected "${this.tokenInfo.typ}" but got ` + '"' + header.typ + '".' + verifyJwtTokenDocsMessage; } // FPNV Token - else if (!((payload.aud as string[]).some(item => item === this.issuer + projectId))) { + else if (!((Array.isArray(payload.aud) ? payload.aud : []).some(item => item === this.issuer + projectId))) { errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` + this.issuer + projectId + '" to be one of "' + payload.aud + '".' + projectIdMatchMessage + verifyJwtTokenDocsMessage; @@ -202,7 +205,7 @@ export class FirebasePhoneNumberTokenVerifier { } if (errorMessage) { - throw new FirebasePnvError(FpnvErrorCode.COMMON_ISSUE, errorMessage); + throw new FirebasePnvError(FpnvErrorCode.INVALID_ARGUMENT, errorMessage); } } @@ -224,14 +227,14 @@ export class FirebasePhoneNumberTokenVerifier { return new FirebasePnvError(this.tokenInfo.expiredErrorCode, errorMessage); } else if (error.code === JwtErrorCode.INVALID_SIGNATURE) { const errorMessage = `${this.tokenInfo.jwtName} has invalid signature.` + verifyJwtTokenDocsMessage; - return new FirebasePnvError(FpnvErrorCode.COMMON_ISSUE, errorMessage); + return new FirebasePnvError(FpnvErrorCode.INVALID_ARGUMENT, errorMessage); } else if (error.code === JwtErrorCode.NO_MATCHING_KID) { const errorMessage = `${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.'; - return new FirebasePnvError(FpnvErrorCode.COMMON_ISSUE, errorMessage); + return new FirebasePnvError(FpnvErrorCode.INVALID_ARGUMENT, errorMessage); } - return new FirebasePnvError(FpnvErrorCode.COMMON_ISSUE, error.message); + return new FirebasePnvError(FpnvErrorCode.INVALID_ARGUMENT, error.message); } } diff --git a/src/utils/error.ts b/src/utils/error.ts index 1887b6c799..7a25d9b7f8 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -1169,11 +1169,21 @@ export class FirebasePnvError extends PrefixedFirebaseError { } export class FpnvErrorCode { - static INVALID_TOKEN = 'invalid_token'; - static EXPIRED_TOKEN = 'expired_token'; - static COMMON_ISSUE = { - code: "code", - message: "message" + static readonly INVALID_ARGUMENT: ErrorInfo = { + code: 'invalid-argument', + message: 'Invalid argument provided to the FPNV method.', + }; + static readonly INVALID_TOKEN: ErrorInfo = { + code: 'invalid-token', + message: 'The provided phone number verification token is invalid.', }; - // TODO: need to update codes properly + static readonly EXPIRED_TOKEN: ErrorInfo = { + code: 'expired-token', + message: 'The provided phone number verification token has expired.', + }; + static readonly PROJECT_NOT_FOUND: ErrorInfo = { + code: 'project-not-found', + message: 'No Firebase project was found for the provided credential.', + }; + } \ No newline at end of file From 7f71eedc76d88442405ea4dbcb9e10f4aca5ad7b Mon Sep 17 00:00:00 2001 From: Andrii Boiko Date: Wed, 10 Dec 2025 15:32:32 +0100 Subject: [PATCH 3/9] fix: remove unused interface --- src/fpnv/token-verifier.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fpnv/token-verifier.ts b/src/fpnv/token-verifier.ts index ae3c1bf357..fbe7ca7171 100644 --- a/src/fpnv/token-verifier.ts +++ b/src/fpnv/token-verifier.ts @@ -17,7 +17,7 @@ import { App } from '../app'; import { FpnvErrorCode, FirebasePnvError, ErrorInfo } from '../utils/error'; -import {FirebasePhoneNumberTokenInfo, FpnvToken} from './fpnv-api'; +import { FpnvToken} from './fpnv-api'; import * as util from '../utils/index'; import * as validator from '../utils/validator'; import { From 8413368bba47be484592f5ecc3a90ab0b946f55a Mon Sep 17 00:00:00 2001 From: Andrii Boiko Date: Wed, 10 Dec 2025 16:28:30 +0100 Subject: [PATCH 4/9] chore: update export --- src/fpnv/index.ts | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/fpnv/index.ts b/src/fpnv/index.ts index a295136b6b..96682f1543 100644 --- a/src/fpnv/index.ts +++ b/src/fpnv/index.ts @@ -1,3 +1,9 @@ +/** + * Firebase Phone Number Verification. + * + * @packageDocumentation + */ + /*! * @license * Copyright 2025 Google LLC @@ -15,16 +21,21 @@ * limitations under the License. */ -/** - * Firebase Phone Number Verification. - * - * @packageDocumentation - */ - import { App, getApp } from '../app'; import { FirebaseApp } from '../app/firebase-app'; import { Fpnv } from './fpnv'; +export { + Fpnv +} from './fpnv'; + +export { + FpnvToken, + FirebasePnvError, + FpnvErrorCode, + ErrorInfo +} from './fpnv-api' + /** * Gets the {@link Fpnv} service for the default app or a * given app. From e90c2bd13f25cb36284b97c78fc4213a7e365ed9 Mon Sep 17 00:00:00 2001 From: Andrii Boiko Date: Wed, 10 Dec 2025 16:30:22 +0100 Subject: [PATCH 5/9] chore: linting --- src/fpnv/fpnv-api.ts | 2 +- src/fpnv/fpnv.ts | 2 +- src/fpnv/token-verifier.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/fpnv/fpnv-api.ts b/src/fpnv/fpnv-api.ts index ca3e46d22d..df15661fd0 100644 --- a/src/fpnv/fpnv-api.ts +++ b/src/fpnv/fpnv-api.ts @@ -35,5 +35,5 @@ export interface FpnvToken { [key: string]: any; } -export {FpnvErrorCode, FirebasePnvError, ErrorInfo} from '../utils/error'; +export { FpnvErrorCode, FirebasePnvError, ErrorInfo } from '../utils/error'; diff --git a/src/fpnv/fpnv.ts b/src/fpnv/fpnv.ts index dc64a4cd6e..9596418f25 100644 --- a/src/fpnv/fpnv.ts +++ b/src/fpnv/fpnv.ts @@ -28,7 +28,7 @@ import { export class Fpnv { private readonly app_: App; - protected readonly fpnvVerifier: FirebasePhoneNumberTokenVerifier; + protected readonly fpnvVerifier: FirebasePhoneNumberTokenVerifier; constructor(app: App) { diff --git a/src/fpnv/token-verifier.ts b/src/fpnv/token-verifier.ts index fbe7ca7171..f56cee69e1 100644 --- a/src/fpnv/token-verifier.ts +++ b/src/fpnv/token-verifier.ts @@ -17,7 +17,7 @@ import { App } from '../app'; import { FpnvErrorCode, FirebasePnvError, ErrorInfo } from '../utils/error'; -import { FpnvToken} from './fpnv-api'; +import { FpnvToken } from './fpnv-api'; import * as util from '../utils/index'; import * as validator from '../utils/validator'; import { From 2a759df08eff51f313f161869f0feafb0ab5d1e7 Mon Sep 17 00:00:00 2001 From: Andrii Boiko Date: Wed, 10 Dec 2025 16:33:54 +0100 Subject: [PATCH 6/9] chore: update export --- src/fpnv/fpnv-api.ts | 2 +- src/fpnv/index.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/fpnv/fpnv-api.ts b/src/fpnv/fpnv-api.ts index df15661fd0..0cea0e98ac 100644 --- a/src/fpnv/fpnv-api.ts +++ b/src/fpnv/fpnv-api.ts @@ -35,5 +35,5 @@ export interface FpnvToken { [key: string]: any; } -export { FpnvErrorCode, FirebasePnvError, ErrorInfo } from '../utils/error'; +export { FpnvErrorCode, FirebasePnvError } from '../utils/error'; diff --git a/src/fpnv/index.ts b/src/fpnv/index.ts index 96682f1543..1d296412c9 100644 --- a/src/fpnv/index.ts +++ b/src/fpnv/index.ts @@ -33,7 +33,6 @@ export { FpnvToken, FirebasePnvError, FpnvErrorCode, - ErrorInfo } from './fpnv-api' /** From fabcf80af71651861b4a79d2a5ec1995c26557af Mon Sep 17 00:00:00 2001 From: Andrii Boiko Date: Wed, 10 Dec 2025 16:35:01 +0100 Subject: [PATCH 7/9] feat: add apidocs --- etc/firebase-admin.fpnv.api.md | 64 ++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 etc/firebase-admin.fpnv.api.md diff --git a/etc/firebase-admin.fpnv.api.md b/etc/firebase-admin.fpnv.api.md new file mode 100644 index 0000000000..2b32fa3d91 --- /dev/null +++ b/etc/firebase-admin.fpnv.api.md @@ -0,0 +1,64 @@ +## API Report File for "firebase-admin.fpnv" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Agent } from 'http'; + +// Warning: (ae-forgotten-export) The symbol "PrefixedFirebaseError" needs to be exported by the entry point index.d.ts +// +// @public +export class FirebasePnvError extends PrefixedFirebaseError { +} + +// @public +export class Fpnv { + // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts + constructor(app: App); + get app(): App; + // Warning: (ae-forgotten-export) The symbol "FirebasePhoneNumberTokenVerifier" needs to be exported by the entry point index.d.ts + // + // (undocumented) + protected readonly fpnvVerifier: FirebasePhoneNumberTokenVerifier; + // (undocumented) + verifyToken(idToken: string): Promise; +} + +// @public (undocumented) +export class FpnvErrorCode { + // (undocumented) + static readonly EXPIRED_TOKEN: ErrorInfo; + // Warning: (ae-forgotten-export) The symbol "ErrorInfo" needs to be exported by the entry point index.d.ts + // + // (undocumented) + static readonly INVALID_ARGUMENT: ErrorInfo; + // (undocumented) + static readonly INVALID_TOKEN: ErrorInfo; + // (undocumented) + static readonly PROJECT_NOT_FOUND: ErrorInfo; +} + +// @public +export interface FpnvToken { + [key: string]: any; + // (undocumented) + aud: string; + // (undocumented) + auth_time: number; + // (undocumented) + exp: number; + // (undocumented) + getPhoneNumber(): string; + // (undocumented) + iat: number; + // (undocumented) + iss: string; + // (undocumented) + sub: string; +} + +// @public +export function getFirebasePnv(app?: App): Fpnv; + +``` From 38e5a17f6cd25bc2252f14959bf244f65ae9d7e0 Mon Sep 17 00:00:00 2001 From: Andrii Boiko Date: Thu, 11 Dec 2025 14:12:32 +0100 Subject: [PATCH 8/9] chore: resolve commnets --- src/fpnv/fpnv-api-client-internal.ts | 71 +++++++++++++++++++ src/fpnv/fpnv-api.ts | 51 ++++++++++++-- src/fpnv/fpnv.ts | 35 ++++++---- src/fpnv/token-verifier.ts | 100 ++++++++------------------- src/utils/error.ts | 42 ----------- 5 files changed, 166 insertions(+), 133 deletions(-) create mode 100644 src/fpnv/fpnv-api-client-internal.ts diff --git a/src/fpnv/fpnv-api-client-internal.ts b/src/fpnv/fpnv-api-client-internal.ts new file mode 100644 index 0000000000..82fceffb24 --- /dev/null +++ b/src/fpnv/fpnv-api-client-internal.ts @@ -0,0 +1,71 @@ +/*! + * @license + * Copyright 2025 Google LLC + * + * 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 { PrefixedFirebaseError } from '../utils/error'; + +export interface FirebasePhoneNumberTokenInfo { + /** Documentation URL. */ + url: string; + /** verify API name. */ + verifyApiName: string; + /** The JWT full name. */ + jwtName: string; + /** The JWT short name. */ + shortName: string; + /** The JWT typ" (Type) */ + typ: string; +} + +export const CLIENT_CERT_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'; + +export const PN_TOKEN_INFO: FirebasePhoneNumberTokenInfo = { + url: 'https://firebase.google.com/docs/phone-number-verification', + verifyApiName: 'verifyToken()', + jwtName: 'Firebase Phone Verification token', + shortName: 'FPNV token', + typ: 'JWT', +}; + +export const FPNV_ERROR_CODE_MAPPING = { + INVALID_ARGUMENT: 'invalid-argument', + INVALID_TOKEN: 'invalid-token', + EXPIRED_TOKEN: 'expired-token', +} satisfies Record; + +export type FpnvErrorCode = + | 'invalid-argument' + | 'invalid-token' + | 'expired-token' + +/** + * Firebase Phone Number Verification error code structure. This extends `PrefixedFirebaseError`. + * + * @param code - The error code. + * @param message - The error message. + * @constructor + */ +export class FirebasePnvError extends PrefixedFirebaseError { + constructor(code: FpnvErrorCode, message: string) { + super('fpnv', code, message); + + /* tslint:disable:max-line-length */ + // Set the prototype explicitly. See the following link for more details: + // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work + /* tslint:enable:max-line-length */ + (this as any).__proto__ = FirebasePnvError.prototype; + } +} \ No newline at end of file diff --git a/src/fpnv/fpnv-api.ts b/src/fpnv/fpnv-api.ts index 0cea0e98ac..4528ca2d79 100644 --- a/src/fpnv/fpnv-api.ts +++ b/src/fpnv/fpnv-api.ts @@ -20,20 +20,61 @@ * Interface representing a Fpnv token. */ export interface FpnvToken { - aud: string; - auth_time: number; + /** + * The issuer identifier for the issuer of the response. + * This value is a URL with the format + * `https://firebaseappcheck.googleapis.com/`, where `` is the + * same project number specified in the {@link aud} property. + */ + iss: string; + + /** + * The audience for which this token is intended. + * This value is a JSON array of two strings, the first is the project number of your + * Firebase project, and the second is the project ID of the same project. + */ + aud: string[]; + + /** + * The Fpnv token's expiration time, in seconds since the Unix epoch. That is, the + * time at which this Fpnv token expires and should no longer be considered valid. + */ exp: number; + + /** + * The Fpnv token's issued-at time, in seconds since the Unix epoch. That is, the + * time at which this Fpnv token was issued and should start to be considered + * valid. + */ iat: number; - iss: string; + + /** + * The phone number of User. + */ sub: string; + /** + * Unique ID. + */ + jti: string; + + /** + * Unique ID. + */ + nonce: string; + + /** + * The corresponding user's phone number. + * This value is not actually one of the JWT token claims. It is added as a + * convenience, and is set as the value of the {@link sub} property. + */ getPhoneNumber(): string; /** - * Other arbitrary claims included in the ID token. + * Other arbitrary claims included in the token. */ [key: string]: any; } -export { FpnvErrorCode, FirebasePnvError } from '../utils/error'; +export { FpnvErrorCode, FirebasePnvError } from './fpnv-api-client-internal'; diff --git a/src/fpnv/fpnv.ts b/src/fpnv/fpnv.ts index 9596418f25..5d72b480f5 100644 --- a/src/fpnv/fpnv.ts +++ b/src/fpnv/fpnv.ts @@ -17,35 +17,42 @@ import { App } from '../app'; import { FpnvToken } from './fpnv-api'; -import { - FirebasePhoneNumberTokenVerifier, - createFPNTVerifier, -} from './token-verifier'; +import { FirebasePhoneNumberTokenVerifier } from './token-verifier'; +import { CLIENT_CERT_URL, PN_TOKEN_INFO } from './fpnv-api-client-internal'; /** * Fpnv service bound to the provided app. */ -export class Fpnv { - private readonly app_: App; - +export class Fpnv { + private readonly appInternal: App; protected readonly fpnvVerifier: FirebasePhoneNumberTokenVerifier; + /** + * @param app - The app for this `Fpnv` service. + * @constructor + * @internal + */ constructor(app: App) { - this.app_ = app; - this.fpnvVerifier = createFPNTVerifier(app); + this.appInternal = app; + this.fpnvVerifier = new FirebasePhoneNumberTokenVerifier( + CLIENT_CERT_URL, + 'https://fpnv.googleapis.com/projects/', + PN_TOKEN_INFO, + app + ); } /** - * Returns the app associated with this Auth instance. + * Returns the app associated with this `Fpnv` instance. * - * @returns The app associated with this Auth instance. + * @returns The app associated with this `Fpnv` instance. */ get app(): App { - return this.app_; + return this.appInternal; } - public async verifyToken(idToken: string): Promise { - return await this.fpnvVerifier.verifyJWT(idToken); + public async verifyToken(fpnvJwt: string): Promise { + return await this.fpnvVerifier.verifyJWT(fpnvJwt); } } diff --git a/src/fpnv/token-verifier.ts b/src/fpnv/token-verifier.ts index f56cee69e1..75c84ac935 100644 --- a/src/fpnv/token-verifier.ts +++ b/src/fpnv/token-verifier.ts @@ -16,41 +16,14 @@ */ import { App } from '../app'; -import { FpnvErrorCode, FirebasePnvError, ErrorInfo } from '../utils/error'; -import { FpnvToken } from './fpnv-api'; +import { FirebasePnvError, FpnvToken } from './fpnv-api'; import * as util from '../utils/index'; import * as validator from '../utils/validator'; import { DecodedToken, decodeJwt, JwtError, JwtErrorCode, PublicKeySignatureVerifier, ALGORITHM_ES256, SignatureVerifier, } from '../utils/jwt'; - -const CLIENT_CERT_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'; - - -const PN_TOKEN_INFO: FirebasePhoneNumberTokenInfo = { - url: 'https://firebase.google.com/docs/phone-number-verification', - verifyApiName: 'verifyToken()', - jwtName: 'Firebase Phone Verification token', - shortName: 'FPNV token', - typ: 'JWT', - expiredErrorCode: FpnvErrorCode.EXPIRED_TOKEN, -}; - -interface FirebasePhoneNumberTokenInfo { - /** Documentation URL. */ - url: string; - /** verify API name. */ - verifyApiName: string; - /** The JWT full name. */ - jwtName: string; - /** The JWT short name. */ - shortName: string; - /** JWT Expiration error code. */ - expiredErrorCode: ErrorInfo; - /** The JWT typ" (Type) */ - typ: string; -} +import { FirebasePhoneNumberTokenInfo, FPNV_ERROR_CODE_MAPPING } from './fpnv-api-client-internal'; export class FirebasePhoneNumberTokenVerifier { @@ -66,44 +39,39 @@ export class FirebasePhoneNumberTokenVerifier { if (!validator.isURL(clientCertUrl)) { throw new FirebasePnvError( - FpnvErrorCode.INVALID_ARGUMENT, + FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, 'The provided public client certificate URL is an invalid URL.', ); } else if (!validator.isURL(issuer)) { throw new FirebasePnvError( - FpnvErrorCode.INVALID_ARGUMENT, + FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, 'The provided JWT issuer is an invalid URL.', ); } else if (!validator.isNonNullObject(tokenInfo)) { throw new FirebasePnvError( - FpnvErrorCode.INVALID_ARGUMENT, + FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, 'The provided JWT information is not an object or null.', ); } else if (!validator.isURL(tokenInfo.url)) { throw new FirebasePnvError( - FpnvErrorCode.INVALID_ARGUMENT, + FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, 'The provided JWT verification documentation URL is invalid.', ); } else if (!validator.isNonEmptyString(tokenInfo.verifyApiName)) { throw new FirebasePnvError( - FpnvErrorCode.INVALID_ARGUMENT, + FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, 'The JWT verify API name must be a non-empty string.', ); } else if (!validator.isNonEmptyString(tokenInfo.jwtName)) { throw new FirebasePnvError( - FpnvErrorCode.INVALID_ARGUMENT, + FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, 'The JWT public full name must be a non-empty string.', ); } else if (!validator.isNonEmptyString(tokenInfo.shortName)) { throw new FirebasePnvError( - FpnvErrorCode.INVALID_ARGUMENT, + FPNV_ERROR_CODE_MAPPING.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 FirebasePnvError( - FpnvErrorCode.INVALID_ARGUMENT, - 'The JWT expiration error code must be a non-null ErrorInfo object.', - ); } this.shortNameArticle = tokenInfo.shortName.charAt(0).match(/[aeiou]/i) ? 'an' : 'a'; @@ -116,8 +84,8 @@ export class FirebasePhoneNumberTokenVerifier { public async verifyJWT(jwtToken: string): Promise { if (!validator.isString(jwtToken)) { throw new FirebasePnvError( - FpnvErrorCode.PROJECT_NOT_FOUND, - `First argument to ${this.tokenInfo.verifyApiName} must be a ${this.tokenInfo.jwtName} string.`, + FPNV_ERROR_CODE_MAPPING.INVALID_TOKEN, + `First argument to ${this.tokenInfo.verifyApiName} must be a string.`, ); } @@ -132,7 +100,7 @@ export class FirebasePhoneNumberTokenVerifier { const projectId = await util.findProjectId(this.app); if (!validator.isNonEmptyString(projectId)) { throw new FirebasePnvError( - FpnvErrorCode.PROJECT_NOT_FOUND, + FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, '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}.`); } @@ -159,14 +127,13 @@ export class FirebasePhoneNumberTokenVerifier { const errorMessage = `Decoding ${this.tokenInfo.jwtName} failed. Make sure you passed ` + `the entire string JWT which represents ${this.shortNameArticle} ` + `${this.tokenInfo.shortName}.` + verifyJwtTokenDocsMessage; - throw new FirebasePnvError(FpnvErrorCode.INVALID_ARGUMENT, + throw new FirebasePnvError(FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, errorMessage); } - throw new FirebasePnvError(FpnvErrorCode.INVALID_ARGUMENT, err.message); + throw new FirebasePnvError(FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, err.message); } } - private verifyContent( fullDecodedToken: DecodedToken, projectId: string | null, @@ -174,6 +141,7 @@ export class FirebasePhoneNumberTokenVerifier { const header = fullDecodedToken && fullDecodedToken.header; const payload = fullDecodedToken && fullDecodedToken.payload; + const scopedProjectId = `${this.issuer}${projectId}`; const projectIdMatchMessage = ` Make sure the ${this.tokenInfo.shortName} comes from the same ` + 'Firebase project as the service account used to authenticate this SDK.'; const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` + @@ -190,22 +158,20 @@ export class FirebasePhoneNumberTokenVerifier { `"${ALGORITHM_ES256}" but got "${header.alg}". ${verifyJwtTokenDocsMessage}`; } else if (header.typ !== this.tokenInfo.typ) { errorMessage = `${this.tokenInfo.jwtName} has incorrect typ. Expected "${this.tokenInfo.typ}" but got ` + - '"' + header.typ + '".' + verifyJwtTokenDocsMessage; + `"${header.typ}". ${verifyJwtTokenDocsMessage}`; } // FPNV Token - else if (!((Array.isArray(payload.aud) ? payload.aud : []).some(item => item === this.issuer + projectId))) { - errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` + - this.issuer + projectId + '" to be one of "' + payload.aud + '".' + projectIdMatchMessage + - verifyJwtTokenDocsMessage; + else if (!validator.isNonEmptyArray(payload.aud) || !payload.aud.includes(scopedProjectId)) { + errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected ` + + `"${scopedProjectId}" to be one of "${payload.aud}". ${projectIdMatchMessage} ${verifyJwtTokenDocsMessage}`; } else if (typeof payload.sub !== 'string') { - errorMessage = `${this.tokenInfo.jwtName} has no "sub" (subject) claim.` + verifyJwtTokenDocsMessage; + errorMessage = `${this.tokenInfo.jwtName} has no "sub" (subject) claim. ${verifyJwtTokenDocsMessage}`; } else if (payload.sub === '') { - errorMessage = `${this.tokenInfo.jwtName} has an empty "sub" (subject) claim.` + - verifyJwtTokenDocsMessage; + errorMessage = `${this.tokenInfo.jwtName} has an empty "sub" (subject) claim. ${verifyJwtTokenDocsMessage}`; } if (errorMessage) { - throw new FirebasePnvError(FpnvErrorCode.INVALID_ARGUMENT, errorMessage); + throw new FirebasePnvError(FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, errorMessage); } } @@ -222,28 +188,18 @@ export class FirebasePhoneNumberTokenVerifier { `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`; if (error.code === JwtErrorCode.TOKEN_EXPIRED) { 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 new FirebasePnvError(this.tokenInfo.expiredErrorCode, errorMessage); + ` from your client app and try again. ${verifyJwtTokenDocsMessage}`; + return new FirebasePnvError(FPNV_ERROR_CODE_MAPPING.EXPIRED_TOKEN, errorMessage); } else if (error.code === JwtErrorCode.INVALID_SIGNATURE) { - const errorMessage = `${this.tokenInfo.jwtName} has invalid signature.` + verifyJwtTokenDocsMessage; - return new FirebasePnvError(FpnvErrorCode.INVALID_ARGUMENT, errorMessage); + const errorMessage = `${this.tokenInfo.jwtName} has invalid signature. ${verifyJwtTokenDocsMessage}`; + return new FirebasePnvError(FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, errorMessage); } else if (error.code === JwtErrorCode.NO_MATCHING_KID) { const errorMessage = `${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.'; - return new FirebasePnvError(FpnvErrorCode.INVALID_ARGUMENT, errorMessage); + return new FirebasePnvError(FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, errorMessage); } - return new FirebasePnvError(FpnvErrorCode.INVALID_ARGUMENT, error.message); + return new FirebasePnvError(FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, error.message); } } - -export function createFPNTVerifier(app: App): FirebasePhoneNumberTokenVerifier { - return new FirebasePhoneNumberTokenVerifier( - CLIENT_CERT_URL, - 'https://fpnv.googleapis.com/projects/', - PN_TOKEN_INFO, - app - ); -} diff --git a/src/utils/error.ts b/src/utils/error.ts index 7a25d9b7f8..db013b8967 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -1145,45 +1145,3 @@ const TOPIC_MGT_SERVER_TO_CLIENT_CODE: ServerToClientCode = { INTERNAL: 'INTERNAL_ERROR', UNKNOWN: 'UNKNOWN_ERROR', }; - -/** - * Firebase Phone Number Verification error code structure. This extends PrefixedFirebaseError. - */ -export class FirebasePnvError extends PrefixedFirebaseError { - /** - * @param info - The error code info. - * @param message - The error message. This will override the default message if provided. - * @constructor - * @internal - */ - constructor(info: ErrorInfo, message?: string) { - // Override default message if custom message provided. - super('fpnv', info.code, message || info.message); - - /* tslint:disable:max-line-length */ - // Set the prototype explicitly. See the following link for more details: - // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work - /* tslint:enable:max-line-length */ - (this as any).__proto__ = FirebasePnvError.prototype; - } -} - -export class FpnvErrorCode { - static readonly INVALID_ARGUMENT: ErrorInfo = { - code: 'invalid-argument', - message: 'Invalid argument provided to the FPNV method.', - }; - static readonly INVALID_TOKEN: ErrorInfo = { - code: 'invalid-token', - message: 'The provided phone number verification token is invalid.', - }; - static readonly EXPIRED_TOKEN: ErrorInfo = { - code: 'expired-token', - message: 'The provided phone number verification token has expired.', - }; - static readonly PROJECT_NOT_FOUND: ErrorInfo = { - code: 'project-not-found', - message: 'No Firebase project was found for the provided credential.', - }; - -} \ No newline at end of file From 71f48cbafb8ec95d236e1fafb7b5fb1f53b58fc3 Mon Sep 17 00:00:00 2001 From: Andrii Boiko Date: Thu, 11 Dec 2025 14:36:37 +0100 Subject: [PATCH 9/9] chore: linting --- etc/firebase-admin.fpnv.api.md | 35 ++++------------------------ src/fpnv/fpnv-api-client-internal.ts | 4 ++-- src/fpnv/fpnv-api.ts | 6 ++--- src/fpnv/index.ts | 2 -- src/fpnv/token-verifier.ts | 34 +++++++++++++-------------- 5 files changed, 26 insertions(+), 55 deletions(-) diff --git a/etc/firebase-admin.fpnv.api.md b/etc/firebase-admin.fpnv.api.md index 2b32fa3d91..8a390a79fb 100644 --- a/etc/firebase-admin.fpnv.api.md +++ b/etc/firebase-admin.fpnv.api.md @@ -6,55 +6,28 @@ import { Agent } from 'http'; -// Warning: (ae-forgotten-export) The symbol "PrefixedFirebaseError" needs to be exported by the entry point index.d.ts -// -// @public -export class FirebasePnvError extends PrefixedFirebaseError { -} - // @public export class Fpnv { // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts - constructor(app: App); get app(): App; // Warning: (ae-forgotten-export) The symbol "FirebasePhoneNumberTokenVerifier" needs to be exported by the entry point index.d.ts // // (undocumented) protected readonly fpnvVerifier: FirebasePhoneNumberTokenVerifier; // (undocumented) - verifyToken(idToken: string): Promise; -} - -// @public (undocumented) -export class FpnvErrorCode { - // (undocumented) - static readonly EXPIRED_TOKEN: ErrorInfo; - // Warning: (ae-forgotten-export) The symbol "ErrorInfo" needs to be exported by the entry point index.d.ts - // - // (undocumented) - static readonly INVALID_ARGUMENT: ErrorInfo; - // (undocumented) - static readonly INVALID_TOKEN: ErrorInfo; - // (undocumented) - static readonly PROJECT_NOT_FOUND: ErrorInfo; + verifyToken(fpnvJwt: string): Promise; } // @public export interface FpnvToken { [key: string]: any; - // (undocumented) - aud: string; - // (undocumented) - auth_time: number; - // (undocumented) + aud: string[]; exp: number; - // (undocumented) getPhoneNumber(): string; - // (undocumented) iat: number; - // (undocumented) iss: string; - // (undocumented) + jti: string; + nonce: string; sub: string; } diff --git a/src/fpnv/fpnv-api-client-internal.ts b/src/fpnv/fpnv-api-client-internal.ts index 82fceffb24..22a3a85ff3 100644 --- a/src/fpnv/fpnv-api-client-internal.ts +++ b/src/fpnv/fpnv-api-client-internal.ts @@ -58,7 +58,7 @@ export type FpnvErrorCode = * @param message - The error message. * @constructor */ -export class FirebasePnvError extends PrefixedFirebaseError { +export class FirebaseFpnvError extends PrefixedFirebaseError { constructor(code: FpnvErrorCode, message: string) { super('fpnv', code, message); @@ -66,6 +66,6 @@ export class FirebasePnvError extends PrefixedFirebaseError { // Set the prototype explicitly. See the following link for more details: // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work /* tslint:enable:max-line-length */ - (this as any).__proto__ = FirebasePnvError.prototype; + (this as any).__proto__ = FirebaseFpnvError.prototype; } } \ No newline at end of file diff --git a/src/fpnv/fpnv-api.ts b/src/fpnv/fpnv-api.ts index 4528ca2d79..1c298302a6 100644 --- a/src/fpnv/fpnv-api.ts +++ b/src/fpnv/fpnv-api.ts @@ -24,7 +24,7 @@ export interface FpnvToken { * The issuer identifier for the issuer of the response. * This value is a URL with the format * `https://firebaseappcheck.googleapis.com/`, where `` is the - * same project number specified in the {@link aud} property. + * same project number specified in the {@link FpnvToken.aud} property. */ iss: string; @@ -66,7 +66,7 @@ export interface FpnvToken { /** * The corresponding user's phone number. * This value is not actually one of the JWT token claims. It is added as a - * convenience, and is set as the value of the {@link sub} property. + * convenience, and is set as the value of the {@link FpnvToken.sub} property. */ getPhoneNumber(): string; @@ -76,5 +76,5 @@ export interface FpnvToken { [key: string]: any; } -export { FpnvErrorCode, FirebasePnvError } from './fpnv-api-client-internal'; +export { FpnvErrorCode, FirebaseFpnvError } from './fpnv-api-client-internal'; diff --git a/src/fpnv/index.ts b/src/fpnv/index.ts index 1d296412c9..90d96bff92 100644 --- a/src/fpnv/index.ts +++ b/src/fpnv/index.ts @@ -31,8 +31,6 @@ export { export { FpnvToken, - FirebasePnvError, - FpnvErrorCode, } from './fpnv-api' /** diff --git a/src/fpnv/token-verifier.ts b/src/fpnv/token-verifier.ts index 75c84ac935..3debad2595 100644 --- a/src/fpnv/token-verifier.ts +++ b/src/fpnv/token-verifier.ts @@ -16,7 +16,7 @@ */ import { App } from '../app'; -import { FirebasePnvError, FpnvToken } from './fpnv-api'; +import { FirebaseFpnvError, FpnvToken } from './fpnv-api'; import * as util from '../utils/index'; import * as validator from '../utils/validator'; import { @@ -38,37 +38,37 @@ export class FirebasePhoneNumberTokenVerifier { ) { if (!validator.isURL(clientCertUrl)) { - throw new FirebasePnvError( + throw new FirebaseFpnvError( FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, 'The provided public client certificate URL is an invalid URL.', ); } else if (!validator.isURL(issuer)) { - throw new FirebasePnvError( + throw new FirebaseFpnvError( FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, 'The provided JWT issuer is an invalid URL.', ); } else if (!validator.isNonNullObject(tokenInfo)) { - throw new FirebasePnvError( + throw new FirebaseFpnvError( FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, 'The provided JWT information is not an object or null.', ); } else if (!validator.isURL(tokenInfo.url)) { - throw new FirebasePnvError( + throw new FirebaseFpnvError( FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, 'The provided JWT verification documentation URL is invalid.', ); } else if (!validator.isNonEmptyString(tokenInfo.verifyApiName)) { - throw new FirebasePnvError( + throw new FirebaseFpnvError( FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, 'The JWT verify API name must be a non-empty string.', ); } else if (!validator.isNonEmptyString(tokenInfo.jwtName)) { - throw new FirebasePnvError( + throw new FirebaseFpnvError( FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, 'The JWT public full name must be a non-empty string.', ); } else if (!validator.isNonEmptyString(tokenInfo.shortName)) { - throw new FirebasePnvError( + throw new FirebaseFpnvError( FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, 'The JWT public short name must be a non-empty string.', ); @@ -83,7 +83,7 @@ export class FirebasePhoneNumberTokenVerifier { public async verifyJWT(jwtToken: string): Promise { if (!validator.isString(jwtToken)) { - throw new FirebasePnvError( + throw new FirebaseFpnvError( FPNV_ERROR_CODE_MAPPING.INVALID_TOKEN, `First argument to ${this.tokenInfo.verifyApiName} must be a string.`, ); @@ -99,7 +99,7 @@ export class FirebasePhoneNumberTokenVerifier { private async ensureProjectId(): Promise { const projectId = await util.findProjectId(this.app); if (!validator.isNonEmptyString(projectId)) { - throw new FirebasePnvError( + throw new FirebaseFpnvError( FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, '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}.`); @@ -127,10 +127,10 @@ export class FirebasePhoneNumberTokenVerifier { const errorMessage = `Decoding ${this.tokenInfo.jwtName} failed. Make sure you passed ` + `the entire string JWT which represents ${this.shortNameArticle} ` + `${this.tokenInfo.shortName}.` + verifyJwtTokenDocsMessage; - throw new FirebasePnvError(FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + throw new FirebaseFpnvError(FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, errorMessage); } - throw new FirebasePnvError(FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, err.message); + throw new FirebaseFpnvError(FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, err.message); } } @@ -171,7 +171,7 @@ export class FirebasePhoneNumberTokenVerifier { } if (errorMessage) { - throw new FirebasePnvError(FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, errorMessage); + throw new FirebaseFpnvError(FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, errorMessage); } } @@ -189,17 +189,17 @@ export class FirebasePhoneNumberTokenVerifier { if (error.code === JwtErrorCode.TOKEN_EXPIRED) { const errorMessage = `${this.tokenInfo.jwtName} has expired. Get a fresh ${this.tokenInfo.shortName}` + ` from your client app and try again. ${verifyJwtTokenDocsMessage}`; - return new FirebasePnvError(FPNV_ERROR_CODE_MAPPING.EXPIRED_TOKEN, errorMessage); + return new FirebaseFpnvError(FPNV_ERROR_CODE_MAPPING.EXPIRED_TOKEN, errorMessage); } else if (error.code === JwtErrorCode.INVALID_SIGNATURE) { const errorMessage = `${this.tokenInfo.jwtName} has invalid signature. ${verifyJwtTokenDocsMessage}`; - return new FirebasePnvError(FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, errorMessage); + return new FirebaseFpnvError(FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, errorMessage); } else if (error.code === JwtErrorCode.NO_MATCHING_KID) { const errorMessage = `${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.'; - return new FirebasePnvError(FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, errorMessage); + return new FirebaseFpnvError(FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, errorMessage); } - return new FirebasePnvError(FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, error.message); + return new FirebaseFpnvError(FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, error.message); } }