diff --git a/package-lock.json b/package-lock.json index 1759c62455..c4961706c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@firebase/database-types": "1.0.5", "@types/node": "^22.0.1", "farmhash-modern": "^1.1.0", + "google-auth-library": "^9.14.2", "jsonwebtoken": "^9.0.0", "jwks-rsa": "^3.1.0", "node-forge": "^1.3.1", @@ -2334,7 +2335,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "optional": true, "dependencies": { "debug": "^4.3.4" }, @@ -2923,7 +2923,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "devOptional": true, "funding": [ { "type": "github", @@ -2966,7 +2965,6 @@ "version": "9.1.2", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", - "optional": true, "engines": { "node": "*" } @@ -4372,8 +4370,7 @@ "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "devOptional": true + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "node_modules/extend-shallow": { "version": "3.0.2", @@ -4957,7 +4954,6 @@ "version": "6.6.0", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.6.0.tgz", "integrity": "sha512-bpOZVQV5gthH/jVCSuYuokRo2bTKOcuBiVWpjmTn6C5Agl5zclGfTljuGsQZxwwDBkli+YhZhP4TdlqTnhOezQ==", - "optional": true, "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", @@ -4977,7 +4973,6 @@ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], - "optional": true, "bin": { "uuid": "dist/bin/uuid" } @@ -4986,7 +4981,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", - "optional": true, "dependencies": { "gaxios": "^6.0.0", "json-bigint": "^1.0.0" @@ -5265,10 +5259,10 @@ } }, "node_modules/google-auth-library": { - "version": "9.10.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.10.0.tgz", - "integrity": "sha512-ol+oSa5NbcGdDqA+gZ3G3mev59OHBZksBTxY/tYwjtcp1H/scAFwJfSQU9/1RALoyZ7FslNbke8j4i3ipwlyuQ==", - "optional": true, + "version": "9.14.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.14.2.tgz", + "integrity": "sha512-R+FRIfk1GBo3RdlRYWPdwk8nmtVUOn6+BkDomAC46KoU8kzXzE1HLmOasSCbWUByMMAGkknVF0G5kQ69Vj7dlA==", + "license": "Apache-2.0", "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", @@ -5345,7 +5339,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", - "optional": true, "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" @@ -6149,7 +6142,6 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", - "optional": true, "dependencies": { "agent-base": "^7.0.2", "debug": "4" @@ -6611,7 +6603,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "devOptional": true, "engines": { "node": ">=8" }, @@ -6989,7 +6980,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "optional": true, "dependencies": { "bignumber.js": "^9.0.0" } @@ -7116,7 +7106,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "optional": true, "dependencies": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", @@ -7151,7 +7140,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "optional": true, "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" @@ -7857,7 +7845,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "devOptional": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -10465,8 +10452,7 @@ "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "devOptional": true + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/ts-node": { "version": "10.9.2", @@ -10982,8 +10968,7 @@ "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "devOptional": true + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/websocket-driver": { "version": "0.7.4", @@ -11010,7 +10995,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "devOptional": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -13040,7 +13024,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "optional": true, "requires": { "debug": "^4.3.4" } @@ -13485,8 +13468,7 @@ "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "devOptional": true + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, "bcrypt": { "version": "5.1.1", @@ -13510,8 +13492,7 @@ "bignumber.js": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", - "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", - "optional": true + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==" }, "binary-extensions": { "version": "2.3.0", @@ -14580,8 +14561,7 @@ "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "devOptional": true + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "extend-shallow": { "version": "3.0.2", @@ -15022,7 +15002,6 @@ "version": "6.6.0", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.6.0.tgz", "integrity": "sha512-bpOZVQV5gthH/jVCSuYuokRo2bTKOcuBiVWpjmTn6C5Agl5zclGfTljuGsQZxwwDBkli+YhZhP4TdlqTnhOezQ==", - "optional": true, "requires": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", @@ -15034,8 +15013,7 @@ "uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "optional": true + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" } } }, @@ -15043,7 +15021,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", - "optional": true, "requires": { "gaxios": "^6.0.0", "json-bigint": "^1.0.0" @@ -15250,10 +15227,9 @@ } }, "google-auth-library": { - "version": "9.10.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.10.0.tgz", - "integrity": "sha512-ol+oSa5NbcGdDqA+gZ3G3mev59OHBZksBTxY/tYwjtcp1H/scAFwJfSQU9/1RALoyZ7FslNbke8j4i3ipwlyuQ==", - "optional": true, + "version": "9.14.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.14.2.tgz", + "integrity": "sha512-R+FRIfk1GBo3RdlRYWPdwk8nmtVUOn6+BkDomAC46KoU8kzXzE1HLmOasSCbWUByMMAGkknVF0G5kQ69Vj7dlA==", "requires": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", @@ -15316,7 +15292,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", - "optional": true, "requires": { "gaxios": "^6.0.0", "jws": "^4.0.0" @@ -15965,7 +15940,6 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", - "optional": true, "requires": { "agent-base": "^7.0.2", "debug": "4" @@ -16281,8 +16255,7 @@ "is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "devOptional": true + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" }, "is-string": { "version": "1.0.7", @@ -16564,7 +16537,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "optional": true, "requires": { "bignumber.js": "^9.0.0" } @@ -16680,7 +16652,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "optional": true, "requires": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", @@ -16714,7 +16685,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "optional": true, "requires": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" @@ -17300,7 +17270,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "devOptional": true, "requires": { "whatwg-url": "^5.0.0" } @@ -19300,8 +19269,7 @@ "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "devOptional": true + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "ts-node": { "version": "10.9.2", @@ -19684,8 +19652,7 @@ "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "devOptional": true + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "websocket-driver": { "version": "0.7.4", @@ -19706,7 +19673,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "devOptional": true, "requires": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" diff --git a/package.json b/package.json index 0b29a03372..c61ecd58fa 100644 --- a/package.json +++ b/package.json @@ -209,6 +209,7 @@ "@firebase/database-types": "1.0.5", "@types/node": "^22.0.1", "farmhash-modern": "^1.1.0", + "google-auth-library": "^9.14.2", "jsonwebtoken": "^9.0.0", "jwks-rsa": "^3.1.0", "node-forge": "^1.3.1", diff --git a/src/app/credential-internal.ts b/src/app/credential-internal.ts index 94ac1ea844..2d240cc301 100644 --- a/src/app/credential-internal.ts +++ b/src/app/credential-internal.ts @@ -16,45 +16,92 @@ */ import fs = require('fs'); -import os = require('os'); -import path = require('path'); +import { Credentials as GoogleAuthCredentials, GoogleAuth, Compute, AnyAuthClient } from 'google-auth-library' import { Agent } from 'http'; import { Credential, GoogleOAuthAccessToken } from './credential'; import { AppErrorCodes, FirebaseAppError } from '../utils/error'; -import { HttpClient, HttpRequestConfig, RequestResponseError, RequestResponse } from '../utils/api-request'; import * as util from '../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; +const SCOPES = [ + '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', +]; + +/** + * Implementation of ADC that uses google-auth-library-nodejs. + */ +export class ApplicationDefaultCredential implements Credential { + + private readonly googleAuth: GoogleAuth; + private authClient: AnyAuthClient; + private projectId?: string; + private accountId?: string; + + constructor(httpAgent?: Agent) { + this.googleAuth = new GoogleAuth({ + scopes: SCOPES, + clientOptions: { + transporterOptions: { + agent: httpAgent, + }, + }, + }); } - // On *nix the gcloud cli creates a . dir. - return process.env.HOME && path.resolve(process.env.HOME, '.config'); -})(); + public async getAccessToken(): Promise { + if (!this.authClient) { + this.authClient = await this.googleAuth.getClient(); + } + await this.authClient.getAccessToken(); + const credentials = this.authClient.credentials; + return populateCredential(credentials); + } -const GCLOUD_CREDENTIAL_SUFFIX = 'gcloud/application_default_credentials.json'; -const GCLOUD_CREDENTIAL_PATH = configDir && path.resolve(configDir, GCLOUD_CREDENTIAL_SUFFIX); + public async getProjectId(): Promise { + if (!this.projectId) { + this.projectId = await this.googleAuth.getProjectId(); + } + return Promise.resolve(this.projectId); + } -const REFRESH_TOKEN_HOST = 'www.googleapis.com'; -const REFRESH_TOKEN_PATH = '/oauth2/v4/token'; + public async isComputeEngineCredential(): Promise { + if (!this.authClient) { + this.authClient = await this.googleAuth.getClient(); + } + return Promise.resolve(this.authClient instanceof Compute); + } -const ONE_HOUR_IN_SECONDS = 60 * 60; -const JWT_ALGORITHM = 'RS256'; + /** + * 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. +*/ + public async getIDToken(audience: string): Promise { + if (await this.isComputeEngineCredential()) { + return (this.authClient as Compute).fetchIdToken(audience); + } + else { + throw new FirebaseAppError( + AppErrorCodes.INVALID_CREDENTIAL, + 'Credentials type should be Compute Engine Credentials.', + ); + } + } + + public async getServiceAccountEmail(): Promise { + if (this.accountId) { + return Promise.resolve(this.accountId); + } + + const { client_email: clientEmail } = await this.googleAuth.getCredentials(); + this.accountId = clientEmail ?? ''; + return Promise.resolve(this.accountId); + } +} /** * Implementation of Credential that uses a service account. @@ -65,20 +112,21 @@ export class ServiceAccountCredential implements Credential { public readonly privateKey: string; public readonly clientEmail: string; - private readonly httpClient: HttpClient; + private googleAuth: GoogleAuth; + private authClient: AnyAuthClient | undefined; /** * 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 + * @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( - serviceAccountPathOrObject: string | object, + private readonly serviceAccountPathOrObject: string | object, private readonly httpAgent?: Agent, readonly implicit: boolean = false) { @@ -88,46 +136,26 @@ export class ServiceAccountCredential implements Credential { this.projectId = serviceAccount.projectId; this.privateKey = serviceAccount.privateKey; this.clientEmail = serviceAccount.clientEmail; - this.httpClient = new HttpClient(); } - public getAccessToken(): Promise { - const token = this.createAuthJwt_(); - const postData = 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3A' + - 'grant-type%3Ajwt-bearer&assertion=' + token; - const request: HttpRequestConfig = { - 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); + private getGoogleAuth(): GoogleAuth { + if (this.googleAuth) { + return this.googleAuth; + } + const { auth, client } = populateGoogleAuth(this.serviceAccountPathOrObject, this.httpAgent); + this.googleAuth = auth; + this.authClient = client; + return this.googleAuth; } - // eslint-disable-next-line @typescript-eslint/naming-convention - private createAuthJwt_(): string { - 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, - }); + public async getAccessToken(): Promise { + const googleAuth = this.getGoogleAuth(); + if (this.authClient === undefined) { + this.authClient = await googleAuth.getClient(); + } + await this.authClient.getAccessToken(); + const credentials = this.authClient.credentials; + return populateCredential(credentials); } } @@ -189,96 +217,13 @@ class ServiceAccount { } } -/** - * 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. - */ -export class ComputeEngineCredential implements Credential { - - private readonly httpClient = new HttpClient(); - private readonly httpAgent?: Agent; - private projectId?: string; - private accountId?: string; - - constructor(httpAgent?: Agent) { - this.httpAgent = httpAgent; - } - - public getAccessToken(): Promise { - 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. - */ - public getIDToken(audience: string): Promise { - const request = this.buildRequest(`${GOOGLE_METADATA_SERVICE_IDENTITY_PATH}?audience=${audience}`); - return requestIDToken(this.httpClient, request); - } - - public getProjectId(): Promise { - 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: string = - (err instanceof RequestResponseError) ? getDetailFromResponse(err.response) : err.message; - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - `Failed to determine project ID: ${detail}`); - }); - } - - public getServiceAccountEmail(): Promise { - 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: string = - (err instanceof RequestResponseError) ? getDetailFromResponse(err.response) : err.message; - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - `Failed to determine service account email: ${detail}`); - }); - } - - private buildRequest(urlPath: string): HttpRequestConfig { - return { - method: 'GET', - url: `http://${GOOGLE_METADATA_SERVICE_HOST}${urlPath}`, - headers: { - 'Metadata-Flavor': 'Google', - }, - httpAgent: this.httpAgent, - }; - } -} - /** * Implementation of Credential that gets access tokens from refresh tokens. */ export class RefreshTokenCredential implements Credential { - private readonly refreshToken: RefreshToken; - private readonly httpClient: HttpClient; + private googleAuth: GoogleAuth; + private authClient: AnyAuthClient | undefined; /** * Creates a new RefreshTokenCredential from the given parameters. @@ -292,32 +237,33 @@ export class RefreshTokenCredential implements Credential { * @constructor */ constructor( - refreshTokenPathOrObject: string | object, + private readonly refreshTokenPathOrObject: string | object, private readonly httpAgent?: Agent, readonly implicit: boolean = false) { - this.refreshToken = (typeof refreshTokenPathOrObject === 'string') ? - RefreshToken.fromPath(refreshTokenPathOrObject) - : new RefreshToken(refreshTokenPathOrObject); - this.httpClient = new HttpClient(); + (typeof refreshTokenPathOrObject === 'string') ? + RefreshToken.validateFromPath(refreshTokenPathOrObject) + : RefreshToken.validateFromJSON(refreshTokenPathOrObject); } - public getAccessToken(): Promise { - const postData = - 'client_id=' + this.refreshToken.clientId + '&' + - 'client_secret=' + this.refreshToken.clientSecret + '&' + - 'refresh_token=' + this.refreshToken.refreshToken + '&' + - 'grant_type=refresh_token'; - const request: HttpRequestConfig = { - 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); + private getGoogleAuth(): GoogleAuth { + if (this.googleAuth) { + return this.googleAuth; + } + const { auth, client } = populateGoogleAuth(this.refreshTokenPathOrObject, this.httpAgent); + this.googleAuth = auth; + this.authClient = client; + return this.googleAuth; + } + + public async getAccessToken(): Promise { + const googleAuth = this.getGoogleAuth(); + if (this.authClient === undefined) { + this.authClient = await googleAuth.getClient(); + } + await this.authClient.getAccessToken(); + const credentials = this.authClient.credentials; + return populateCredential(credentials); } } @@ -332,9 +278,9 @@ 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. */ - public static fromPath(filePath: string): RefreshToken { + public static validateFromPath(filePath: string): void { try { - return new RefreshToken(JSON.parse(fs.readFileSync(filePath, 'utf8'))); + RefreshToken.validateFromJSON(JSON.parse(fs.readFileSync(filePath, 'utf8'))); } catch (error) { // Throw a nicely formed error message if the file contents cannot be parsed throw new FirebaseAppError( @@ -344,20 +290,23 @@ class RefreshToken { } } - constructor(json: object) { - copyAttr(this, json, 'clientId', 'client_id'); - copyAttr(this, json, 'clientSecret', 'client_secret'); - copyAttr(this, json, 'refreshToken', 'refresh_token'); - copyAttr(this, json, 'type', 'type'); + public static validateFromJSON(json: object): void { + + const creds = { clientId: '', clientSecret: '', refreshToken: '', type: '' }; + + copyAttr(creds, json, 'clientId', 'client_id'); + copyAttr(creds, json, 'clientSecret', 'client_secret'); + copyAttr(creds, json, 'refreshToken', 'refresh_token'); + copyAttr(creds, json, 'type', 'type'); let errorMessage; - if (!util.isNonEmptyString(this.clientId)) { + if (!util.isNonEmptyString(creds.clientId)) { errorMessage = 'Refresh token must contain a "client_id" property.'; - } else if (!util.isNonEmptyString(this.clientSecret)) { + } else if (!util.isNonEmptyString(creds.clientSecret)) { errorMessage = 'Refresh token must contain a "client_secret" property.'; - } else if (!util.isNonEmptyString(this.refreshToken)) { + } else if (!util.isNonEmptyString(creds.refreshToken)) { errorMessage = 'Refresh token must contain a "refresh_token" property.'; - } else if (!util.isNonEmptyString(this.type)) { + } else if (!util.isNonEmptyString(creds.type)) { errorMessage = 'Refresh token must contain a "type" property.'; } @@ -367,14 +316,13 @@ class RefreshToken { } } - /** * Implementation of Credential that uses impersonated service account. */ export class ImpersonatedServiceAccountCredential implements Credential { - private readonly impersonatedServiceAccount: ImpersonatedServiceAccount; - private readonly httpClient: HttpClient; + private googleAuth: GoogleAuth; + private authClient: AnyAuthClient | undefined; /** * Creates a new ImpersonatedServiceAccountCredential from the given parameters. @@ -388,52 +336,48 @@ export class ImpersonatedServiceAccountCredential implements Credential { * @constructor */ constructor( - impersonatedServiceAccountPathOrObject: string | object, + private readonly impersonatedServiceAccountPathOrObject: string | object, private readonly httpAgent?: Agent, readonly implicit: boolean = false) { - this.impersonatedServiceAccount = (typeof impersonatedServiceAccountPathOrObject === 'string') ? - ImpersonatedServiceAccount.fromPath(impersonatedServiceAccountPathOrObject) - : new ImpersonatedServiceAccount(impersonatedServiceAccountPathOrObject); - this.httpClient = new HttpClient(); + (typeof impersonatedServiceAccountPathOrObject === 'string') ? + ImpersonatedServiceAccount.validateFromPath(impersonatedServiceAccountPathOrObject) + : ImpersonatedServiceAccount.validateFromJSON(impersonatedServiceAccountPathOrObject); } - public getAccessToken(): Promise { - const postData = - 'client_id=' + this.impersonatedServiceAccount.clientId + '&' + - 'client_secret=' + this.impersonatedServiceAccount.clientSecret + '&' + - 'refresh_token=' + this.impersonatedServiceAccount.refreshToken + '&' + - 'grant_type=refresh_token'; - const request: HttpRequestConfig = { - 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); + private getGoogleAuth(): GoogleAuth { + if (this.googleAuth) { + return this.googleAuth; + } + const { auth, client } = populateGoogleAuth(this.impersonatedServiceAccountPathOrObject, this.httpAgent); + this.googleAuth = auth; + this.authClient = client; + return this.googleAuth; + } + + public async getAccessToken(): Promise { + const googleAuth = this.getGoogleAuth(); + if (this.authClient === undefined) { + this.authClient = await googleAuth.getClient(); + } + await this.authClient.getAccessToken(); + const credentials = this.authClient.credentials; + return populateCredential(credentials); } } /** - * A struct containing the properties necessary to use impersonated service account JSON credentials. + * A helper class to validate the properties necessary to use impersonated service account credentials. */ class ImpersonatedServiceAccount { - public readonly clientId: string; - public readonly clientSecret: string; - public readonly refreshToken: string; - public readonly type: string; - /* * Tries to load a ImpersonatedServiceAccount from a path. Throws if the path doesn't exist or the * data at the path is invalid. */ - public static fromPath(filePath: string): ImpersonatedServiceAccount { + public static validateFromPath(filePath: string): void { try { - return new ImpersonatedServiceAccount(JSON.parse(fs.readFileSync(filePath, 'utf8'))); + ImpersonatedServiceAccount.validateFromJSON(JSON.parse(fs.readFileSync(filePath, 'utf8'))); } catch (error) { // Throw a nicely formed error message if the file contents cannot be parsed throw new FirebaseAppError( @@ -443,23 +387,19 @@ class ImpersonatedServiceAccount { } } - constructor(json: object) { - const sourceCredentials = (json as {[key: string]: any})['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'); - } + public static validateFromJSON(json: object): void { + const { + client_id: clientId, client_secret: clientSecret, refresh_token: refreshToken, type + } = (json as { [key: string]: any })['source_credentials']; let errorMessage; - if (!util.isNonEmptyString(this.clientId)) { + if (!util.isNonEmptyString(clientId)) { errorMessage = 'Impersonated Service Account must contain a "source_credentials.client_id" property.'; - } else if (!util.isNonEmptyString(this.clientSecret)) { + } else if (!util.isNonEmptyString(clientSecret)) { errorMessage = 'Impersonated Service Account must contain a "source_credentials.client_secret" property.'; - } else if (!util.isNonEmptyString(this.refreshToken)) { + } else if (!util.isNonEmptyString(refreshToken)) { errorMessage = 'Impersonated Service Account must contain a "source_credentials.refresh_token" property.'; - } else if (!util.isNonEmptyString(this.type)) { + } else if (!util.isNonEmptyString(type)) { errorMessage = 'Impersonated Service Account must contain a "source_credentials.type" property.'; } @@ -470,32 +410,17 @@ class ImpersonatedServiceAccount { } /** - * 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. + * Checks if the given credential was loaded via the application default credentials mechanism. * * @param credential - The credential instance to check. */ export function isApplicationDefault(credential?: Credential): boolean { - return credential instanceof ComputeEngineCredential || - (credential instanceof ServiceAccountCredential && credential.implicit) || - (credential instanceof RefreshTokenCredential && credential.implicit) || - (credential instanceof ImpersonatedServiceAccountCredential && credential.implicit); + return credential instanceof ApplicationDefaultCredential || + (credential instanceof RefreshTokenCredential && credential.implicit); } export function getApplicationDefault(httpAgent?: Agent): Credential { - 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); + return new ApplicationDefaultCredential(httpAgent); } /** @@ -509,7 +434,7 @@ export function getApplicationDefault(httpAgent?: Agent): Credential { * @param key - Name of the property to copy. * @param alt - Alternative name of the property to copy. */ -function copyAttr(to: {[key: string]: any}, from: {[key: string]: any}, key: string, alt: string): void { +function copyAttr(to: { [key: string]: any }, from: { [key: string]: any }, key: string, alt: string): void { const tmp = from[key] || from[alt]; if (typeof tmp !== 'undefined') { to[key] = tmp; @@ -517,114 +442,56 @@ function copyAttr(to: {[key: string]: any}, from: {[key: string]: any}, key: str } /** - * Obtain a new OAuth2 token by making a remote service call. + * Populate google-auth-library GoogleAuth credentials type. */ -function requestAccessToken(client: HttpClient, request: HttpRequestConfig): Promise { - return client.send(request).then((resp) => { - const json = resp.data; - if (!json.access_token || !json.expires_in) { - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - `Unexpected response while fetching access token: ${ JSON.stringify(json) }`, - ); - } - return json; - }).catch((err) => { - throw new FirebaseAppError(AppErrorCodes.INVALID_CREDENTIAL, getErrorMessage(err)); +function populateGoogleAuth(keyFile: string | object, httpAgent?: Agent) + : { auth: GoogleAuth, client: AnyAuthClient | undefined } { + let client: AnyAuthClient | undefined; + const auth = new GoogleAuth({ + scopes: SCOPES, + clientOptions: { + transporterOptions: { + agent: httpAgent, + }, + }, + keyFile: (typeof keyFile === 'string') ? keyFile : undefined, }); -} -/** - * Obtain a new OIDC token by making a remote service call. - */ -function requestIDToken(client: HttpClient, request: HttpRequestConfig): Promise { - return client.send(request).then((resp) => { - if (!resp.text) { + if (typeof keyFile === 'object') { + if (!util.isNonNullObject(keyFile)) { throw new FirebaseAppError( AppErrorCodes.INVALID_CREDENTIAL, - 'Unexpected response while fetching id token: response.text is undefined', + 'Service account must be an object.', ); } - return resp.text; - }).catch((err) => { - throw new FirebaseAppError(AppErrorCodes.INVALID_CREDENTIAL, getErrorMessage(err)); - }); -} - -/** - * Constructs a human-readable error message from the given Error. - */ -function getErrorMessage(err: Error): string { - const detail: string = (err instanceof RequestResponseError) ? getDetailFromResponse(err.response) : err.message; - return `Error fetching access token: ${detail}`; + client = auth.fromJSON(keyFile); + } + return { auth, client }; } /** - * 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. + * Populate GoogleOAuthAccessToken credentials from google-auth-library Credentials type. */ -function getDetailFromResponse(response: RequestResponse): string { - 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 populateCredential(credentials?: GoogleAuthCredentials): GoogleOAuthAccessToken { + const accessToken = credentials?.access_token; + const expiryDate = credentials?.expiry_date; -function credentialFromFile(filePath: string, httpAgent?: Agent, ignoreMissing?: boolean): Credential | null { - const credentialsFile = readCredentialFile(filePath, ignoreMissing); - if (typeof credentialsFile !== 'object' || credentialsFile === null) { - if (ignoreMissing) { return null; } + if (typeof accessToken !== 'string') throw new FirebaseAppError( AppErrorCodes.INVALID_CREDENTIAL, - 'Failed to parse contents of the credentials file as an object', + 'Failed to parse Google auth credential: access_token must be a non empty string.', ); - } - - 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 FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - 'Invalid contents in the credentials file', - ); -} - -function readCredentialFile(filePath: string, ignoreMissing?: boolean): {[key: string]: any} | null { - let fileText: string; - try { - fileText = fs.readFileSync(filePath, 'utf8'); - } catch (error) { - if (ignoreMissing) { - return null; - } - + if (typeof expiryDate !== 'number') throw new FirebaseAppError( AppErrorCodes.INVALID_CREDENTIAL, - `Failed to read credentials from file ${filePath}: ` + error, + 'Failed to parse Google auth credential: Invalid expiry_date.', ); - } - try { - return JSON.parse(fileText); - } catch (error) { - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - 'Failed to parse contents of the credentials file as an object: ' + error, - ); + return { + ...credentials, + access_token: accessToken, + // inverse operation of following + // https://github.com/googleapis/google-auth-library-nodejs/blob/5ed910513451c82e2551777a3e2212964799ef8e/src/auth/baseexternalclient.ts#L446-L446 + expires_in: Math.floor((expiryDate - new Date().getTime()) / 1000), } } diff --git a/src/functions/functions-api-client-internal.ts b/src/functions/functions-api-client-internal.ts index e020a70921..c3c3859c0d 100644 --- a/src/functions/functions-api-client-internal.ts +++ b/src/functions/functions-api-client-internal.ts @@ -24,7 +24,7 @@ import { PrefixedFirebaseError } from '../utils/error'; import * as utils from '../utils/index'; import * as validator from '../utils/validator'; import { TaskOptions } from './functions-api'; -import { ComputeEngineCredential } from '../app/credential-internal'; +import { ApplicationDefaultCredential } from '../app/credential-internal'; const CLOUD_TASKS_API_RESOURCE_PATH = 'projects/{projectId}/locations/{locationId}/queues/{resourceId}/tasks'; const CLOUD_TASKS_API_URL_FORMAT = 'https://cloudtasks.googleapis.com/v2/' + CLOUD_TASKS_API_RESOURCE_PATH; @@ -319,7 +319,8 @@ export class FunctionsApiClient { task.httpRequest.url = functionUrl; // When run from a deployed extension, we should be using ComputeEngineCredentials - if (validator.isNonEmptyString(extensionId) && this.app.options.credential instanceof ComputeEngineCredential) { + if (validator.isNonEmptyString(extensionId) && this.app.options.credential + instanceof ApplicationDefaultCredential && await this.app.options.credential.isComputeEngineCredential()) { const idToken = await this.app.options.credential.getIDToken(functionUrl); task.httpRequest.headers = { ...task.httpRequest.headers, 'Authorization': `Bearer ${idToken}` }; // Don't send httpRequest.oidcToken if we set Authorization header, or Cloud Tasks will overwrite it. diff --git a/src/utils/index.ts b/src/utils/index.ts index 824d41c0f9..ebc405ad8e 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -17,7 +17,7 @@ import { App } from '../app/index'; import { - ServiceAccountCredential, ComputeEngineCredential + ServiceAccountCredential, ApplicationDefaultCredential } from '../app/credential-internal'; import * as validator from './validator'; @@ -113,7 +113,7 @@ export function findProjectId(app: App): Promise { } const credential = app.options.credential; - if (credential instanceof ComputeEngineCredential) { + if (credential instanceof ApplicationDefaultCredential) { return credential.getProjectId(); } @@ -160,7 +160,7 @@ export function findServiceAccountEmail(app: App): Promise { } const credential = app.options.credential; - if (credential instanceof ComputeEngineCredential) { + if (credential instanceof ApplicationDefaultCredential) { return credential.getServiceAccountEmail(); } diff --git a/test/resources/mocks.ts b/test/resources/mocks.ts index 626e9663cb..d4afddd879 100644 --- a/test/resources/mocks.ts +++ b/test/resources/mocks.ts @@ -30,7 +30,7 @@ import * as sinon from 'sinon'; import { AppOptions } from '../../src/firebase-namespace-api'; import { FirebaseApp } from '../../src/app/firebase-app'; import { Credential, GoogleOAuthAccessToken, cert } from '../../src/app/index'; -import { ComputeEngineCredential } from '../../src/app/credential-internal'; +import { ApplicationDefaultCredential } from '../../src/app/credential-internal'; const ALGORITHM = 'RS256' as const; const ONE_HOUR_IN_SECONDS = 60 * 60; @@ -93,7 +93,7 @@ export class MockCredential implements Credential { } } -export class MockComputeEngineCredential extends ComputeEngineCredential { +export class MockComputeEngineCredential extends ApplicationDefaultCredential { public getAccessToken(): Promise { return Promise.resolve({ access_token: 'mock-token', @@ -104,6 +104,10 @@ export class MockComputeEngineCredential extends ComputeEngineCredential { public getIDToken(): Promise { return Promise.resolve('mockIdToken'); } + + public isComputeEngineCredential(): Promise { + return Promise.resolve(true); + } } export function app(altName?: string): FirebaseApp { diff --git a/test/unit/app/credential-internal.spec.ts b/test/unit/app/credential-internal.spec.ts index 93afa1fe9f..f37716e998 100644 --- a/test/unit/app/credential-internal.spec.ts +++ b/test/unit/app/credential-internal.spec.ts @@ -23,24 +23,19 @@ import path = require('path'); import * as _ from 'lodash'; import * as chai from 'chai'; -import * as nock from 'nock'; import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; import * as chaiAsPromised from 'chai-as-promised'; -import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; import { - GoogleOAuthAccessToken, Credential + Credential } from '../../../src/app/index'; import { RefreshTokenCredential, ServiceAccountCredential, - ComputeEngineCredential, getApplicationDefault, isApplicationDefault, ImpersonatedServiceAccountCredential + getApplicationDefault, isApplicationDefault, ImpersonatedServiceAccountCredential, ApplicationDefaultCredential } from '../../../src/app/credential-internal'; -import { HttpClient } from '../../../src/utils/api-request'; -import { Agent } from 'https'; -import { FirebaseAppError } from '../../../src/utils/error'; import { deepCopy } from '../../../src/utils/deep-copy'; chai.should(); @@ -54,12 +49,6 @@ if (!process.env.HOME) { throw new Error('$HOME environment variable must be set to run the tests.'); } const GCLOUD_CREDENTIAL_PATH = path.resolve(process.env.HOME!, '.config', GCLOUD_CREDENTIAL_SUFFIX); -const MOCK_REFRESH_TOKEN_CONFIG = { - client_id: 'test_client_id', - client_secret: 'test_client_secret', - type: 'authorized_user', - refresh_token: 'test_token', -}; const MOCK_IMPERSONATED_TOKEN_CONFIG = { delegates: [], service_account_impersonation_url: '', @@ -72,30 +61,9 @@ const MOCK_IMPERSONATED_TOKEN_CONFIG = { type: 'impersonated_service_account' } -const ONE_HOUR_IN_SECONDS = 60 * 60; -const FIVE_MINUTES_IN_SECONDS = 5 * 60; - - describe('Credential', () => { let mockCertificateObject: any; let oldProcessEnv: NodeJS.ProcessEnv; - let getTokenScope: nock.Scope; - let mockedRequests: nock.Scope[] = []; - - before(() => { - getTokenScope = nock('https://accounts.google.com') - .persist() - .post('/o/oauth2/token') - .reply(200, { - access_token: utils.generateRandomAccessToken(), - token_type: 'Bearer', - expires_in: 3600, - }, { - 'cache-control': 'no-cache, no-store, max-age=0, must-revalidate', - }); - }); - - after(() => getTokenScope.done()); beforeEach(() => { mockCertificateObject = _.clone(mocks.certificateObject); @@ -103,8 +71,6 @@ describe('Credential', () => { }); afterEach(() => { - _.forEach(mockedRequests, (mockedRequest) => mockedRequest.done()); - mockedRequests = []; process.env = oldProcessEnv; }); @@ -212,46 +178,6 @@ describe('Credential', () => { implicit: true, }); }); - - it('should create access tokens', () => { - const c = new ServiceAccountCredential(mockCertificateObject); - return c.getAccessToken().then((token) => { - expect(token.access_token).to.be.a('string').and.to.not.be.empty; - expect(token.expires_in).to.equal(ONE_HOUR_IN_SECONDS); - }); - }); - - describe('Error Handling', () => { - let httpStub: sinon.SinonStub; - before(() => { - httpStub = sinon.stub(HttpClient.prototype, 'send'); - }); - after(() => httpStub.restore()); - - it('should throw an error including error details', () => { - httpStub.rejects(utils.errorFrom({ - error: 'invalid_grant', - error_description: 'reason', - })); - const c = new ServiceAccountCredential(mockCertificateObject); - return expect(c.getAccessToken()).to.be - .rejectedWith('Error fetching access token: invalid_grant (reason)'); - }); - - it('should throw an error including error text payload', () => { - httpStub.rejects(utils.errorFrom('not json')); - const c = new ServiceAccountCredential(mockCertificateObject); - return expect(c.getAccessToken()).to.be - .rejectedWith('Error fetching access token: not json'); - }); - - it('should throw when the success response is malformed', () => { - httpStub.resolves(utils.responseFrom({})); - const c = new ServiceAccountCredential(mockCertificateObject); - return expect(c.getAccessToken()).to.be - .rejectedWith('Unexpected response while fetching access token'); - }); - }); }); describe('RefreshTokenCredential', () => { @@ -298,127 +224,6 @@ describe('Credential', () => { implicit: true, }); }); - - it('should create access tokens', () => { - const scope = nock('https://www.googleapis.com') - .post('/oauth2/v4/token') - .reply(200, { - access_token: 'token', - token_type: 'Bearer', - expires_in: 60 * 60, - }, { - 'cache-control': 'no-cache, no-store, max-age=0, must-revalidate', - }); - mockedRequests.push(scope); - - const c = new RefreshTokenCredential(mocks.refreshToken); - return c.getAccessToken().then((token) => { - expect(token.access_token).to.be.a('string').and.to.not.be.empty; - expect(token.expires_in).to.greaterThan(FIVE_MINUTES_IN_SECONDS); - }); - }); - }); - - describe('ComputeEngineCredential', () => { - let httpStub: sinon.SinonStub; - beforeEach(() => httpStub = sinon.stub(HttpClient.prototype, 'send')); - afterEach(() => httpStub.restore()); - - it('should create access tokens', () => { - const expected: GoogleOAuthAccessToken = { - access_token: 'anAccessToken', - expires_in: 42, - }; - const response = utils.responseFrom(expected); - httpStub.resolves(response); - - const c = new ComputeEngineCredential(); - return c.getAccessToken().then((token) => { - expect(token.access_token).to.equal('anAccessToken'); - expect(token.expires_in).to.equal(42); - expect(httpStub).to.have.been.calledOnce.and.calledWith({ - method: 'GET', - url: 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token', - headers: { 'Metadata-Flavor': 'Google' }, - httpAgent: undefined, - }); - }); - }); - - it('should create id tokens', () => { - const expected = 'an-id-token-encoded'; - const response = utils.responseFrom(expected); - httpStub.resolves(response); - - const c = new ComputeEngineCredential(); - return c.getIDToken('my-audience.cloudfunctions.net').then((token) => { - expect(token).to.equal(expected); - expect(httpStub).to.have.been.calledOnce.and.calledWith({ - method: 'GET', - url: 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=my-audience.cloudfunctions.net', - headers: { 'Metadata-Flavor': 'Google' }, - httpAgent: undefined, - }); - }); - }); - - it('should discover project id', () => { - const expectedProjectId = 'test-project-id'; - const response = utils.responseFrom(expectedProjectId); - httpStub.resolves(response); - - const c = new ComputeEngineCredential(); - return c.getProjectId().then((projectId) => { - expect(projectId).to.equal(expectedProjectId); - expect(httpStub).to.have.been.calledOnce.and.calledWith({ - method: 'GET', - url: 'http://metadata.google.internal/computeMetadata/v1/project/project-id', - headers: { 'Metadata-Flavor': 'Google' }, - httpAgent: undefined, - }); - }); - }); - - it('should cache discovered project id', () => { - const expectedProjectId = 'test-project-id'; - const response = utils.responseFrom(expectedProjectId); - httpStub.resolves(response); - - const c = new ComputeEngineCredential(); - return c.getProjectId() - .then((projectId) => { - expect(projectId).to.equal(expectedProjectId); - return c.getProjectId(); - }) - .then((projectId) => { - expect(projectId).to.equal(expectedProjectId); - expect(httpStub).to.have.been.calledOnce.and.calledWith({ - method: 'GET', - url: 'http://metadata.google.internal/computeMetadata/v1/project/project-id', - headers: { 'Metadata-Flavor': 'Google' }, - httpAgent: undefined, - }); - }); - }); - - it('should reject when the metadata service is not available', () => { - httpStub.rejects(new FirebaseAppError('network-error', 'Failed to connect')); - - const c = new ComputeEngineCredential(); - return c.getProjectId().should.eventually - .rejectedWith('Failed to determine project ID: Failed to connect') - .and.have.property('code', 'app/invalid-credential'); - }); - - it('should reject when the metadata service responds with an error', () => { - const response = utils.errorFrom('Unexpected error'); - httpStub.rejects(response); - - const c = new ComputeEngineCredential(); - return c.getProjectId().should.eventually - .rejectedWith('Failed to determine project ID: Unexpected error') - .and.have.property('code', 'app/invalid-credential'); - }); }); describe('ImpersonatedServiceAccountCredential', () => { @@ -469,25 +274,6 @@ describe('Credential', () => { implicit: true, }); }); - - it('should create access tokens', () => { - const scope = nock('https://www.googleapis.com') - .post('/oauth2/v4/token') - .reply(200, { - access_token: 'token', - token_type: 'Bearer', - expires_in: 60 * 60, - }, { - 'cache-control': 'no-cache, no-store, max-age=0, must-revalidate', - }); - mockedRequests.push(scope); - - const c = new ImpersonatedServiceAccountCredential(MOCK_IMPERSONATED_TOKEN_CONFIG); - return c.getAccessToken().then((token) => { - expect(token.access_token).to.be.a('string').and.to.not.be.empty; - expect(token.expires_in).to.greaterThan(FIVE_MINUTES_IN_SECONDS); - }); - }); }); describe('getApplicationDefault()', () => { @@ -499,40 +285,10 @@ describe('Credential', () => { } }); - it('should return a CertCredential with GOOGLE_APPLICATION_CREDENTIALS set', () => { + it('should return an ApplicationDefaultCredential with GOOGLE_APPLICATION_CREDENTIALS set', () => { process.env.GOOGLE_APPLICATION_CREDENTIALS = path.resolve(__dirname, '../../resources/mock.key.json'); const c = getApplicationDefault(); - expect(c).to.be.an.instanceof(ServiceAccountCredential); - }); - - it('should return a ImpersonatedCredential with impersonated GOOGLE_APPLICATION_CREDENTIALS set', () => { - process.env.GOOGLE_APPLICATION_CREDENTIALS - = path.resolve(__dirname, '../../resources/mock.impersonated_key.json'); - const c = getApplicationDefault(); - expect(c).to.be.an.instanceof(ImpersonatedServiceAccountCredential); - }); - - it('should throw if explicitly pointing to an invalid path', () => { - process.env.GOOGLE_APPLICATION_CREDENTIALS = 'invalidpath'; - expect(() => getApplicationDefault()).to.throw(Error); - }); - - it('should throw if explicitly pointing to an invalid cert file', () => { - fsStub = sinon.stub(fs, 'readFileSync').returns('invalidjson'); - expect(() => getApplicationDefault()).to.throw(Error); - }); - - it('should throw error if type not specified on cert file', () => { - fsStub = sinon.stub(fs, 'readFileSync').returns(JSON.stringify({})); - expect(() => getApplicationDefault()) - .to.throw(Error, 'Invalid contents in the credentials file'); - }); - - it('should throw error if type is unknown on cert file', () => { - fsStub = sinon.stub(fs, 'readFileSync').returns(JSON.stringify({ - type: 'foo', - })); - expect(() => getApplicationDefault()).to.throw(Error, 'Invalid contents in the credentials file'); + expect(c).to.be.an.instanceof(ApplicationDefaultCredential); }); it('should return a RefreshTokenCredential with gcloud login', () => { @@ -547,58 +303,10 @@ describe('Credential', () => { expect((getApplicationDefault())).to.be.an.instanceof(RefreshTokenCredential); }); - it('should throw if a the gcloud login cache is invalid', () => { - delete process.env.GOOGLE_APPLICATION_CREDENTIALS; - fsStub = sinon.stub(fs, 'readFileSync').returns('invalidjson'); - expect(() => getApplicationDefault()).to.throw(Error); - }); - - it('should throw if the credentials file content is not an object', () => { - process.env.GOOGLE_APPLICATION_CREDENTIALS = path.resolve(__dirname, '../../resources/mock.key.json'); - fsStub = sinon.stub(fs, 'readFileSync').returns('2'); - expect(() => getApplicationDefault()).to.throw(Error); - }); - it('should return a MetadataServiceCredential as a last resort', () => { delete process.env.GOOGLE_APPLICATION_CREDENTIALS; fsStub = sinon.stub(fs, 'readFileSync').throws(new Error('no gcloud credential file')); - expect(getApplicationDefault()).to.be.an.instanceof(ComputeEngineCredential); - }); - - it('should create access tokens', () => { - process.env.GOOGLE_APPLICATION_CREDENTIALS = path.resolve(__dirname, '../../resources/mock.key.json'); - const c = getApplicationDefault(); - return c.getAccessToken().then((token) => { - expect(token.access_token).to.be.a('string').and.to.not.be.empty; - expect(token.expires_in).to.equal(ONE_HOUR_IN_SECONDS); - }); - }); - - it('should return a Credential', () => { - process.env.GOOGLE_APPLICATION_CREDENTIALS = path.resolve(__dirname, '../../resources/mock.key.json'); - const c = getApplicationDefault(); - expect(c).to.deep.include({ - projectId: mockCertificateObject.project_id, - clientEmail: mockCertificateObject.client_email, - privateKey: mockCertificateObject.private_key, - }); - }); - - it('should parse valid RefreshTokenCredential if GOOGLE_APPLICATION_CREDENTIALS environment variable ' + - 'points to default refresh token location', () => { - process.env.GOOGLE_APPLICATION_CREDENTIALS = GCLOUD_CREDENTIAL_PATH; - - fsStub = sinon.stub(fs, 'readFileSync').returns(JSON.stringify(MOCK_REFRESH_TOKEN_CONFIG)); - - const c = getApplicationDefault(); - expect(c).is.instanceOf(RefreshTokenCredential); - expect(c).to.have.property('refreshToken').that.includes({ - clientId: MOCK_REFRESH_TOKEN_CONFIG.client_id, - clientSecret: MOCK_REFRESH_TOKEN_CONFIG.client_secret, - refreshToken: MOCK_REFRESH_TOKEN_CONFIG.refresh_token, - type: MOCK_REFRESH_TOKEN_CONFIG.type, - }); - expect(fsStub.alwaysCalledWith(GCLOUD_CREDENTIAL_PATH, 'utf8')).to.be.true; + expect(getApplicationDefault()).to.be.an.instanceof(ApplicationDefaultCredential); }); }); @@ -611,27 +319,10 @@ describe('Credential', () => { } }); - it('should return true for ServiceAccountCredential loaded from GOOGLE_APPLICATION_CREDENTIALS', () => { + it('should return true for ApplicationDefaultCredential loaded from GOOGLE_APPLICATION_CREDENTIALS', () => { process.env.GOOGLE_APPLICATION_CREDENTIALS = path.resolve(__dirname, '../../resources/mock.key.json'); const c = getApplicationDefault(); - expect(c).to.be.an.instanceof(ServiceAccountCredential); - expect(isApplicationDefault(c)).to.be.true; - }); - - it('should return true for RefreshTokenCredential loaded from GOOGLE_APPLICATION_CREDENTIALS', () => { - process.env.GOOGLE_APPLICATION_CREDENTIALS = GCLOUD_CREDENTIAL_PATH; - fsStub = sinon.stub(fs, 'readFileSync').returns(JSON.stringify(MOCK_REFRESH_TOKEN_CONFIG)); - const c = getApplicationDefault(); - expect(c).is.instanceOf(RefreshTokenCredential); - expect(isApplicationDefault(c)).to.be.true; - }); - - it('should return true for ImpersonatedServiceAccountCredential loaded from GOOGLE_APPLICATION_CREDENTIALS', () => { - process.env.GOOGLE_APPLICATION_CREDENTIALS = path.resolve( - __dirname, '../../resources/mock.impersonated_key.json' - ); - const c = getApplicationDefault(); - expect(c).is.instanceOf(ImpersonatedServiceAccountCredential); + expect(c).to.be.an.instanceof(ApplicationDefaultCredential); expect(isApplicationDefault(c)).to.be.true; }); @@ -653,7 +344,7 @@ describe('Credential', () => { delete process.env.GOOGLE_APPLICATION_CREDENTIALS; fsStub = sinon.stub(fs, 'readFileSync').throws(new Error('no gcloud credential file')); const c = getApplicationDefault(); - expect(c).to.be.an.instanceof(ComputeEngineCredential); + expect(c).to.be.an.instanceof(ApplicationDefaultCredential); expect(isApplicationDefault(c)).to.be.true; }); @@ -681,72 +372,4 @@ describe('Credential', () => { expect(isApplicationDefault(c)).to.be.false; }); }); - - describe('HTTP Agent', () => { - const expectedToken = utils.generateRandomAccessToken(); - let stub: sinon.SinonStub; - - beforeEach(() => { - stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({ - access_token: expectedToken, - token_type: 'Bearer', - expires_in: 60 * 60, - })); - }); - - afterEach(() => { - stub.restore(); - }); - - it('ServiceAccountCredential should use the provided HTTP Agent', () => { - const agent = new Agent(); - const c = new ServiceAccountCredential(mockCertificateObject, agent); - return c.getAccessToken().then((token) => { - expect(token.access_token).to.equal(expectedToken); - expect(stub).to.have.been.calledOnce; - expect(stub.args[0][0].httpAgent).to.equal(agent); - }); - }); - - it('RefreshTokenCredential should use the provided HTTP Agent', () => { - const agent = new Agent(); - const c = new RefreshTokenCredential(MOCK_REFRESH_TOKEN_CONFIG, agent); - return c.getAccessToken().then((token) => { - expect(token.access_token).to.equal(expectedToken); - expect(stub).to.have.been.calledOnce; - expect(stub.args[0][0].httpAgent).to.equal(agent); - }); - }); - - it('ComputeEngineCredential should use the provided HTTP Agent', () => { - const agent = new Agent(); - const c = new ComputeEngineCredential(agent); - return c.getAccessToken().then((token) => { - expect(token.access_token).to.equal(expectedToken); - expect(stub).to.have.been.calledOnce; - expect(stub.args[0][0].httpAgent).to.equal(agent); - }); - }); - - it('ApplicationDefaultCredential should use the provided HTTP Agent', () => { - process.env.GOOGLE_APPLICATION_CREDENTIALS = path.resolve(__dirname, '../../resources/mock.key.json'); - const agent = new Agent(); - const c = getApplicationDefault(agent); - return c.getAccessToken().then((token) => { - expect(token.access_token).to.equal(expectedToken); - expect(stub).to.have.been.calledOnce; - expect(stub.args[0][0].httpAgent).to.equal(agent); - }); - }); - - it('ImpersonatedServiceAccountCredential should use the provided HTTP Agent', () => { - const agent = new Agent(); - const c = new ImpersonatedServiceAccountCredential(MOCK_IMPERSONATED_TOKEN_CONFIG, agent); - return c.getAccessToken().then((token) => { - expect(token.access_token).to.equal(expectedToken); - expect(stub).to.have.been.calledOnce; - expect(stub.args[0][0].httpAgent).to.equal(agent); - }); - }); - }); }); diff --git a/test/unit/app/firebase-namespace.spec.ts b/test/unit/app/firebase-namespace.spec.ts index 467854d124..ee3cb951fb 100644 --- a/test/unit/app/firebase-namespace.spec.ts +++ b/test/unit/app/firebase-namespace.spec.ts @@ -77,6 +77,7 @@ import ProjectManagement = projectManagement.ProjectManagement; import RemoteConfig = remoteConfig.RemoteConfig; import SecurityRules = securityRules.SecurityRules; import Storage = storage.Storage; +import { ApplicationDefaultCredential } from '../../../src/app/credential-internal'; chai.should(); chai.use(sinonChai); @@ -760,16 +761,14 @@ describe('FirebaseNamespace', () => { }); }); - it('should create application default credentials from environment', () => { + it('should create application default credentials from environment', async () => { process.env.GOOGLE_APPLICATION_CREDENTIALS = path.resolve(__dirname, '../../resources/mock.key.json'); const mockCertificateObject = mocks.certificateObject; const credential = firebaseNamespace.credential.applicationDefault(); - expect(credential).to.deep.include({ - projectId: mockCertificateObject.project_id, - clientEmail: mockCertificateObject.client_email, - privateKey: mockCertificateObject.private_key, - implicit: true, - }); + const projectId = await (credential as ApplicationDefaultCredential).getProjectId(); + const clientEmail = await (credential as ApplicationDefaultCredential).getServiceAccountEmail(); + expect(projectId).to.eq(mockCertificateObject.project_id); + expect(clientEmail).to.eq(mockCertificateObject.client_email); }); after(clearGlobalAppDefaultCred); diff --git a/test/unit/app/index.spec.ts b/test/unit/app/index.spec.ts index 9de58baf84..896fd2534e 100644 --- a/test/unit/app/index.spec.ts +++ b/test/unit/app/index.spec.ts @@ -31,6 +31,7 @@ import { } from '../../../src/app/index'; import { clearGlobalAppDefaultCred } from '../../../src/app/credential-factory'; import { defaultAppStore } from '../../../src/app/lifecycle'; +import { ApplicationDefaultCredential } from '../../../src/app/credential-internal'; chai.should(); chai.use(chaiAsPromised); @@ -230,14 +231,9 @@ describe('firebase-admin/app', () => { }); it('should create application default credentials from environment', () => { - const mockCertificateObject = mocks.certificateObject; + //const mockCertificateObject = mocks.certificateObject; const credential: Credential = applicationDefault(); - expect(credential).to.deep.include({ - projectId: mockCertificateObject.project_id, - clientEmail: mockCertificateObject.client_email, - privateKey: mockCertificateObject.private_key, - implicit: true, - }); + expect(credential).to.be.instanceOf(ApplicationDefaultCredential); }); it('should cache application default credentials globally', () => { diff --git a/test/unit/firebase.spec.ts b/test/unit/firebase.spec.ts index ca441f852d..3bd63209f8 100644 --- a/test/unit/firebase.spec.ts +++ b/test/unit/firebase.spec.ts @@ -30,6 +30,7 @@ import * as mocks from '../resources/mocks'; import * as firebaseAdmin from '../../src/index'; import { FirebaseApp, FirebaseAppInternals } from '../../src/app/firebase-app'; import { + ApplicationDefaultCredential, RefreshTokenCredential, ServiceAccountCredential, isApplicationDefault } from '../../src/app/credential-internal'; import { defaultAppStore, initializeApp } from '../../src/app/lifecycle'; @@ -129,12 +130,16 @@ describe('Firebase', () => { }); it('should initialize SDK given an application default credential', () => { + getTokenStub.restore(); + getTokenStub = sinon.stub(ApplicationDefaultCredential.prototype, 'getAccessToken').resolves({ + access_token: 'mock-access-token', + expires_in: 3600, + }); const credPath: string | undefined = process.env.GOOGLE_APPLICATION_CREDENTIALS; process.env.GOOGLE_APPLICATION_CREDENTIALS = path.resolve(__dirname, '../resources/mock.key.json'); firebaseAdmin.initializeApp({ credential: firebaseAdmin.credential.applicationDefault(), }); - expect(isApplicationDefault(firebaseAdmin.app().options.credential)).to.be.true; return getAppInternals().getToken().then((token) => { if (typeof credPath === 'undefined') { diff --git a/test/unit/firestore/firestore.spec.ts b/test/unit/firestore/firestore.spec.ts index 19d769c555..146ba2c640 100644 --- a/test/unit/firestore/firestore.spec.ts +++ b/test/unit/firestore/firestore.spec.ts @@ -23,7 +23,8 @@ import { expect } from 'chai'; import * as mocks from '../../resources/mocks'; import { FirebaseApp } from '../../../src/app/firebase-app'; import { - ComputeEngineCredential, RefreshTokenCredential + ApplicationDefaultCredential, + RefreshTokenCredential } from '../../../src/app/credential-internal'; import { FirestoreService, getFirestoreOptions } from '../../../src/firestore/firestore-internal'; import { DEFAULT_DATABASE_ID } from '@google-cloud/firestore/build/src/path'; @@ -48,7 +49,7 @@ describe('Firestore', () => { { name: 'ComputeEngineCredentials', app: mocks.appWithOptions({ - credential: new ComputeEngineCredential(), + credential: new ApplicationDefaultCredential(), }), }, { diff --git a/test/unit/utils/index.spec.ts b/test/unit/utils/index.spec.ts index 7105726e67..5226353107 100644 --- a/test/unit/utils/index.spec.ts +++ b/test/unit/utils/index.spec.ts @@ -26,10 +26,7 @@ import { } from '../../../src/utils/index'; import { isNonEmptyString } from '../../../src/utils/validator'; import { FirebaseApp } from '../../../src/app/firebase-app'; -import { ComputeEngineCredential } from '../../../src/app/credential-internal'; import { HttpClient } from '../../../src/utils/api-request'; -import * as utils from '../utils'; -import { FirebaseAppError } from '../../../src/utils/error'; import { getSdkVersion } from '../../../src/utils/index'; interface Obj { @@ -198,25 +195,6 @@ describe('findProjectId()', () => { return findProjectId(app).should.eventually.equal('env-var-project-id'); }); - it('should return the project ID discovered from the metadata service', () => { - const expectedProjectId = 'test-project-id'; - const response = utils.responseFrom(expectedProjectId); - httpStub.resolves(response); - const app: FirebaseApp = mocks.appWithOptions({ - credential: new ComputeEngineCredential(), - }); - return findProjectId(app).should.eventually.equal(expectedProjectId); - }); - - it('should reject when the metadata service is not available', () => { - httpStub.rejects(new FirebaseAppError('network-error', 'Failed to connect')); - const app: FirebaseApp = mocks.appWithOptions({ - credential: new ComputeEngineCredential(), - }); - return findProjectId(app).should.eventually - .rejectedWith('Failed to determine project ID: Failed to connect'); - }); - it('should return null when project ID is not set and discoverable', () => { const app: FirebaseApp = mocks.mockCredentialApp(); return findProjectId(app).should.eventually.be.null;