171 lines
11 KiB
JavaScript
171 lines
11 KiB
JavaScript
/*! 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;
|