UNPKG

firebase-admin

Version:
171 lines (170 loc) 11.1 kB
/*! firebase-admin v13.8.0 */ "use strict"; /*! * @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. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.PhoneNumberTokenVerifier = void 0; const phone_number_verification_api_1 = require("./phone-number-verification-api"); const util = require("../utils/index"); const validator = require("../utils/validator"); const jwt_1 = require("../utils/jwt"); const phone_number_verification_api_client_internal_1 = require("./phone-number-verification-api-client-internal"); class PhoneNumberTokenVerifier { constructor(jwksUrl, issuer, tokenInfo, app) { this.issuer = issuer; this.tokenInfo = tokenInfo; this.app = app; if (!validator.isURL(jwksUrl)) { throw new phone_number_verification_api_1.FirebasePhoneNumberVerificationError(phone_number_verification_api_client_internal_1.FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, 'The provided public client certificate URL is an invalid URL.'); } else if (!validator.isURL(issuer)) { throw new phone_number_verification_api_1.FirebasePhoneNumberVerificationError(phone_number_verification_api_client_internal_1.FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, 'The provided JWT issuer is an invalid URL.'); } else if (!validator.isNonNullObject(tokenInfo)) { throw new phone_number_verification_api_1.FirebasePhoneNumberVerificationError(phone_number_verification_api_client_internal_1.FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, 'The provided JWT information is not an object or null.'); } else if (!validator.isURL(tokenInfo.url)) { throw new phone_number_verification_api_1.FirebasePhoneNumberVerificationError(phone_number_verification_api_client_internal_1.FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, 'The provided JWT verification documentation URL is invalid.'); } else if (!validator.isNonEmptyString(tokenInfo.verifyApiName)) { throw new phone_number_verification_api_1.FirebasePhoneNumberVerificationError(phone_number_verification_api_client_internal_1.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 phone_number_verification_api_1.FirebasePhoneNumberVerificationError(phone_number_verification_api_client_internal_1.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 phone_number_verification_api_1.FirebasePhoneNumberVerificationError(phone_number_verification_api_client_internal_1.FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, 'The JWT public short name must be a non-empty string.'); } this.shortNameArticle = tokenInfo.shortName.charAt(0).match(/[aeiou]/i) ? 'an' : 'a'; this.signatureVerifier = jwt_1.PublicKeySignatureVerifier.withJwksUrl(jwksUrl, app.options.httpAgent); // Project ID is validated in the verification call. } async verifyJWT(jwtToken) { if (!validator.isString(jwtToken)) { throw new phone_number_verification_api_1.FirebasePhoneNumberVerificationError(phone_number_verification_api_client_internal_1.FPNV_ERROR_CODE_MAPPING.INVALID_TOKEN, `First argument to ${this.tokenInfo.verifyApiName} must be a string.`); } const projectId = await this.ensureProjectId(); const decoded = await this.decodeAndVerify(jwtToken, projectId); const decodedIdToken = decoded.payload; decodedIdToken.phoneNumber = decodedIdToken.sub; return decodedIdToken; } async ensureProjectId() { const projectId = await util.findProjectId(this.app); if (!validator.isNonEmptyString(projectId)) { throw new phone_number_verification_api_1.FirebasePhoneNumberVerificationError(phone_number_verification_api_client_internal_1.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}.`); } return projectId; } async decodeAndVerify(token, projectId) { const decodedToken = await this.safeDecode(token); this.verifyContent(decodedToken, projectId); await this.verifySignature(token); return decodedToken; } async safeDecode(jwtToken) { try { return await (0, jwt_1.decodeJwt)(jwtToken); } catch (err) { if (err.code === jwt_1.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 phone_number_verification_api_1.FirebasePhoneNumberVerificationError(phone_number_verification_api_client_internal_1.FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, errorMessage); } throw new phone_number_verification_api_1.FirebasePhoneNumberVerificationError(phone_number_verification_api_client_internal_1.FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, err.message); } } verifyContent(fullDecodedToken, projectId) { 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}.`; const scopedProjectId = `${this.issuer}${projectId}`; let errorMessage; // JWT Header if (!header || typeof header.kid === 'undefined') { errorMessage = `${this.tokenInfo.jwtName} has no "kid" claim.`; errorMessage += verifyJwtTokenDocsMessage; } else if (header.alg !== jwt_1.ALGORITHM_ES256) { errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected ` + `"${jwt_1.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) { errorMessage = `${this.tokenInfo.jwtName} has no payload. ${verifyJwtTokenDocsMessage}`; } else if (typeof payload.iss !== 'string' || !payload.iss.startsWith(this.issuer)) { errorMessage = `${this.tokenInfo.jwtName} has incorrect "iss" (issuer) claim. Expected ` + `an issuer starting with "${this.issuer}" but got "${payload.iss}".` + ` ${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}`; } else if (payload.sub === '') { errorMessage = `${this.tokenInfo.jwtName} has an empty "sub" (subject) claim. ${verifyJwtTokenDocsMessage}`; } if (errorMessage) { throw new phone_number_verification_api_1.FirebasePhoneNumberVerificationError(phone_number_verification_api_client_internal_1.FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, errorMessage); } } async verifySignature(jwtToken) { try { return await this.signatureVerifier.verify(jwtToken); } catch (error) { throw this.mapJwtErrorToAuthError(error); } } mapJwtErrorToAuthError(error) { const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` + `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`; if (error.code === jwt_1.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 phone_number_verification_api_1.FirebasePhoneNumberVerificationError(phone_number_verification_api_client_internal_1.FPNV_ERROR_CODE_MAPPING.EXPIRED_TOKEN, errorMessage); } else if (error.code === jwt_1.JwtErrorCode.INVALID_SIGNATURE) { const errorMessage = `${this.tokenInfo.jwtName} has invalid signature. ${verifyJwtTokenDocsMessage}`; return new phone_number_verification_api_1.FirebasePhoneNumberVerificationError(phone_number_verification_api_client_internal_1.FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, errorMessage); } else if (error.code === jwt_1.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 phone_number_verification_api_1.FirebasePhoneNumberVerificationError(phone_number_verification_api_client_internal_1.FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, errorMessage); } return new phone_number_verification_api_1.FirebasePhoneNumberVerificationError(phone_number_verification_api_client_internal_1.FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, error.message); } } exports.PhoneNumberTokenVerifier = PhoneNumberTokenVerifier;