UNPKG

23.2 kBJavaScriptView Raw
1/*! firebase-admin v12.0.0 */
2"use strict";
3/*!
4 * @license
5 * Copyright 2020 Google Inc.
6 *
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
10 *
11 * http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 */
19Object.defineProperty(exports, "__esModule", { value: true });
20exports.getApplicationDefault = exports.isApplicationDefault = exports.ImpersonatedServiceAccountCredential = exports.RefreshTokenCredential = exports.ComputeEngineCredential = exports.ServiceAccountCredential = void 0;
21const fs = require("fs");
22const os = require("os");
23const path = require("path");
24const error_1 = require("../utils/error");
25const api_request_1 = require("../utils/api-request");
26const util = require("../utils/validator");
27const GOOGLE_TOKEN_AUDIENCE = 'https://accounts.google.com/o/oauth2/token';
28const GOOGLE_AUTH_TOKEN_HOST = 'accounts.google.com';
29const GOOGLE_AUTH_TOKEN_PATH = '/o/oauth2/token';
30// NOTE: the Google Metadata Service uses HTTP over a vlan
31const GOOGLE_METADATA_SERVICE_HOST = 'metadata.google.internal';
32const GOOGLE_METADATA_SERVICE_TOKEN_PATH = '/computeMetadata/v1/instance/service-accounts/default/token';
33const GOOGLE_METADATA_SERVICE_IDENTITY_PATH = '/computeMetadata/v1/instance/service-accounts/default/identity';
34const GOOGLE_METADATA_SERVICE_PROJECT_ID_PATH = '/computeMetadata/v1/project/project-id';
35const GOOGLE_METADATA_SERVICE_ACCOUNT_ID_PATH = '/computeMetadata/v1/instance/service-accounts/default/email';
36const configDir = (() => {
37 // Windows has a dedicated low-rights location for apps at ~/Application Data
38 const sys = os.platform();
39 if (sys && sys.length >= 3 && sys.substring(0, 3).toLowerCase() === 'win') {
40 return process.env.APPDATA;
41 }
42 // On *nix the gcloud cli creates a . dir.
43 return process.env.HOME && path.resolve(process.env.HOME, '.config');
44})();
45const GCLOUD_CREDENTIAL_SUFFIX = 'gcloud/application_default_credentials.json';
46const GCLOUD_CREDENTIAL_PATH = configDir && path.resolve(configDir, GCLOUD_CREDENTIAL_SUFFIX);
47const REFRESH_TOKEN_HOST = 'www.googleapis.com';
48const REFRESH_TOKEN_PATH = '/oauth2/v4/token';
49const ONE_HOUR_IN_SECONDS = 60 * 60;
50const JWT_ALGORITHM = 'RS256';
51/**
52 * Implementation of Credential that uses a service account.
53 */
54class ServiceAccountCredential {
55 /**
56 * Creates a new ServiceAccountCredential from the given parameters.
57 *
58 * @param serviceAccountPathOrObject - Service account json object or path to a service account json file.
59 * @param httpAgent - Optional http.Agent to use when calling the remote token server.
60 * @param implicit - An optinal boolean indicating whether this credential was implicitly discovered from the
61 * environment, as opposed to being explicitly specified by the developer.
62 *
63 * @constructor
64 */
65 constructor(serviceAccountPathOrObject, httpAgent, implicit = false) {
66 this.httpAgent = httpAgent;
67 this.implicit = implicit;
68 const serviceAccount = (typeof serviceAccountPathOrObject === 'string') ?
69 ServiceAccount.fromPath(serviceAccountPathOrObject)
70 : new ServiceAccount(serviceAccountPathOrObject);
71 this.projectId = serviceAccount.projectId;
72 this.privateKey = serviceAccount.privateKey;
73 this.clientEmail = serviceAccount.clientEmail;
74 this.httpClient = new api_request_1.HttpClient();
75 }
76 getAccessToken() {
77 const token = this.createAuthJwt_();
78 const postData = 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3A' +
79 'grant-type%3Ajwt-bearer&assertion=' + token;
80 const request = {
81 method: 'POST',
82 url: `https://${GOOGLE_AUTH_TOKEN_HOST}${GOOGLE_AUTH_TOKEN_PATH}`,
83 headers: {
84 'Content-Type': 'application/x-www-form-urlencoded',
85 },
86 data: postData,
87 httpAgent: this.httpAgent,
88 };
89 return requestAccessToken(this.httpClient, request);
90 }
91 // eslint-disable-next-line @typescript-eslint/naming-convention
92 createAuthJwt_() {
93 const claims = {
94 scope: [
95 'https://www.googleapis.com/auth/cloud-platform',
96 'https://www.googleapis.com/auth/firebase.database',
97 'https://www.googleapis.com/auth/firebase.messaging',
98 'https://www.googleapis.com/auth/identitytoolkit',
99 'https://www.googleapis.com/auth/userinfo.email',
100 ].join(' '),
101 };
102 // eslint-disable-next-line @typescript-eslint/no-var-requires
103 const jwt = require('jsonwebtoken');
104 // This method is actually synchronous so we can capture and return the buffer.
105 return jwt.sign(claims, this.privateKey, {
106 audience: GOOGLE_TOKEN_AUDIENCE,
107 expiresIn: ONE_HOUR_IN_SECONDS,
108 issuer: this.clientEmail,
109 algorithm: JWT_ALGORITHM,
110 });
111 }
112}
113exports.ServiceAccountCredential = ServiceAccountCredential;
114/**
115 * A struct containing the properties necessary to use service account JSON credentials.
116 */
117class ServiceAccount {
118 static fromPath(filePath) {
119 try {
120 return new ServiceAccount(JSON.parse(fs.readFileSync(filePath, 'utf8')));
121 }
122 catch (error) {
123 // Throw a nicely formed error message if the file contents cannot be parsed
124 throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, 'Failed to parse service account json file: ' + error);
125 }
126 }
127 constructor(json) {
128 if (!util.isNonNullObject(json)) {
129 throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, 'Service account must be an object.');
130 }
131 copyAttr(this, json, 'projectId', 'project_id');
132 copyAttr(this, json, 'privateKey', 'private_key');
133 copyAttr(this, json, 'clientEmail', 'client_email');
134 let errorMessage;
135 if (!util.isNonEmptyString(this.projectId)) {
136 errorMessage = 'Service account object must contain a string "project_id" property.';
137 }
138 else if (!util.isNonEmptyString(this.privateKey)) {
139 errorMessage = 'Service account object must contain a string "private_key" property.';
140 }
141 else if (!util.isNonEmptyString(this.clientEmail)) {
142 errorMessage = 'Service account object must contain a string "client_email" property.';
143 }
144 if (typeof errorMessage !== 'undefined') {
145 throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, errorMessage);
146 }
147 // eslint-disable-next-line @typescript-eslint/no-var-requires
148 const forge = require('node-forge');
149 try {
150 forge.pki.privateKeyFromPem(this.privateKey);
151 }
152 catch (error) {
153 throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, 'Failed to parse private key: ' + error);
154 }
155 }
156}
157/**
158 * Implementation of Credential that gets access tokens from the metadata service available
159 * in the Google Cloud Platform. This authenticates the process as the default service account
160 * of an App Engine instance or Google Compute Engine machine.
161 */
162class ComputeEngineCredential {
163 constructor(httpAgent) {
164 this.httpClient = new api_request_1.HttpClient();
165 this.httpAgent = httpAgent;
166 }
167 getAccessToken() {
168 const request = this.buildRequest(GOOGLE_METADATA_SERVICE_TOKEN_PATH);
169 return requestAccessToken(this.httpClient, request);
170 }
171 /**
172 * getIDToken returns a OIDC token from the compute metadata service
173 * that can be used to make authenticated calls to audience
174 * @param audience the URL the returned ID token will be used to call.
175 */
176 getIDToken(audience) {
177 const request = this.buildRequest(`${GOOGLE_METADATA_SERVICE_IDENTITY_PATH}?audience=${audience}`);
178 return requestIDToken(this.httpClient, request);
179 }
180 getProjectId() {
181 if (this.projectId) {
182 return Promise.resolve(this.projectId);
183 }
184 const request = this.buildRequest(GOOGLE_METADATA_SERVICE_PROJECT_ID_PATH);
185 return this.httpClient.send(request)
186 .then((resp) => {
187 this.projectId = resp.text;
188 return this.projectId;
189 })
190 .catch((err) => {
191 const detail = (err instanceof api_request_1.HttpError) ? getDetailFromResponse(err.response) : err.message;
192 throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, `Failed to determine project ID: ${detail}`);
193 });
194 }
195 getServiceAccountEmail() {
196 if (this.accountId) {
197 return Promise.resolve(this.accountId);
198 }
199 const request = this.buildRequest(GOOGLE_METADATA_SERVICE_ACCOUNT_ID_PATH);
200 return this.httpClient.send(request)
201 .then((resp) => {
202 this.accountId = resp.text;
203 return this.accountId;
204 })
205 .catch((err) => {
206 const detail = (err instanceof api_request_1.HttpError) ? getDetailFromResponse(err.response) : err.message;
207 throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, `Failed to determine service account email: ${detail}`);
208 });
209 }
210 buildRequest(urlPath) {
211 return {
212 method: 'GET',
213 url: `http://${GOOGLE_METADATA_SERVICE_HOST}${urlPath}`,
214 headers: {
215 'Metadata-Flavor': 'Google',
216 },
217 httpAgent: this.httpAgent,
218 };
219 }
220}
221exports.ComputeEngineCredential = ComputeEngineCredential;
222/**
223 * Implementation of Credential that gets access tokens from refresh tokens.
224 */
225class RefreshTokenCredential {
226 /**
227 * Creates a new RefreshTokenCredential from the given parameters.
228 *
229 * @param refreshTokenPathOrObject - Refresh token json object or path to a refresh token
230 * (user credentials) json file.
231 * @param httpAgent - Optional http.Agent to use when calling the remote token server.
232 * @param implicit - An optinal boolean indicating whether this credential was implicitly
233 * discovered from the environment, as opposed to being explicitly specified by the developer.
234 *
235 * @constructor
236 */
237 constructor(refreshTokenPathOrObject, httpAgent, implicit = false) {
238 this.httpAgent = httpAgent;
239 this.implicit = implicit;
240 this.refreshToken = (typeof refreshTokenPathOrObject === 'string') ?
241 RefreshToken.fromPath(refreshTokenPathOrObject)
242 : new RefreshToken(refreshTokenPathOrObject);
243 this.httpClient = new api_request_1.HttpClient();
244 }
245 getAccessToken() {
246 const postData = 'client_id=' + this.refreshToken.clientId + '&' +
247 'client_secret=' + this.refreshToken.clientSecret + '&' +
248 'refresh_token=' + this.refreshToken.refreshToken + '&' +
249 'grant_type=refresh_token';
250 const request = {
251 method: 'POST',
252 url: `https://${REFRESH_TOKEN_HOST}${REFRESH_TOKEN_PATH}`,
253 headers: {
254 'Content-Type': 'application/x-www-form-urlencoded',
255 },
256 data: postData,
257 httpAgent: this.httpAgent,
258 };
259 return requestAccessToken(this.httpClient, request);
260 }
261}
262exports.RefreshTokenCredential = RefreshTokenCredential;
263class RefreshToken {
264 /*
265 * Tries to load a RefreshToken from a path. Throws if the path doesn't exist or the
266 * data at the path is invalid.
267 */
268 static fromPath(filePath) {
269 try {
270 return new RefreshToken(JSON.parse(fs.readFileSync(filePath, 'utf8')));
271 }
272 catch (error) {
273 // Throw a nicely formed error message if the file contents cannot be parsed
274 throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, 'Failed to parse refresh token file: ' + error);
275 }
276 }
277 constructor(json) {
278 copyAttr(this, json, 'clientId', 'client_id');
279 copyAttr(this, json, 'clientSecret', 'client_secret');
280 copyAttr(this, json, 'refreshToken', 'refresh_token');
281 copyAttr(this, json, 'type', 'type');
282 let errorMessage;
283 if (!util.isNonEmptyString(this.clientId)) {
284 errorMessage = 'Refresh token must contain a "client_id" property.';
285 }
286 else if (!util.isNonEmptyString(this.clientSecret)) {
287 errorMessage = 'Refresh token must contain a "client_secret" property.';
288 }
289 else if (!util.isNonEmptyString(this.refreshToken)) {
290 errorMessage = 'Refresh token must contain a "refresh_token" property.';
291 }
292 else if (!util.isNonEmptyString(this.type)) {
293 errorMessage = 'Refresh token must contain a "type" property.';
294 }
295 if (typeof errorMessage !== 'undefined') {
296 throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, errorMessage);
297 }
298 }
299}
300/**
301 * Implementation of Credential that uses impersonated service account.
302 */
303class ImpersonatedServiceAccountCredential {
304 /**
305 * Creates a new ImpersonatedServiceAccountCredential from the given parameters.
306 *
307 * @param impersonatedServiceAccountPathOrObject - Impersonated Service account json object or
308 * path to a service account json file.
309 * @param httpAgent - Optional http.Agent to use when calling the remote token server.
310 * @param implicit - An optional boolean indicating whether this credential was implicitly
311 * discovered from the environment, as opposed to being explicitly specified by the developer.
312 *
313 * @constructor
314 */
315 constructor(impersonatedServiceAccountPathOrObject, httpAgent, implicit = false) {
316 this.httpAgent = httpAgent;
317 this.implicit = implicit;
318 this.impersonatedServiceAccount = (typeof impersonatedServiceAccountPathOrObject === 'string') ?
319 ImpersonatedServiceAccount.fromPath(impersonatedServiceAccountPathOrObject)
320 : new ImpersonatedServiceAccount(impersonatedServiceAccountPathOrObject);
321 this.httpClient = new api_request_1.HttpClient();
322 }
323 getAccessToken() {
324 const postData = 'client_id=' + this.impersonatedServiceAccount.clientId + '&' +
325 'client_secret=' + this.impersonatedServiceAccount.clientSecret + '&' +
326 'refresh_token=' + this.impersonatedServiceAccount.refreshToken + '&' +
327 'grant_type=refresh_token';
328 const request = {
329 method: 'POST',
330 url: `https://${REFRESH_TOKEN_HOST}${REFRESH_TOKEN_PATH}`,
331 headers: {
332 'Content-Type': 'application/x-www-form-urlencoded',
333 },
334 data: postData,
335 httpAgent: this.httpAgent,
336 };
337 return requestAccessToken(this.httpClient, request);
338 }
339}
340exports.ImpersonatedServiceAccountCredential = ImpersonatedServiceAccountCredential;
341/**
342 * A struct containing the properties necessary to use impersonated service account JSON credentials.
343 */
344class ImpersonatedServiceAccount {
345 /*
346 * Tries to load a ImpersonatedServiceAccount from a path. Throws if the path doesn't exist or the
347 * data at the path is invalid.
348 */
349 static fromPath(filePath) {
350 try {
351 return new ImpersonatedServiceAccount(JSON.parse(fs.readFileSync(filePath, 'utf8')));
352 }
353 catch (error) {
354 // Throw a nicely formed error message if the file contents cannot be parsed
355 throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, 'Failed to parse impersonated service account file: ' + error);
356 }
357 }
358 constructor(json) {
359 const sourceCredentials = json['source_credentials'];
360 if (sourceCredentials) {
361 copyAttr(this, sourceCredentials, 'clientId', 'client_id');
362 copyAttr(this, sourceCredentials, 'clientSecret', 'client_secret');
363 copyAttr(this, sourceCredentials, 'refreshToken', 'refresh_token');
364 copyAttr(this, sourceCredentials, 'type', 'type');
365 }
366 let errorMessage;
367 if (!util.isNonEmptyString(this.clientId)) {
368 errorMessage = 'Impersonated Service Account must contain a "source_credentials.client_id" property.';
369 }
370 else if (!util.isNonEmptyString(this.clientSecret)) {
371 errorMessage = 'Impersonated Service Account must contain a "source_credentials.client_secret" property.';
372 }
373 else if (!util.isNonEmptyString(this.refreshToken)) {
374 errorMessage = 'Impersonated Service Account must contain a "source_credentials.refresh_token" property.';
375 }
376 else if (!util.isNonEmptyString(this.type)) {
377 errorMessage = 'Impersonated Service Account must contain a "source_credentials.type" property.';
378 }
379 if (typeof errorMessage !== 'undefined') {
380 throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, errorMessage);
381 }
382 }
383}
384/**
385 * Checks if the given credential was loaded via the application default credentials mechanism. This
386 * includes all ComputeEngineCredential instances, and the ServiceAccountCredential and RefreshTokenCredential
387 * instances that were loaded from well-known files or environment variables, rather than being explicitly
388 * instantiated.
389 *
390 * @param credential - The credential instance to check.
391 */
392function isApplicationDefault(credential) {
393 return credential instanceof ComputeEngineCredential ||
394 (credential instanceof ServiceAccountCredential && credential.implicit) ||
395 (credential instanceof RefreshTokenCredential && credential.implicit) ||
396 (credential instanceof ImpersonatedServiceAccountCredential && credential.implicit);
397}
398exports.isApplicationDefault = isApplicationDefault;
399function getApplicationDefault(httpAgent) {
400 if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
401 return credentialFromFile(process.env.GOOGLE_APPLICATION_CREDENTIALS, httpAgent, false);
402 }
403 // It is OK to not have this file. If it is present, it must be valid.
404 if (GCLOUD_CREDENTIAL_PATH) {
405 const credential = credentialFromFile(GCLOUD_CREDENTIAL_PATH, httpAgent, true);
406 if (credential)
407 return credential;
408 }
409 return new ComputeEngineCredential(httpAgent);
410}
411exports.getApplicationDefault = getApplicationDefault;
412/**
413 * Copies the specified property from one object to another.
414 *
415 * If no property exists by the given "key", looks for a property identified by "alt", and copies it instead.
416 * This can be used to implement behaviors such as "copy property myKey or my_key".
417 *
418 * @param to - Target object to copy the property into.
419 * @param from - Source object to copy the property from.
420 * @param key - Name of the property to copy.
421 * @param alt - Alternative name of the property to copy.
422 */
423function copyAttr(to, from, key, alt) {
424 const tmp = from[key] || from[alt];
425 if (typeof tmp !== 'undefined') {
426 to[key] = tmp;
427 }
428}
429/**
430 * Obtain a new OAuth2 token by making a remote service call.
431 */
432function requestAccessToken(client, request) {
433 return client.send(request).then((resp) => {
434 const json = resp.data;
435 if (!json.access_token || !json.expires_in) {
436 throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, `Unexpected response while fetching access token: ${JSON.stringify(json)}`);
437 }
438 return json;
439 }).catch((err) => {
440 throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, getErrorMessage(err));
441 });
442}
443/**
444 * Obtain a new OIDC token by making a remote service call.
445 */
446function requestIDToken(client, request) {
447 return client.send(request).then((resp) => {
448 if (!resp.text) {
449 throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, 'Unexpected response while fetching id token: response.text is undefined');
450 }
451 return resp.text;
452 }).catch((err) => {
453 throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, getErrorMessage(err));
454 });
455}
456/**
457 * Constructs a human-readable error message from the given Error.
458 */
459function getErrorMessage(err) {
460 const detail = (err instanceof api_request_1.HttpError) ? getDetailFromResponse(err.response) : err.message;
461 return `Error fetching access token: ${detail}`;
462}
463/**
464 * Extracts details from the given HTTP error response, and returns a human-readable description. If
465 * the response is JSON-formatted, looks up the error and error_description fields sent by the
466 * Google Auth servers. Otherwise returns the entire response payload as the error detail.
467 */
468function getDetailFromResponse(response) {
469 if (response.isJson() && response.data.error) {
470 const json = response.data;
471 let detail = json.error;
472 if (json.error_description) {
473 detail += ' (' + json.error_description + ')';
474 }
475 return detail;
476 }
477 return response.text || 'Missing error payload';
478}
479function credentialFromFile(filePath, httpAgent, ignoreMissing) {
480 const credentialsFile = readCredentialFile(filePath, ignoreMissing);
481 if (typeof credentialsFile !== 'object' || credentialsFile === null) {
482 if (ignoreMissing) {
483 return null;
484 }
485 throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, 'Failed to parse contents of the credentials file as an object');
486 }
487 if (credentialsFile.type === 'service_account') {
488 return new ServiceAccountCredential(credentialsFile, httpAgent, true);
489 }
490 if (credentialsFile.type === 'authorized_user') {
491 return new RefreshTokenCredential(credentialsFile, httpAgent, true);
492 }
493 if (credentialsFile.type === 'impersonated_service_account') {
494 return new ImpersonatedServiceAccountCredential(credentialsFile, httpAgent, true);
495 }
496 throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, 'Invalid contents in the credentials file');
497}
498function readCredentialFile(filePath, ignoreMissing) {
499 let fileText;
500 try {
501 fileText = fs.readFileSync(filePath, 'utf8');
502 }
503 catch (error) {
504 if (ignoreMissing) {
505 return null;
506 }
507 throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, `Failed to read credentials from file ${filePath}: ` + error);
508 }
509 try {
510 return JSON.parse(fileText);
511 }
512 catch (error) {
513 throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, 'Failed to parse contents of the credentials file as an object: ' + error);
514 }
515}