firebase-admin
Version:
Firebase admin SDK for Node.js
171 lines (170 loc) • 11.1 kB
JavaScript
/*! firebase-admin v13.8.0 */
;
/*!
* @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;