UNPKG

firebase-admin

Version:
323 lines (322 loc) 14 kB
/*! firebase-admin v5.11.0 */ "use strict"; /*! * Copyright 2017 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 }); var jwt = require("jsonwebtoken"); var forge = require("node-forge"); // Use untyped import syntax for Node built-ins var fs = require("fs"); var os = require("os"); var http = require("http"); var path = require("path"); var https = require("https"); var error_1 = require("../utils/error"); var GOOGLE_TOKEN_AUDIENCE = 'https://accounts.google.com/o/oauth2/token'; var GOOGLE_AUTH_TOKEN_HOST = 'accounts.google.com'; var GOOGLE_AUTH_TOKEN_PATH = '/o/oauth2/token'; var GOOGLE_AUTH_TOKEN_PORT = 443; // NOTE: the Google Metadata Service uses HTTP over a vlan var GOOGLE_METADATA_SERVICE_HOST = 'metadata.google.internal'; var GOOGLE_METADATA_SERVICE_PATH = '/computeMetadata/v1beta1/instance/service-accounts/default/token'; var configDir = (function () { // Windows has a dedicated low-rights location for apps at ~/Application Data var 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'); })(); var GCLOUD_CREDENTIAL_SUFFIX = 'gcloud/application_default_credentials.json'; var GCLOUD_CREDENTIAL_PATH = configDir && path.resolve(configDir, GCLOUD_CREDENTIAL_SUFFIX); var REFRESH_TOKEN_HOST = 'www.googleapis.com'; var REFRESH_TOKEN_PORT = 443; var REFRESH_TOKEN_PATH = '/oauth2/v4/token'; var ONE_HOUR_IN_SECONDS = 60 * 60; var JWT_ALGORITHM = 'RS256'; function copyAttr(to, from, key, alt) { var tmp = from[key] || from[alt]; if (typeof tmp !== 'undefined') { to[key] = tmp; } } var RefreshToken = /** @class */ (function () { function RefreshToken(json) { copyAttr(this, json, 'clientId', 'client_id'); copyAttr(this, json, 'clientSecret', 'client_secret'); copyAttr(this, json, 'refreshToken', 'refresh_token'); copyAttr(this, json, 'type', 'type'); var errorMessage; if (typeof this.clientId !== 'string' || !this.clientId) { errorMessage = 'Refresh token must contain a "client_id" property.'; } else if (typeof this.clientSecret !== 'string' || !this.clientSecret) { errorMessage = 'Refresh token must contain a "client_secret" property.'; } else if (typeof this.refreshToken !== 'string' || !this.refreshToken) { errorMessage = 'Refresh token must contain a "refresh_token" property.'; } else if (typeof this.type !== 'string' || !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); } } /* * Tries to load a RefreshToken from a path. If the path is not present, returns null. * Throws if data at the path is invalid. */ RefreshToken.fromPath = function (filePath) { var jsonString; try { jsonString = fs.readFileSync(filePath, 'utf8'); } catch (ignored) { // Ignore errors if the file is not present, as this is sometimes an expected condition return null; } try { return new RefreshToken(JSON.parse(jsonString)); } 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); } }; return RefreshToken; }()); exports.RefreshToken = RefreshToken; /** * A struct containing the properties necessary to use service-account JSON credentials. */ var Certificate = /** @class */ (function () { function Certificate(json) { if (typeof json !== 'object' || json === null) { throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, 'Certificate object must be an object.'); } copyAttr(this, json, 'projectId', 'project_id'); copyAttr(this, json, 'privateKey', 'private_key'); copyAttr(this, json, 'clientEmail', 'client_email'); var errorMessage; if (typeof this.privateKey !== 'string' || !this.privateKey) { errorMessage = 'Certificate object must contain a string "private_key" property.'; } else if (typeof this.clientEmail !== 'string' || !this.clientEmail) { errorMessage = 'Certificate object must contain a string "client_email" property.'; } if (typeof errorMessage !== 'undefined') { throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, errorMessage); } try { forge.pki.privateKeyFromPem(this.privateKey); } catch (error) { throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, 'Failed to parse private key: ' + error); } } Certificate.fromPath = function (filePath) { // Node bug encountered in v6.x. fs.readFileSync hangs when path is a 0 or 1. if (typeof filePath !== 'string') { throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, 'Failed to parse certificate key file: TypeError: path must be a string'); } try { return new Certificate(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 certificate key file: ' + error); } }; return Certificate; }()); exports.Certificate = Certificate; /** * A wrapper around the http and https request libraries to simplify & promisify JSON requests. * TODO(inlined): Create a type for "transit". */ function requestAccessToken(transit, options, data) { return new Promise(function (resolve, reject) { var req = transit.request(options, function (res) { var buffers = []; res.on('data', function (buffer) { return buffers.push(buffer); }); res.on('end', function () { try { var json = JSON.parse(Buffer.concat(buffers).toString()); if (json.error) { var errorMessage = 'Error fetching access token: ' + json.error; if (json.error_description) { errorMessage += ' (' + json.error_description + ')'; } reject(new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, errorMessage)); } else if (!json.access_token || !json.expires_in) { reject(new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, "Unexpected response while fetching access token: " + JSON.stringify(json))); } else { resolve(json); } } catch (err) { reject(new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_CREDENTIAL, "Failed to parse access token response: " + err.toString())); } }); }); req.on('error', reject); if (data) { req.write(data); } req.end(); }); } /** * Implementation of Credential that uses a service account certificate. */ var CertCredential = /** @class */ (function () { function CertCredential(serviceAccountPathOrObject) { this.certificate_ = (typeof serviceAccountPathOrObject === 'string') ? Certificate.fromPath(serviceAccountPathOrObject) : new Certificate(serviceAccountPathOrObject); } CertCredential.prototype.getAccessToken = function () { var token = this.createAuthJwt_(); var postData = 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3A' + 'grant-type%3Ajwt-bearer&assertion=' + token; var options = { method: 'POST', host: GOOGLE_AUTH_TOKEN_HOST, port: GOOGLE_AUTH_TOKEN_PORT, path: GOOGLE_AUTH_TOKEN_PATH, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': postData.length, }, }; return requestAccessToken(https, options, postData); }; CertCredential.prototype.getCertificate = function () { return this.certificate_; }; CertCredential.prototype.createAuthJwt_ = function () { var 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(' '), }; // This method is actually synchronous so we can capture and return the buffer. return jwt.sign(claims, this.certificate_.privateKey, { audience: GOOGLE_TOKEN_AUDIENCE, expiresIn: ONE_HOUR_IN_SECONDS, issuer: this.certificate_.clientEmail, algorithm: JWT_ALGORITHM, }); }; return CertCredential; }()); exports.CertCredential = CertCredential; /** * Implementation of Credential that gets access tokens from refresh tokens. */ var RefreshTokenCredential = /** @class */ (function () { function RefreshTokenCredential(refreshTokenPathOrObject) { this.refreshToken_ = (typeof refreshTokenPathOrObject === 'string') ? RefreshToken.fromPath(refreshTokenPathOrObject) : new RefreshToken(refreshTokenPathOrObject); } RefreshTokenCredential.prototype.getAccessToken = function () { var postData = 'client_id=' + this.refreshToken_.clientId + '&' + 'client_secret=' + this.refreshToken_.clientSecret + '&' + 'refresh_token=' + this.refreshToken_.refreshToken + '&' + 'grant_type=refresh_token'; var options = { method: 'POST', host: REFRESH_TOKEN_HOST, port: REFRESH_TOKEN_PORT, path: REFRESH_TOKEN_PATH, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': postData.length, }, }; return requestAccessToken(https, options, postData); }; RefreshTokenCredential.prototype.getCertificate = function () { return null; }; return RefreshTokenCredential; }()); exports.RefreshTokenCredential = RefreshTokenCredential; /** * 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. */ var MetadataServiceCredential = /** @class */ (function () { function MetadataServiceCredential() { } MetadataServiceCredential.prototype.getAccessToken = function () { var options = { method: 'GET', host: GOOGLE_METADATA_SERVICE_HOST, path: GOOGLE_METADATA_SERVICE_PATH, headers: { 'Content-Length': 0, }, }; return requestAccessToken(http, options); }; MetadataServiceCredential.prototype.getCertificate = function () { return null; }; return MetadataServiceCredential; }()); exports.MetadataServiceCredential = MetadataServiceCredential; /** * ApplicationDefaultCredential implements the process for loading credentials as * described in https://developers.google.com/identity/protocols/application-default-credentials */ var ApplicationDefaultCredential = /** @class */ (function () { function ApplicationDefaultCredential() { if (process.env.GOOGLE_APPLICATION_CREDENTIALS) { var serviceAccount = Certificate.fromPath(process.env.GOOGLE_APPLICATION_CREDENTIALS); this.credential_ = new CertCredential(serviceAccount); return; } // It is OK to not have this file. If it is present, it must be valid. var refreshToken = RefreshToken.fromPath(GCLOUD_CREDENTIAL_PATH); if (refreshToken) { this.credential_ = new RefreshTokenCredential(refreshToken); return; } this.credential_ = new MetadataServiceCredential(); } ApplicationDefaultCredential.prototype.getAccessToken = function () { return this.credential_.getAccessToken(); }; ApplicationDefaultCredential.prototype.getCertificate = function () { return this.credential_.getCertificate(); }; // Used in testing to verify we are delegating to the correct implementation. ApplicationDefaultCredential.prototype.getCredential = function () { return this.credential_; }; return ApplicationDefaultCredential; }()); exports.ApplicationDefaultCredential = ApplicationDefaultCredential;