123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515 |
- /*! firebase-admin v12.1.1 */
- "use strict";
- /*!
- * @license
- * Copyright 2020 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.getApplicationDefault = exports.isApplicationDefault = exports.ImpersonatedServiceAccountCredential = exports.RefreshTokenCredential = exports.ComputeEngineCredential = exports.ServiceAccountCredential = void 0;
- const fs = require("fs");
- const os = require("os");
- const path = require("path");
- const error_1 = require("../utils/error");
- const api_request_1 = require("../utils/api-request");
- const util = require("../utils/validator");
- const GOOGLE_TOKEN_AUDIENCE = 'https://accounts.google.com/o/oauth2/token';
- const GOOGLE_AUTH_TOKEN_HOST = 'accounts.google.com';
- const GOOGLE_AUTH_TOKEN_PATH = '/o/oauth2/token';
- // NOTE: the Google Metadata Service uses HTTP over a vlan
- const GOOGLE_METADATA_SERVICE_HOST = 'metadata.google.internal';
- const GOOGLE_METADATA_SERVICE_TOKEN_PATH = '/computeMetadata/v1/instance/service-accounts/default/token';
- const GOOGLE_METADATA_SERVICE_IDENTITY_PATH = '/computeMetadata/v1/instance/service-accounts/default/identity';
- const GOOGLE_METADATA_SERVICE_PROJECT_ID_PATH = '/computeMetadata/v1/project/project-id';
- const GOOGLE_METADATA_SERVICE_ACCOUNT_ID_PATH = '/computeMetadata/v1/instance/service-accounts/default/email';
- const configDir = (() => {
- // Windows has a dedicated low-rights location for apps at ~/Application Data
- const sys = os.platform();
- if (sys && sys.length >= 3 && sys.substring(0, 3).toLowerCase() === 'win') {
- return process.env.APPDATA;
- }
- // On *nix the gcloud cli creates a . dir.
- return process.env.HOME && path.resolve(process.env.HOME, '.config');
- })();
- const GCLOUD_CREDENTIAL_SUFFIX = 'gcloud/application_default_credentials.json';
- const GCLOUD_CREDENTIAL_PATH = configDir && path.resolve(configDir, GCLOUD_CREDENTIAL_SUFFIX);
- const REFRESH_TOKEN_HOST = 'www.googleapis.com';
- const REFRESH_TOKEN_PATH = '/oauth2/v4/token';
- const ONE_HOUR_IN_SECONDS = 60 * 60;
- const JWT_ALGORITHM = 'RS256';
- /**
- * Implementation of Credential that uses a service account.
- */
- class ServiceAccountCredential {
- /**
- * Creates a new ServiceAccountCredential from the given parameters.
- *
- * @param serviceAccountPathOrObject - Service account json object or path to a service account json file.
- * @param httpAgent - Optional http.Agent to use when calling the remote token server.
- * @param implicit - An optinal boolean indicating whether this credential was implicitly discovered from the
- * environment, as opposed to being explicitly specified by the developer.
- *
- * @constructor
- */
- constructor(serviceAccountPathOrObject, httpAgent, implicit = false) {
- this.httpAgent = httpAgent;
- this.implicit = implicit;
- const serviceAccount = (typeof serviceAccountPathOrObject === 'string') ?
- ServiceAccount.fromPath(serviceAccountPathOrObject)
- : new ServiceAccount(serviceAccountPathOrObject);
- this.projectId = serviceAccount.projectId;
- this.privateKey = serviceAccount.privateKey;
- this.clientEmail = serviceAccount.clientEmail;
- this.httpClient = new api_request_1.HttpClient();
- }
- getAccessToken() {
- const token = this.createAuthJwt_();
- const postData = 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3A' +
- 'grant-type%3Ajwt-bearer&assertion=' + token;
- const request = {
- method: 'POST',
- url: `https://${GOOGLE_AUTH_TOKEN_HOST}${GOOGLE_AUTH_TOKEN_PATH}`,
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- },
- data: postData,
- httpAgent: this.httpAgent,
- };
- return requestAccessToken(this.httpClient, request);
- }
- // eslint-disable-next-line @typescript-eslint/naming-convention
- createAuthJwt_() {
- const claims = {
- scope: [
- 'https://www.googleapis.com/auth/cloud-platform',
- 'https://www.googleapis.com/auth/firebase.database',
- 'https://www.googleapis.com/auth/firebase.messaging',
- 'https://www.googleapis.com/auth/identitytoolkit',
- 'https://www.googleapis.com/auth/userinfo.email',
- ].join(' '),
- };
- // eslint-disable-next-line @typescript-eslint/no-var-requires
- const jwt = require('jsonwebtoken');
- // This method is actually synchronous so we can capture and return the buffer.
- return jwt.sign(claims, this.privateKey, {
- audience: GOOGLE_TOKEN_AUDIENCE,
- expiresIn: ONE_HOUR_IN_SECONDS,
- issuer: this.clientEmail,
- algorithm: JWT_ALGORITHM,
- });
- }
- }
- exports.ServiceAccountCredential = ServiceAccountCredential;
- /**
- * A struct containing the properties necessary to use service account JSON credentials.
- */
- class ServiceAccount {
- static fromPath(filePath) {
- try {
- return new ServiceAccount(JSON.parse(fs.readFileSync(filePath, 'utf8')));
- }
- catch (error) {
- // Throw a nicely formed error message if the file contents cannot be parsed
- throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, 'Failed to parse service account json file: ' + error);
- }
- }
- constructor(json) {
- if (!util.isNonNullObject(json)) {
- throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, 'Service account must be an object.');
- }
- copyAttr(this, json, 'projectId', 'project_id');
- copyAttr(this, json, 'privateKey', 'private_key');
- copyAttr(this, json, 'clientEmail', 'client_email');
- let errorMessage;
- if (!util.isNonEmptyString(this.projectId)) {
- errorMessage = 'Service account object must contain a string "project_id" property.';
- }
- else if (!util.isNonEmptyString(this.privateKey)) {
- errorMessage = 'Service account object must contain a string "private_key" property.';
- }
- else if (!util.isNonEmptyString(this.clientEmail)) {
- errorMessage = 'Service account object must contain a string "client_email" property.';
- }
- if (typeof errorMessage !== 'undefined') {
- throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, errorMessage);
- }
- // eslint-disable-next-line @typescript-eslint/no-var-requires
- const forge = require('node-forge');
- try {
- forge.pki.privateKeyFromPem(this.privateKey);
- }
- catch (error) {
- throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, 'Failed to parse private key: ' + error);
- }
- }
- }
- /**
- * Implementation of Credential that gets access tokens from the metadata service available
- * in the Google Cloud Platform. This authenticates the process as the default service account
- * of an App Engine instance or Google Compute Engine machine.
- */
- class ComputeEngineCredential {
- constructor(httpAgent) {
- this.httpClient = new api_request_1.HttpClient();
- this.httpAgent = httpAgent;
- }
- getAccessToken() {
- const request = this.buildRequest(GOOGLE_METADATA_SERVICE_TOKEN_PATH);
- return requestAccessToken(this.httpClient, request);
- }
- /**
- * getIDToken returns a OIDC token from the compute metadata service
- * that can be used to make authenticated calls to audience
- * @param audience the URL the returned ID token will be used to call.
- */
- getIDToken(audience) {
- const request = this.buildRequest(`${GOOGLE_METADATA_SERVICE_IDENTITY_PATH}?audience=${audience}`);
- return requestIDToken(this.httpClient, request);
- }
- getProjectId() {
- if (this.projectId) {
- return Promise.resolve(this.projectId);
- }
- const request = this.buildRequest(GOOGLE_METADATA_SERVICE_PROJECT_ID_PATH);
- return this.httpClient.send(request)
- .then((resp) => {
- this.projectId = resp.text;
- return this.projectId;
- })
- .catch((err) => {
- const detail = (err instanceof api_request_1.HttpError) ? getDetailFromResponse(err.response) : err.message;
- throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, `Failed to determine project ID: ${detail}`);
- });
- }
- getServiceAccountEmail() {
- if (this.accountId) {
- return Promise.resolve(this.accountId);
- }
- const request = this.buildRequest(GOOGLE_METADATA_SERVICE_ACCOUNT_ID_PATH);
- return this.httpClient.send(request)
- .then((resp) => {
- this.accountId = resp.text;
- return this.accountId;
- })
- .catch((err) => {
- const detail = (err instanceof api_request_1.HttpError) ? getDetailFromResponse(err.response) : err.message;
- throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, `Failed to determine service account email: ${detail}`);
- });
- }
- buildRequest(urlPath) {
- return {
- method: 'GET',
- url: `http://${GOOGLE_METADATA_SERVICE_HOST}${urlPath}`,
- headers: {
- 'Metadata-Flavor': 'Google',
- },
- httpAgent: this.httpAgent,
- };
- }
- }
- exports.ComputeEngineCredential = ComputeEngineCredential;
- /**
- * Implementation of Credential that gets access tokens from refresh tokens.
- */
- class RefreshTokenCredential {
- /**
- * Creates a new RefreshTokenCredential from the given parameters.
- *
- * @param refreshTokenPathOrObject - Refresh token json object or path to a refresh token
- * (user credentials) json file.
- * @param httpAgent - Optional http.Agent to use when calling the remote token server.
- * @param implicit - An optinal boolean indicating whether this credential was implicitly
- * discovered from the environment, as opposed to being explicitly specified by the developer.
- *
- * @constructor
- */
- constructor(refreshTokenPathOrObject, httpAgent, implicit = false) {
- this.httpAgent = httpAgent;
- this.implicit = implicit;
- this.refreshToken = (typeof refreshTokenPathOrObject === 'string') ?
- RefreshToken.fromPath(refreshTokenPathOrObject)
- : new RefreshToken(refreshTokenPathOrObject);
- this.httpClient = new api_request_1.HttpClient();
- }
- getAccessToken() {
- const postData = 'client_id=' + this.refreshToken.clientId + '&' +
- 'client_secret=' + this.refreshToken.clientSecret + '&' +
- 'refresh_token=' + this.refreshToken.refreshToken + '&' +
- 'grant_type=refresh_token';
- const request = {
- method: 'POST',
- url: `https://${REFRESH_TOKEN_HOST}${REFRESH_TOKEN_PATH}`,
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- },
- data: postData,
- httpAgent: this.httpAgent,
- };
- return requestAccessToken(this.httpClient, request);
- }
- }
- exports.RefreshTokenCredential = RefreshTokenCredential;
- class RefreshToken {
- /*
- * Tries to load a RefreshToken from a path. Throws if the path doesn't exist or the
- * data at the path is invalid.
- */
- static fromPath(filePath) {
- try {
- return new RefreshToken(JSON.parse(fs.readFileSync(filePath, 'utf8')));
- }
- catch (error) {
- // Throw a nicely formed error message if the file contents cannot be parsed
- throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, 'Failed to parse refresh token file: ' + error);
- }
- }
- constructor(json) {
- copyAttr(this, json, 'clientId', 'client_id');
- copyAttr(this, json, 'clientSecret', 'client_secret');
- copyAttr(this, json, 'refreshToken', 'refresh_token');
- copyAttr(this, json, 'type', 'type');
- let errorMessage;
- if (!util.isNonEmptyString(this.clientId)) {
- errorMessage = 'Refresh token must contain a "client_id" property.';
- }
- else if (!util.isNonEmptyString(this.clientSecret)) {
- errorMessage = 'Refresh token must contain a "client_secret" property.';
- }
- else if (!util.isNonEmptyString(this.refreshToken)) {
- errorMessage = 'Refresh token must contain a "refresh_token" property.';
- }
- else if (!util.isNonEmptyString(this.type)) {
- errorMessage = 'Refresh token must contain a "type" property.';
- }
- if (typeof errorMessage !== 'undefined') {
- throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, errorMessage);
- }
- }
- }
- /**
- * Implementation of Credential that uses impersonated service account.
- */
- class ImpersonatedServiceAccountCredential {
- /**
- * Creates a new ImpersonatedServiceAccountCredential from the given parameters.
- *
- * @param impersonatedServiceAccountPathOrObject - Impersonated Service account json object or
- * path to a service account json file.
- * @param httpAgent - Optional http.Agent to use when calling the remote token server.
- * @param implicit - An optional boolean indicating whether this credential was implicitly
- * discovered from the environment, as opposed to being explicitly specified by the developer.
- *
- * @constructor
- */
- constructor(impersonatedServiceAccountPathOrObject, httpAgent, implicit = false) {
- this.httpAgent = httpAgent;
- this.implicit = implicit;
- this.impersonatedServiceAccount = (typeof impersonatedServiceAccountPathOrObject === 'string') ?
- ImpersonatedServiceAccount.fromPath(impersonatedServiceAccountPathOrObject)
- : new ImpersonatedServiceAccount(impersonatedServiceAccountPathOrObject);
- this.httpClient = new api_request_1.HttpClient();
- }
- getAccessToken() {
- const postData = 'client_id=' + this.impersonatedServiceAccount.clientId + '&' +
- 'client_secret=' + this.impersonatedServiceAccount.clientSecret + '&' +
- 'refresh_token=' + this.impersonatedServiceAccount.refreshToken + '&' +
- 'grant_type=refresh_token';
- const request = {
- method: 'POST',
- url: `https://${REFRESH_TOKEN_HOST}${REFRESH_TOKEN_PATH}`,
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- },
- data: postData,
- httpAgent: this.httpAgent,
- };
- return requestAccessToken(this.httpClient, request);
- }
- }
- exports.ImpersonatedServiceAccountCredential = ImpersonatedServiceAccountCredential;
- /**
- * A struct containing the properties necessary to use impersonated service account JSON credentials.
- */
- class ImpersonatedServiceAccount {
- /*
- * Tries to load a ImpersonatedServiceAccount from a path. Throws if the path doesn't exist or the
- * data at the path is invalid.
- */
- static fromPath(filePath) {
- try {
- return new ImpersonatedServiceAccount(JSON.parse(fs.readFileSync(filePath, 'utf8')));
- }
- catch (error) {
- // Throw a nicely formed error message if the file contents cannot be parsed
- throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, 'Failed to parse impersonated service account file: ' + error);
- }
- }
- constructor(json) {
- const sourceCredentials = json['source_credentials'];
- if (sourceCredentials) {
- copyAttr(this, sourceCredentials, 'clientId', 'client_id');
- copyAttr(this, sourceCredentials, 'clientSecret', 'client_secret');
- copyAttr(this, sourceCredentials, 'refreshToken', 'refresh_token');
- copyAttr(this, sourceCredentials, 'type', 'type');
- }
- let errorMessage;
- if (!util.isNonEmptyString(this.clientId)) {
- errorMessage = 'Impersonated Service Account must contain a "source_credentials.client_id" property.';
- }
- else if (!util.isNonEmptyString(this.clientSecret)) {
- errorMessage = 'Impersonated Service Account must contain a "source_credentials.client_secret" property.';
- }
- else if (!util.isNonEmptyString(this.refreshToken)) {
- errorMessage = 'Impersonated Service Account must contain a "source_credentials.refresh_token" property.';
- }
- else if (!util.isNonEmptyString(this.type)) {
- errorMessage = 'Impersonated Service Account must contain a "source_credentials.type" property.';
- }
- if (typeof errorMessage !== 'undefined') {
- throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, errorMessage);
- }
- }
- }
- /**
- * Checks if the given credential was loaded via the application default credentials mechanism. This
- * includes all ComputeEngineCredential instances, and the ServiceAccountCredential and RefreshTokenCredential
- * instances that were loaded from well-known files or environment variables, rather than being explicitly
- * instantiated.
- *
- * @param credential - The credential instance to check.
- */
- function isApplicationDefault(credential) {
- return credential instanceof ComputeEngineCredential ||
- (credential instanceof ServiceAccountCredential && credential.implicit) ||
- (credential instanceof RefreshTokenCredential && credential.implicit) ||
- (credential instanceof ImpersonatedServiceAccountCredential && credential.implicit);
- }
- exports.isApplicationDefault = isApplicationDefault;
- function getApplicationDefault(httpAgent) {
- if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
- return credentialFromFile(process.env.GOOGLE_APPLICATION_CREDENTIALS, httpAgent, false);
- }
- // It is OK to not have this file. If it is present, it must be valid.
- if (GCLOUD_CREDENTIAL_PATH) {
- const credential = credentialFromFile(GCLOUD_CREDENTIAL_PATH, httpAgent, true);
- if (credential)
- return credential;
- }
- return new ComputeEngineCredential(httpAgent);
- }
- exports.getApplicationDefault = getApplicationDefault;
- /**
- * Copies the specified property from one object to another.
- *
- * If no property exists by the given "key", looks for a property identified by "alt", and copies it instead.
- * This can be used to implement behaviors such as "copy property myKey or my_key".
- *
- * @param to - Target object to copy the property into.
- * @param from - Source object to copy the property from.
- * @param key - Name of the property to copy.
- * @param alt - Alternative name of the property to copy.
- */
- function copyAttr(to, from, key, alt) {
- const tmp = from[key] || from[alt];
- if (typeof tmp !== 'undefined') {
- to[key] = tmp;
- }
- }
- /**
- * Obtain a new OAuth2 token by making a remote service call.
- */
- function requestAccessToken(client, request) {
- return client.send(request).then((resp) => {
- const json = resp.data;
- if (!json.access_token || !json.expires_in) {
- throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, `Unexpected response while fetching access token: ${JSON.stringify(json)}`);
- }
- return json;
- }).catch((err) => {
- throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, getErrorMessage(err));
- });
- }
- /**
- * Obtain a new OIDC token by making a remote service call.
- */
- function requestIDToken(client, request) {
- return client.send(request).then((resp) => {
- if (!resp.text) {
- throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, 'Unexpected response while fetching id token: response.text is undefined');
- }
- return resp.text;
- }).catch((err) => {
- throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, getErrorMessage(err));
- });
- }
- /**
- * Constructs a human-readable error message from the given Error.
- */
- function getErrorMessage(err) {
- const detail = (err instanceof api_request_1.HttpError) ? getDetailFromResponse(err.response) : err.message;
- return `Error fetching access token: ${detail}`;
- }
- /**
- * Extracts details from the given HTTP error response, and returns a human-readable description. If
- * the response is JSON-formatted, looks up the error and error_description fields sent by the
- * Google Auth servers. Otherwise returns the entire response payload as the error detail.
- */
- function getDetailFromResponse(response) {
- if (response.isJson() && response.data.error) {
- const json = response.data;
- let detail = json.error;
- if (json.error_description) {
- detail += ' (' + json.error_description + ')';
- }
- return detail;
- }
- return response.text || 'Missing error payload';
- }
- function credentialFromFile(filePath, httpAgent, ignoreMissing) {
- const credentialsFile = readCredentialFile(filePath, ignoreMissing);
- if (typeof credentialsFile !== 'object' || credentialsFile === null) {
- if (ignoreMissing) {
- return null;
- }
- throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, 'Failed to parse contents of the credentials file as an object');
- }
- if (credentialsFile.type === 'service_account') {
- return new ServiceAccountCredential(credentialsFile, httpAgent, true);
- }
- if (credentialsFile.type === 'authorized_user') {
- return new RefreshTokenCredential(credentialsFile, httpAgent, true);
- }
- if (credentialsFile.type === 'impersonated_service_account') {
- return new ImpersonatedServiceAccountCredential(credentialsFile, httpAgent, true);
- }
- throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, 'Invalid contents in the credentials file');
- }
- function readCredentialFile(filePath, ignoreMissing) {
- let fileText;
- try {
- fileText = fs.readFileSync(filePath, 'utf8');
- }
- catch (error) {
- if (ignoreMissing) {
- return null;
- }
- throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, `Failed to read credentials from file ${filePath}: ` + error);
- }
- try {
- return JSON.parse(fileText);
- }
- catch (error) {
- throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, 'Failed to parse contents of the credentials file as an object: ' + error);
- }
- }
|