From 98b2b1a884be60e40ff2e8c2dbd93310beb4447e Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Fri, 24 Nov 2017 11:37:34 -0800 Subject: [PATCH] chore: switch to axios (#182) --- package-lock.json | 587 ++++--------- package.json | 7 +- src/auth/authclient.ts | 9 +- src/auth/computeclient.ts | 118 ++- src/auth/credentials.ts | 9 +- src/auth/googleauth.ts | 627 +++++++------- src/auth/jwtaccess.ts | 177 ++-- src/auth/jwtclient.ts | 206 ++--- src/auth/oauth2client.ts | 587 +++++++------ src/auth/refreshclient.ts | 118 +-- src/options.ts | 35 + src/transporters.ts | 145 ++-- test/test.compute.ts | 168 ++-- test/test.googleauth.ts | 1674 ++++++++++++++++--------------------- test/test.iam.ts | 10 +- test/test.jwt.ts | 86 +- test/test.jwtaccess.ts | 18 +- test/test.loginticket.ts | 1 - test/test.oauth2.ts | 37 +- test/test.refresh.ts | 2 - test/test.transporters.ts | 54 +- 21 files changed, 2126 insertions(+), 2549 deletions(-) create mode 100644 src/options.ts diff --git a/package-lock.json b/package-lock.json index 6cd56ec4..d40fb3d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,22 +4,13 @@ "lockfileVersion": 1, "requires": true, "dependencies": { - "@types/form-data": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", - "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", - "dev": true, - "requires": { - "@types/node": "8.0.53" - } - }, "@types/jws": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@types/jws/-/jws-3.1.0.tgz", "integrity": "sha512-D6jyvhGumSgLF2r/iEw9mL27/9hQ15aPeh6nJYVVcPmweZ/7dqKdXSO560lTZRgXQxh/dAvQ+r1+1AAP17Qx8g==", "dev": true, "requires": { - "@types/node": "8.0.53" + "@types/node": "8.0.52" } }, "@types/lodash": { @@ -37,15 +28,6 @@ "@types/lodash": "4.14.85" } }, - "@types/lodash.merge": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/@types/lodash.merge/-/lodash.merge-4.6.3.tgz", - "integrity": "sha512-bOCCutkbehfCeiSqJO5XcVKjsJvX28dgvjhs4aMmBAHAsOy2oOJQwHuIZ0elI1E94qQIdEsCvz8oLgUumd6teA==", - "dev": true, - "requires": { - "@types/lodash": "4.14.85" - } - }, "@types/mocha": { "version": "2.2.44", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.44.tgz", @@ -58,42 +40,21 @@ "integrity": "sha1-H75b3suUPBCad4VT+k0kAcuTlLQ=", "dev": true, "requires": { - "@types/node": "8.0.53" + "@types/node": "8.0.52" } }, "@types/node": { - "version": "8.0.53", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.53.tgz", - "integrity": "sha512-54Dm6NwYeiSQmRB1BLXKr5GELi0wFapR1npi8bnZhEcu84d/yQKqnwwXQ56hZ0RUbTG6L5nqDZaN3dgByQXQRQ==", + "version": "8.0.52", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.52.tgz", + "integrity": "sha512-wOU/VRodnI/4Chxuu6R6bcyN9aE3rztO0i8R76PZO7+DxTXWy60nseGN4ujspucmxrfj5mzgCYPXiXqrD6KC3Q==", "dev": true }, - "@types/request": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.0.7.tgz", - "integrity": "sha512-hlkmO+M7Xxx13YZwqQtXGC03KxcYbqmDQg3oHMRVODfSk2u0BELK7RfTNROisvrLRGOYdGoZPpnHwaPoKrPMbA==", - "dev": true, - "requires": { - "@types/form-data": "2.2.1", - "@types/node": "8.0.53" - } - }, "abbrev": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", "dev": true }, - "ajv": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.4.0.tgz", - "integrity": "sha1-MtHPCNvIDEMvQm8S4QslEfa0ZHQ=", - "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.0.0", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.3.1" - } - }, "align-text": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", @@ -156,12 +117,14 @@ "asn1": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true }, "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true }, "assertion-error": { "version": "1.0.2", @@ -178,17 +141,14 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true }, "aws4": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", + "dev": true }, "axios": { "version": "0.17.1", @@ -225,19 +185,12 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, "optional": true, "requires": { "tweetnacl": "0.14.5" } }, - "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", - "requires": { - "hoek": "4.2.0" - } - }, "boxen": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.2.2.tgz", @@ -279,12 +232,6 @@ "supports-color": "4.5.0" } }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", - "dev": true - }, "supports-color": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", @@ -324,11 +271,10 @@ "dev": true }, "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", - "dev": true, - "optional": true + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true }, "camelcase-keys": { "version": "2.1.0", @@ -338,14 +284,6 @@ "requires": { "camelcase": "2.1.1", "map-obj": "1.0.1" - }, - "dependencies": { - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", - "dev": true - } } }, "capture-stack-trace": { @@ -354,11 +292,6 @@ "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=", "dev": true }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, "center-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", @@ -394,12 +327,6 @@ "supports-color": "2.0.0" } }, - "chardet": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.0.tgz", - "integrity": "sha1-C74TVaxE16PtSpJXB8TvcPgZD2w=", - "dev": true - }, "clang-format": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/clang-format/-/clang-format-1.1.0.tgz", @@ -453,11 +380,6 @@ } } }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -483,6 +405,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "dev": true, "requires": { "delayed-stream": "1.0.0" } @@ -511,20 +434,13 @@ "unique-string": "1.0.0", "write-file-atomic": "2.3.0", "xdg-basedir": "3.0.0" - }, - "dependencies": { - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true - } } }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true }, "coveralls": { "version": "2.13.3", @@ -696,36 +612,6 @@ "lru-cache": "4.1.1", "shebang-command": "1.2.0", "which": "1.3.0" - }, - "dependencies": { - "lru-cache": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", - "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", - "dev": true, - "requires": { - "pseudomap": "1.0.2", - "yallist": "2.1.2" - } - } - } - }, - "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", - "requires": { - "boom": "5.2.0" - }, - "dependencies": { - "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", - "requires": { - "hoek": "4.2.0" - } - } } }, "crypto-random-string": { @@ -747,6 +633,7 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, "requires": { "assert-plus": "1.0.0" } @@ -804,7 +691,14 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "diff": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.4.0.tgz", + "integrity": "sha512-QpVuMTEoJMF7cKzi6bvWhRulU1fZqZnvyVQgNhPaxxuTYwyjn/j1v9falseQ/uXWwPnO56RBfwtg4h/EQXmucA==", + "dev": true }, "dot-prop": { "version": "4.2.0", @@ -825,6 +719,7 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, "optional": true, "requires": { "jsbn": "0.1.1" @@ -865,18 +760,6 @@ "esutils": "2.0.2", "optionator": "0.8.2", "source-map": "0.2.0" - }, - "dependencies": { - "source-map": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", - "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", - "dev": true, - "optional": true, - "requires": { - "amdefine": "1.0.1" - } - } } }, "esprima": { @@ -897,36 +780,43 @@ "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", "dev": true }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + } + }, "extend": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true }, "external-editor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.1.0.tgz", - "integrity": "sha512-E44iT5QVOUJBKij4IIV3uvxuNlbKS38Tw1HiupxEIHPv9qtC2PrDYohbXV5U+1jnfIXttny8gUhj+oZvflFlzA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.0.5.tgz", + "integrity": "sha512-Msjo64WT5W+NhOpQXh0nOHm+n0RfU1QUwDnKYvJ8dEJ8zlwLrqXNTv5mSUTJpepf41PDJGyhueTw2vNZW+Fr/w==", "dev": true, "requires": { - "chardet": "0.4.0", "iconv-lite": "0.4.19", + "jschardet": "1.6.0", "tmp": "0.0.33" } }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-deep-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", - "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true }, "fast-levenshtein": { "version": "2.0.6", @@ -974,17 +864,8 @@ "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", - "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.17" - } + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true }, "fs.realpath": { "version": "1.0.0", @@ -1023,6 +904,7 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, "requires": { "assert-plus": "1.0.0" } @@ -1042,9 +924,9 @@ } }, "global-dirs": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", - "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.0.tgz", + "integrity": "sha1-ENNAOeDfBCcuJizyQiT3IJQ0308=", "dev": true, "requires": { "ini": "1.3.4" @@ -1078,6 +960,12 @@ "url-parse-lax": "1.0.0" } }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, "graceful-readlink": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", @@ -1139,12 +1027,6 @@ "supports-color": "4.5.0" } }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", - "dev": true - }, "supports-color": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", @@ -1179,20 +1061,6 @@ } } }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "requires": { - "ajv": "5.4.0", - "har-schema": "2.0.0" - } - }, "has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", @@ -1203,49 +1071,23 @@ } }, "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", "dev": true }, - "hawk": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", - "requires": { - "boom": "4.3.1", - "cryptiles": "3.1.2", - "hoek": "4.2.0", - "sntp": "2.1.0" - } - }, "he": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", "dev": true }, - "hoek": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", - "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" - }, "hosted-git-info": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==", "dev": true }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.13.1" - } - }, "iconv-lite": { "version": "0.4.19", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", @@ -1305,7 +1147,7 @@ "chalk": "2.3.0", "cli-cursor": "2.1.0", "cli-width": "2.2.0", - "external-editor": "2.1.0", + "external-editor": "2.0.5", "figures": "2.0.0", "lodash": "4.17.4", "mute-stream": "0.0.7", @@ -1343,18 +1185,6 @@ "supports-color": "4.5.0" } }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", - "dev": true - }, - "lodash": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", - "dev": true - }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", @@ -1416,7 +1246,7 @@ "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", "dev": true, "requires": { - "global-dirs": "0.1.1", + "global-dirs": "0.1.0", "is-path-inside": "1.0.0" } }, @@ -1486,7 +1316,8 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true }, "is-utf8": { "version": "0.2.1", @@ -1503,7 +1334,8 @@ "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true }, "istanbul": { "version": "0.4.5", @@ -1540,6 +1372,12 @@ "path-is-absolute": "1.0.1" } }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, "resolve": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", @@ -1577,22 +1415,26 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, "optional": true }, + "jschardet": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-1.6.0.tgz", + "integrity": "sha512-xYuhvQ7I9PDJIGBWev9xm0+SMSed3ZDBAmvVjbFR1ZRLAF+vlXcQu6cRI9uAlj81rzikElRVteehwV7DuX2ZmQ==", + "dev": true + }, "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true }, "json3": { "version": "3.3.2", @@ -1610,6 +1452,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -1698,29 +1541,20 @@ "strip-bom": "2.0.0" }, "dependencies": { - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true - }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "0.2.1" - } } } }, + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", + "dev": true + }, "lodash._baseassign": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", @@ -1794,11 +1628,6 @@ "lodash.isarray": "3.0.4" } }, - "lodash.merge": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.0.tgz", - "integrity": "sha1-aYhLoUSsM/5plzemCG3v+t0PicU=" - }, "log-driver": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.5.tgz", @@ -1827,6 +1656,16 @@ "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=", "dev": true }, + "lru-cache": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", + "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", + "dev": true, + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, "make-dir": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.1.0.tgz", @@ -1868,12 +1707,14 @@ "mime-db": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", - "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" + "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=", + "dev": true }, "mime-types": { "version": "2.1.17", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "dev": true, "requires": { "mime-db": "1.30.0" } @@ -1965,6 +1806,12 @@ "path-is-absolute": "1.0.1" } }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, "supports-color": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", @@ -2002,20 +1849,6 @@ "propagate": "0.4.0", "qs": "6.5.1", "semver": "5.4.1" - }, - "dependencies": { - "lodash": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", - "dev": true - }, - "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", - "dev": true - } } }, "node-forge": { @@ -2040,7 +1873,7 @@ "requires": { "hosted-git-info": "2.5.0", "is-builtin-module": "1.0.0", - "semver": "4.3.6", + "semver": "5.4.1", "validate-npm-package-license": "3.0.1" } }, @@ -2062,7 +1895,8 @@ "oauth-sign": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true }, "object-assign": { "version": "4.1.1", @@ -2148,14 +1982,6 @@ "registry-auth-token": "3.3.1", "registry-url": "3.1.0", "semver": "5.4.1" - }, - "dependencies": { - "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", - "dev": true - } } }, "parse-json": { @@ -2211,12 +2037,6 @@ "pinkie-promise": "2.0.1" }, "dependencies": { - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true - }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -2225,11 +2045,6 @@ } } }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", @@ -2277,12 +2092,14 @@ "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true }, "qs": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", + "dev": true }, "rc": { "version": "1.2.2", @@ -2361,35 +2178,6 @@ "is-finite": "1.0.2" } }, - "request": { - "version": "2.83.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", - "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", - "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.3.1", - "har-validator": "5.0.3", - "hawk": "6.0.2", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.17", - "oauth-sign": "0.8.2", - "performance-now": "2.1.0", - "qs": "6.5.1", - "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.3", - "tunnel-agent": "0.6.0", - "uuid": "3.1.0" - } - }, "resolve": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", @@ -2458,9 +2246,9 @@ "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" }, "semver": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", - "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", "dev": true }, "semver-diff": { @@ -2470,14 +2258,6 @@ "dev": true, "requires": { "semver": "5.4.1" - }, - "dependencies": { - "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", - "dev": true - } } }, "shebang-command": { @@ -2501,20 +2281,16 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true }, - "sntp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, "requires": { - "hoek": "4.2.0" + "amdefine": "1.0.1" } }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - }, "source-map-support": { "version": "0.4.18", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", @@ -2522,6 +2298,14 @@ "dev": true, "requires": { "source-map": "0.5.7" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } } }, "spdx-correct": { @@ -2555,6 +2339,7 @@ "version": "1.13.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "dev": true, "requires": { "asn1": "0.2.3", "assert-plus": "1.0.0", @@ -2596,7 +2381,8 @@ "stringstream": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", + "dev": true }, "strip-ansi": { "version": "3.0.1", @@ -2607,6 +2393,15 @@ "ansi-regex": "2.1.1" } }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + }, "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", @@ -2641,23 +2436,6 @@ "dev": true, "requires": { "execa": "0.7.0" - }, - "dependencies": { - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "dev": true, - "requires": { - "cross-spawn": "5.1.0", - "get-stream": "3.0.0", - "is-stream": "1.1.0", - "npm-run-path": "2.0.2", - "p-finally": "1.0.0", - "signal-exit": "3.0.2", - "strip-eof": "1.0.0" - } - } } }, "through": { @@ -2685,6 +2463,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", + "dev": true, "requires": { "punycode": "1.4.1" } @@ -2740,24 +2519,6 @@ "supports-color": "4.5.0" } }, - "diff": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.4.0.tgz", - "integrity": "sha512-QpVuMTEoJMF7cKzi6bvWhRulU1fZqZnvyVQgNhPaxxuTYwyjn/j1v9falseQ/uXWwPnO56RBfwtg4h/EQXmucA==", - "dev": true - }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", - "dev": true - }, - "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", - "dev": true - }, "supports-color": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", @@ -2778,18 +2539,11 @@ "tslib": "1.8.0" } }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "5.1.1" - } - }, "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, "optional": true }, "type-check": { @@ -2893,12 +2647,6 @@ "supports-color": "4.5.0" } }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", - "dev": true - }, "supports-color": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", @@ -2922,7 +2670,8 @@ "uuid": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==", + "dev": true }, "validate-npm-package-license": { "version": "3.0.1", @@ -2938,6 +2687,7 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, "requires": { "assert-plus": "1.0.0", "core-util-is": "1.0.2", @@ -3012,14 +2762,6 @@ "graceful-fs": "4.1.11", "imurmurhash": "0.1.4", "signal-exit": "3.0.2" - }, - "dependencies": { - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true - } } }, "xdg-basedir": { @@ -3051,6 +2793,15 @@ "cliui": "2.1.0", "decamelize": "1.2.0", "window-size": "0.1.0" + }, + "dependencies": { + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true, + "optional": true + } } } } diff --git a/package.json b/package.json index 68f2ac46..b341c41f 100644 --- a/package.json +++ b/package.json @@ -21,19 +21,16 @@ ], "dependencies": { "gtoken": "^2.0.1", + "axios": "^0.17.1", "jws": "^3.1.4", - "lodash.isstring": "^4.0.1", - "lodash.merge": "^4.6.0", - "request": "^2.81.0" + "lodash.isstring": "^4.0.1" }, "devDependencies": { "@types/jws": "^3.1.0", "@types/lodash.isstring": "^4.0.2", - "@types/lodash.merge": "^4.6.2", "@types/mocha": "^2.2.41", "@types/nock": "^8.2.1", "@types/node": "^8.0.47", - "@types/request": "2.0.7", "coveralls": "^2.13.0", "gts": "^0.5.1", "istanbul": "^0.4.5", diff --git a/src/auth/authclient.ts b/src/auth/authclient.ts index f8578d84..1a45dc0d 100644 --- a/src/auth/authclient.ts +++ b/src/auth/authclient.ts @@ -14,10 +14,8 @@ * limitations under the License. */ -import * as request from 'request'; - +import {AxiosPromise, AxiosRequestConfig} from 'axios'; import {DefaultTransporter} from '../transporters'; - import {Credentials} from './credentials'; export abstract class AuthClient { @@ -25,8 +23,7 @@ export abstract class AuthClient { credentials: Credentials; /** - * Provides an alternative request - * implementations with auth credentials. + * Provides an alternative Axios request implementation with auth credentials */ - abstract request(opts: request.Options): request.Request|void; + abstract request(opts: AxiosRequestConfig): AxiosPromise; } diff --git a/src/auth/computeclient.ts b/src/auth/computeclient.ts index 7878df06..6920674a 100644 --- a/src/auth/computeclient.ts +++ b/src/auth/computeclient.ts @@ -14,19 +14,11 @@ * limitations under the License. */ -import * as request from 'request'; +import {AxiosError, AxiosPromise, AxiosRequestConfig, AxiosResponse} from 'axios'; -import {BodyResponseCallback, RequestError} from './../transporters'; -import {OAuth2Client} from './oauth2client'; - -export interface Token { - expires_in: number; - expiry_date: number; -} - -export declare type RefreshTokenCallback = - (err?: Error|null, token?: Token, - response?: request.RequestResponse|null) => void; +import {RequestError} from './../transporters'; +import {CredentialRequest, Credentials} from './credentials'; +import {GetTokenResponse, OAuth2Client} from './oauth2client'; export class Compute extends OAuth2Client { /** @@ -51,7 +43,7 @@ export class Compute extends OAuth2Client { /** * Indicates whether the credential requires scopes to be created by calling * createdScoped before use. - * @return {object} The cloned instance. + * @return Boolean indicating if scope is required. */ createScopedRequired() { // On compute engine, scopes are specified at the compute instance's @@ -62,64 +54,60 @@ export class Compute extends OAuth2Client { /** * Refreshes the access token. - * @param {object=} ignored_ - * @param {function=} callback Optional callback. + * @param refreshToken Unused parameter */ - protected refreshToken(ignored: null, callback?: BodyResponseCallback): - request.Request { - const uri = this.opts.tokenUrl || Compute._GOOGLE_OAUTH2_TOKEN_URL; + protected async refreshToken(refreshToken?: string| + null): Promise { + const url = this.opts.tokenUrl || Compute._GOOGLE_OAUTH2_TOKEN_URL; + let res: AxiosResponse|null = null; // request for new token - return this.transporter.request( - {method: 'GET', uri, json: true}, (err, body, response) => { - const token = body as Token; - if (!err && token && token.expires_in) { - token.expiry_date = - ((new Date()).getTime() + (token.expires_in * 1000)); - delete token.expires_in; - } - if (callback) { - callback(err, token, response); - } - }); + try { + res = await this.transporter.request({url}); + } catch (e) { + e.message = 'Could not refresh access token.'; + throw e; + } + console.log(res.data); + const tokens = res.data as Credentials; + if (res.data && res.data.expires_in) { + tokens.expiry_date = + ((new Date()).getTime() + (res.data.expires_in * 1000)); + delete (tokens as CredentialRequest).expires_in; + } + return {tokens, res}; } - /** - * Inserts a helpful error message guiding the user toward fixing common auth - * issues. - * @param {object} err Error result. - * @param {object} result The result. - * @param {object} response The HTTP response. - * @param {Function} callback The callback. - */ - protected postRequest( - err: Error, result: {}, response: request.RequestResponse, - callback: BodyResponseCallback) { - if (response && response.statusCode) { - let helpfulMessage = null; - if (response.statusCode === 403) { - helpfulMessage = - 'A Forbidden error was returned while attempting to retrieve an access ' + - 'token for the Compute Engine built-in service account. This may be because the Compute ' + - 'Engine instance does not have the correct permission scopes specified.'; - } else if (response.statusCode === 404) { - helpfulMessage = - 'A Not Found error was returned while attempting to retrieve an access' + - 'token for the Compute Engine built-in service account. This may be because the Compute ' + - 'Engine instance does not have any permission scopes specified.'; - } - if (helpfulMessage) { - if (err && err.message) { - helpfulMessage += ' ' + err.message; - } - if (err) { - err.message = helpfulMessage; - } else { - err = new Error(helpfulMessage); - (err as RequestError).code = response.statusCode; + protected requestAsync(opts: AxiosRequestConfig, retry = false): + AxiosPromise { + return super.requestAsync(opts, retry).catch(e => { + const res = (e as AxiosError).response; + if (res && res.status) { + let helpfulMessage = null; + if (res.status === 403) { + helpfulMessage = + 'A Forbidden error was returned while attempting to retrieve an access ' + + 'token for the Compute Engine built-in service account. This may be because the Compute ' + + 'Engine instance does not have the correct permission scopes specified.'; + } else if (res.status === 404) { + helpfulMessage = + 'A Not Found error was returned while attempting to retrieve an access' + + 'token for the Compute Engine built-in service account. This may be because the Compute ' + + 'Engine instance does not have any permission scopes specified.'; + } + if (helpfulMessage) { + if (e && e.message && !retry) { + helpfulMessage += ' ' + e.message; + } + if (e) { + e.message = helpfulMessage; + } else { + e = new Error(helpfulMessage); + (e as RequestError).code = res.status.toString(); + } } } - } - callback(err, result, response); + throw e; + }); } } diff --git a/src/auth/credentials.ts b/src/auth/credentials.ts index fb948647..00f1e021 100644 --- a/src/auth/credentials.ts +++ b/src/auth/credentials.ts @@ -15,10 +15,17 @@ */ export interface Credentials { + refresh_token?: string|null; + expiry_date?: number|null; + access_token?: string|null; + token_type?: string|null; +} + +export interface CredentialRequest { refresh_token?: string; - expiry_date?: number; access_token?: string; token_type?: string; + expires_in?: number; } export interface JWTInput { diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 372c4bc5..65d288ed 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -21,10 +21,9 @@ import * as path from 'path'; import * as stream from 'stream'; import * as util from 'util'; -import {BodyResponseCallback, DefaultTransporter, Transporter} from '../transporters'; - +import {DefaultTransporter, Transporter} from '../transporters'; import {Compute} from './computeclient'; -import {Credentials, JWTInput} from './credentials'; +import {JWTInput} from './credentials'; import {IAMAuth} from './iam'; import {JWTAccess} from './jwtaccess'; import {JWT} from './jwtclient'; @@ -32,7 +31,20 @@ import {OAuth2Client} from './oauth2client'; import {UserRefreshClient} from './refreshclient'; export interface ProjectIdCallback { - (err?: Error|null, projectId?: string): void; + (err?: Error|null, projectId?: string|null): void; +} + +export interface CredentialCallback { + (err: Error|null, result?: UserRefreshClient|JWT): void; +} + +export interface ADCCallback { + (err: Error|null, credential?: OAuth2Client, projectId?: string|null): void; +} + +export interface ADCResponse { + credential: OAuth2Client; + projectId: string|null; } export interface CredentialBody { @@ -40,6 +52,10 @@ export interface CredentialBody { private_key?: string; } +interface CredentialResult { + default: {email: string;}; +} + export class GoogleAuth { transporter: Transporter; @@ -56,7 +72,6 @@ export class GoogleAuth { return this.checkIsGCE; } - // The _ rule goes away in the next gts release private _cachedProjectId: string; // Note: this properly is only public to satisify unit tests. @@ -108,9 +123,22 @@ export class GoogleAuth { /** * Obtains the default project ID for the application.. - * @param {function=} callback Optional callback. + * @param callback Optional callback + * @returns Promise that resolves with project Id (if used without callback) */ - getDefaultProjectId(callback: ProjectIdCallback) { + getDefaultProjectId(): Promise; + getDefaultProjectId(callback: ProjectIdCallback): void; + getDefaultProjectId(callback?: ProjectIdCallback): Promise|void { + if (callback) { + this.getDefaultProjectIdAsync() + .then(r => callback(null, r)) + .catch(callback); + } else { + return this.getDefaultProjectIdAsync(); + } + } + + private async getDefaultProjectIdAsync(): Promise { // In implicit case, supports three environments. In order of precedence, // the implicit environments are: // @@ -123,182 +151,160 @@ export class GoogleAuth { // implemented yet) if (this._cachedProjectId) { - if (callback) setImmediate(callback, null, this._cachedProjectId); - } else { - const myCallback = (err?: Error|null, projectId?: string) => { - if (!err && projectId) { - this._cachedProjectId = projectId; - } - if (callback) setImmediate(callback, err, projectId); - }; - - // environment variable - if (this._getProductionProjectId(myCallback)) { - return; - } - - // json file - this.getFileProjectId((err?: Error|null, projectId?: string) => { - if (err || projectId) { - myCallback(err, projectId); - return; - } + return this._cachedProjectId; + } - // Google Cloud SDK default project id - this.getDefaultServiceProjectId( - (err2?: Error|null, projectId2?: string) => { - if (err2 || projectId2) { - myCallback(err2, projectId2); - return; - } - - // Get project ID from Compute Engine metadata server - this.getGCEProjectId(myCallback); - }); - }); + const projectId = this.getProductionProjectId() || + await this.getFileProjectId() || + await this.getDefaultServiceProjectId() || await this.getGCEProjectId(); + if (projectId) { + this._cachedProjectId = projectId; } + return projectId; } /** * Run the Google Cloud SDK command that prints the default project ID - * @param {function} callback Callback. - * @api private */ - _getSDKDefaultProjectId( - callback: - (error: Error|null, stdout: string, stderr: string|null) => void) { - exec('gcloud -q config list core/project --format=json', callback); + _getSDKDefaultProjectId(): + Promise<{stdout: string | null, stderr: string|null}> { + // TODO: make this a proper async function + return new Promise((resolve, reject) => { + exec( + 'gcloud -q config list core/project --format=json', + (err, stdout, stderr) => { + if (err) { + reject(err); + } else { + resolve({stdout, stderr}); + } + }); + }); } /** - * Obtains the default service-level credentials for the application.. + * Obtains the default service-level credentials for the application. * @param {function=} callback Optional callback. + * @returns Promise that resolves with the ADCResponse (if no callback was + * passed). */ - getApplicationDefault( - callback?: - (err: Error, credential: JWT|UserRefreshClient, - projectId: string) => void) { + getApplicationDefault(): Promise; + getApplicationDefault(callback: ADCCallback): void; + getApplicationDefault(callback?: ADCCallback): void|Promise { + if (callback) { + this.getApplicationDefaultAsync() + .then(r => callback(null, r.credential, r.projectId)) + .catch(callback); + } else { + return this.getApplicationDefaultAsync(); + } + } + + private async getApplicationDefaultAsync(): Promise { // If we've already got a cached credential, just return it. if (this.cachedCredential) { - if (callback) { - setImmediate( - callback, null, this.cachedCredential, this._cachedProjectId); - } - } else { - // Inject our own callback routine, which will cache the credential once - // it's been created. It also allows us to ensure that the ultimate - // callback is always async. - const myCallback = (err?: Error|null, result?: OAuth2Client) => { - if (!err && result) { - this.cachedCredential = result; - this.getDefaultProjectId((err2, projectId) => { - if (callback) setImmediate(callback, null, result, projectId); - }); - } else { - if (callback) setImmediate(callback, err, result); - } + return { + credential: this.cachedCredential as JWT | UserRefreshClient, + projectId: this._cachedProjectId }; + } - // Check for the existence of a local environment variable pointing to the - // location of the credential file. This is typically used in local - // developer scenarios. - if (this._tryGetApplicationCredentialsFromEnvironmentVariable( - myCallback)) { - return; - } + let credential: OAuth2Client|null; + let projectId: string|null; + // Check for the existence of a local environment variable pointing to the + // location of the credential file. This is typically used in local + // developer scenarios. + credential = + await this._tryGetApplicationCredentialsFromEnvironmentVariable(); + if (credential) { + this.cachedCredential = credential; + projectId = await this.getDefaultProjectId(); + return {credential, projectId}; + } - // Look in the well-known credential file location. - if (this._tryGetApplicationCredentialsFromWellKnownFile(myCallback)) { - return; - } + // Look in the well-known credential file location. + credential = await this._tryGetApplicationCredentialsFromWellKnownFile(); + if (credential) { + this.cachedCredential = credential; + projectId = await this.getDefaultProjectId(); + return {credential, projectId}; + } + try { // Determine if we're running on GCE. - this._checkIsGCE((err, gce) => { - if (gce) { - // For GCE, just return a default ComputeClient. It will take care of - // the rest. - myCallback(null, new Compute()); - } else if (err) { - myCallback(new Error( - 'Unexpected error while acquiring application default ' + - 'credentials: ' + err.message)); - } else { - // We failed to find the default credentials. Bail out with an error. - myCallback(new Error( - 'Could not load the default credentials. Browse to ' + - 'https://developers.google.com/accounts/docs/application-default-credentials for ' + - 'more information.')); - } - }); + const gce = await this._checkIsGCE(); + if (gce) { + // For GCE, just return a default ComputeClient. It will take care of + // the rest. + // TODO: cache the result + return {projectId: null, credential: new Compute()}; + } else { + // We failed to find the default credentials. Bail out with an error. + throw new Error( + 'Could not load the default credentials. Browse to https://developers.google.com/accounts/docs/application-default-credentials for more information.'); + } + } catch (e) { + throw new Error( + 'Unexpected error while acquiring application default credentials: ' + + e.message); } } /** * Determines whether the auth layer is running on Google Compute Engine. - * @param {function=} callback The callback. + * @returns A promise that resolves with the boolean. * @api private */ - _checkIsGCE(callback: (err: Error|null, isGCE?: boolean) => void) { + async _checkIsGCE(): Promise { if (this.checkIsGCE !== undefined) { - setImmediate(() => { - callback(null, this.checkIsGCE); - }); - } else { - if (!this.transporter) { - this.transporter = new DefaultTransporter(); + return this.checkIsGCE; + } + if (!this.transporter) { + this.transporter = new DefaultTransporter(); + } + try { + const res = await this.transporter.request( + {url: 'http://metadata.google.internal'}); + this.checkIsGCE = + res && res.headers && res.headers['metadata-flavor'] === 'Google'; + } catch (e) { + if ((e as NodeJS.ErrnoException).code !== 'ENOTFOUND') { + // Unexpected error occurred. TODO(ofrobots): retry if this was a + // transient error. + throw e; } - this.transporter.request( - {method: 'GET', uri: 'http://metadata.google.internal', json: true}, - (err, body, res) => { - if (err) { - if ((err as NodeJS.ErrnoException).code !== 'ENOTFOUND') { - // Unexpected error occurred. TODO(ofrobots): retry if this was - // a transient error. - return callback(err); - } - this.checkIsGCE = false; - return callback(null, this.checkIsGCE); - } - this.checkIsGCE = res !== null && res !== undefined && - res.headers && res.headers['metadata-flavor'] === 'Google'; - callback(null, this.checkIsGCE); - }); + this.checkIsGCE = false; } + return this.checkIsGCE; } /** * Attempts to load default credentials from the environment variable path.. - * @param {function=} callback Optional callback. - * @return {boolean} Returns true if the callback has been executed; false otherwise. + * @returns Promise that resolves with the OAuth2Client or null. * @api private */ - _tryGetApplicationCredentialsFromEnvironmentVariable( - callback?: (err?: Error|null, result?: JWT|UserRefreshClient) => void) { + async _tryGetApplicationCredentialsFromEnvironmentVariable(): + Promise { const credentialsPath = this._getEnv('GOOGLE_APPLICATION_CREDENTIALS'); if (!credentialsPath || credentialsPath.length === 0) { - return false; - } - this._getApplicationCredentialsFromFilePath(credentialsPath, (err, result) => { - let wrappedError = null; - if (err) { - wrappedError = this.createError( - 'Unable to read the credential file specified by the GOOGLE_APPLICATION_CREDENTIALS ' + - 'environment variable.', - err); - } - if (callback) callback(wrappedError, result); - }); - return true; + return null; + } + try { + return this._getApplicationCredentialsFromFilePath(credentialsPath); + } catch (e) { + throw this.createError( + 'Unable to read the credential file specified by the GOOGLE_APPLICATION_CREDENTIALS environment variable.', + e); + } } /** * Attempts to load default credentials from a well-known file location - * @param {function=} callback Optional callback. - * @return {boolean} Returns true if the callback has been executed; false otherwise. + * @return Promise that resolves with the OAuth2Client or null. * @api private */ - _tryGetApplicationCredentialsFromWellKnownFile( - callback?: (err?: Error|null, result?: JWT|UserRefreshClient) => void) { + async _tryGetApplicationCredentialsFromWellKnownFile(): + Promise { // First, figure out the location of the file, depending upon the OS type. let location = null; if (this._isWindows()) { @@ -324,59 +330,49 @@ export class GoogleAuth { } // The file does not exist. if (!location) { - return false; + return null; } // The file seems to exist. Try to use it. - this._getApplicationCredentialsFromFilePath(location, callback); - return true; + return this._getApplicationCredentialsFromFilePath(location); } /** * Attempts to load default credentials from a file at the given path.. * @param {string=} filePath The path to the file to read. - * @param {function=} callback Optional callback. + * @returns Promise that resolves with the OAuth2Client * @api private */ - _getApplicationCredentialsFromFilePath( - filePath: string, - callback?: (err: Error|null, result?: JWT|UserRefreshClient) => void) { - let error = null; + async _getApplicationCredentialsFromFilePath(filePath: string): + Promise { // Make sure the path looks like a string. if (!filePath || filePath.length === 0) { - error = new Error('The file path is invalid.'); + throw new Error('The file path is invalid.'); } // Make sure there is a file at the path. lstatSync will throw if there is // nothing there. - if (!error) { - try { - // Resolve path to actual file in case of symlink. Expect a thrown error - // if not resolvable. - filePath = fs.realpathSync(filePath); - - if (!fs.lstatSync(filePath).isFile()) { - throw new Error(); - } - } catch (err) { - error = this.createError( - util.format( - 'The file at %s does not exist, or it is not a file.', - filePath), - err); + try { + // Resolve path to actual file in case of symlink. Expect a thrown error + // if not resolvable. + filePath = fs.realpathSync(filePath); + + if (!fs.lstatSync(filePath).isFile()) { + throw new Error(); } + } catch (err) { + throw this.createError( + util.format( + 'The file at %s does not exist, or it is not a file.', filePath), + err); } + // Now open a read stream on the file, and parse it. - if (!error) { - try { - const readStream = this._createReadStream(filePath); - this.fromStream(readStream, callback); - } catch (err) { - error = this.createError( - util.format('Unable to read the file at %s.', filePath), err); - } - } - if (error) { - if (callback) callback(error); + try { + const readStream = this._createReadStream(filePath); + return this.fromStream(readStream); + } catch (err) { + throw this.createError( + util.format('Unable to read the file at %s.', filePath), err); } } @@ -384,35 +380,41 @@ export class GoogleAuth { * Create a credentials instance using the given input options. * @param {object=} json The input object. * @param {function=} callback Optional callback. + * @returns Promise that resolves with the OAuth2Client (if no callback is + * passed) */ - fromJSON( - json: JWTInput, - callback?: (err: Error|null, client?: UserRefreshClient|JWT) => void) { + // TODO: Remove the overloads and just keep this a sync API + fromJSON(json: JWTInput): JWT|UserRefreshClient; + fromJSON(json: JWTInput, callback: CredentialCallback): void; + fromJSON(json: JWTInput, callback?: CredentialCallback): JWT|UserRefreshClient + |void { let client: UserRefreshClient|JWT; - if (!json) { + try { + if (!json) { + throw new Error( + 'Must pass in a JSON object containing the Google auth settings.'); + } + + this.jsonContent = json; + if (json.type === 'authorized_user') { + client = new UserRefreshClient(); + } else { + client = new JWT(); + } + client.fromJSON(json); if (callback) { - callback(new Error( - 'Must pass in a JSON object containing the Google auth settings.')); + return callback(null, client); + } else { + return client; } - return; - } - // Set the JSON contents - this.jsonContent = json; - if (json.type === 'authorized_user') { - client = new UserRefreshClient(); - } else { - client = new JWT(); - } - client.fromJSON(json, (err?: Error|null) => { + } catch (e) { if (callback) { - if (err) { - callback(err); - } else { - callback(null, client); - } + return callback(e); + } else { + throw e; } - }); + } } /** @@ -420,30 +422,40 @@ export class GoogleAuth { * @param {object=} inputStream The input stream. * @param {function=} callback Optional callback. */ - fromStream( - inputStream: stream.Readable, - callback?: (err: Error|null, result?: UserRefreshClient|JWT) => void) { - if (!inputStream) { - if (callback) { - setImmediate( - callback, - new Error( - 'Must pass in a stream containing the Google auth settings.')); - } - return; + fromStream(inputStream: stream.Readable): Promise; + fromStream(inputStream: stream.Readable, callback: CredentialCallback): void; + fromStream(inputStream: stream.Readable, callback?: CredentialCallback): + Promise|void { + if (callback) { + this.fromStreamAsync(inputStream) + .then(r => callback(null, r)) + .catch(callback); + } else { + return this.fromStreamAsync(inputStream); } - let s = ''; - inputStream.setEncoding('utf8'); - inputStream.on('data', (chunk) => { - s += chunk; - }); - inputStream.on('end', () => { - try { - const data = JSON.parse(s); - this.fromJSON(data, callback); - } catch (err) { - if (callback) callback(err); + } + + private fromStreamAsync(inputStream: stream.Readable): + Promise { + return new Promise((resolve, reject) => { + if (!inputStream) { + throw new Error( + 'Must pass in a stream containing the Google auth settings.'); } + let s = ''; + inputStream.setEncoding('utf8'); + inputStream.on('data', (chunk) => { + s += chunk; + }); + inputStream.on('end', () => { + try { + const data = JSON.parse(s); + const r = this.fromJSON(data); + return resolve(r); + } catch (err) { + return reject(err); + } + }); }); } @@ -453,15 +465,23 @@ export class GoogleAuth { * @param {function=} - Optional callback function */ fromAPIKey( - apiKey: string, callback?: (err?: Error|null, client?: JWT) => void) { + apiKey: string, + callback?: (err?: Error|null, client?: JWT) => void): void|JWT { const client = new JWT(); - client.fromAPIKey(apiKey, (err) => { - if (err) { - if (callback) callback(err); + try { + client.fromAPIKey(apiKey); + if (callback) { + callback(null, client); } else { - if (callback) callback(null, client); + return client; } - }); + } catch (e) { + if (callback) { + callback(e); + } else { + throw e; + } + } } /** @@ -545,69 +565,45 @@ export class GoogleAuth { /** * Loads the default project of the Google Cloud SDK. - * @param {function} callback Callback. * @api private */ - private getDefaultServiceProjectId(callback: ProjectIdCallback) { - this._getSDKDefaultProjectId((err, stdout) => { - let projectId = null; - if (!err && stdout) { - try { - projectId = JSON.parse(stdout).core.project; - } catch (err) { - projectId = null; - } + private async getDefaultServiceProjectId(): Promise { + try { + const r = await this._getSDKDefaultProjectId(); + if (r.stdout) { + return JSON.parse(r.stdout).core.project; } + } catch (e) { // Ignore any errors - if (callback) callback(null, projectId); - }); + } + return null; } /** * Loads the project id from environment variables. - * @param {function} callback Callback. * @api private */ - private _getProductionProjectId(callback: ProjectIdCallback) { - const projectId = - this._getEnv('GCLOUD_PROJECT') || this._getEnv('GOOGLE_CLOUD_PROJECT'); - if (projectId) { - if (callback) setImmediate(callback, null, projectId); - } - return projectId; + private getProductionProjectId() { + return this._getEnv('GCLOUD_PROJECT') || + this._getEnv('GOOGLE_CLOUD_PROJECT'); } /** * Loads the project id from the GOOGLE_APPLICATION_CREDENTIALS json file. - * @param {function} callback Callback. * @api private */ - private getFileProjectId(callback: ProjectIdCallback) { + private async getFileProjectId(): Promise { if (this.cachedCredential) { // Try to read the project ID from the cached credentials file - if (callback) { - setImmediate(callback, null, this.cachedCredential.projectId); - } - return; + return this.cachedCredential.projectId; } // Try to load a credentials file and read its project ID - const pathExists = - this._tryGetApplicationCredentialsFromEnvironmentVariable( - (err, result?: JWT|UserRefreshClient) => { - if (!err && result) { - if (callback) { - callback(null, result.projectId); - } - return; - } - if (callback) { - callback(err); - } - }); - - if (!pathExists) { - if (callback) callback(); + const r = await this._tryGetApplicationCredentialsFromEnvironmentVariable(); + if (r) { + return r.projectId; + } else { + return null; } } @@ -621,27 +617,22 @@ export class GoogleAuth { * See https://github.com/google/oauth2client/issues/93 for context about * DNS latency. * - * @param {function} callback Callback. * @api private */ - private getGCEProjectId(callback?: BodyResponseCallback) { + private async getGCEProjectId() { if (!this.transporter) { this.transporter = new DefaultTransporter(); } - this.transporter.request( - { - method: 'GET', - uri: 'http://169.254.169.254/computeMetadata/v1/project/project-id', - headers: {'Metadata-Flavor': 'Google'} - }, - (err, body, res) => { - if (err || !res || res.statusCode !== 200 || !body) { - if (callback) callback(null); - return; - } - // Ignore any errors - if (callback) callback(null, body); - }); + try { + const r = await this.transporter.request({ + url: 'http://169.254.169.254/computeMetadata/v1/project/project-id', + headers: {'Metadata-Flavor': 'Google'} + }); + return r.data; + } catch (e) { + // Ignore any errors + return null; + } } @@ -655,47 +646,47 @@ export class GoogleAuth { * a client_email and optional private key, or the error. * returned */ + getCredentials(): Promise; + getCredentials( + callback: (err: Error|null, credentials?: CredentialBody) => void): void; getCredentials( - callback: (err: Error|null, credentials?: CredentialBody) => void) { + callback?: (err: Error|null, credentials?: CredentialBody) => void): + void|Promise { + if (callback) { + this.getCredentialsAsync().then(r => callback(null, r)).catch(callback); + } else { + return this.getCredentialsAsync(); + } + } + + private async getCredentialsAsync(): Promise { if (this.jsonContent) { const credential: CredentialBody = { client_email: this.jsonContent.client_email, private_key: this.jsonContent.private_key }; - callback(null, credential); - } else { - this._checkIsGCE((err, gce) => { - if (err) { - callback(err); - } else if (!gce) { - callback(new Error('Unknown error.')); - } else { - // For GCE, return the service account details from the metadata - // server - this.transporter.request( - { - method: 'GET', - uri: - 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/?recursive=true', - headers: {'Metadata-Flavor': 'Google'} - }, - (err, body, res) => { - if (err || !res || res.statusCode !== 200 || !body || - !body.default || !body.default.email) { - if (callback) { - callback(new Error('Failure from metadata server.')); - } - } else { - // Callback with the body - const credential: - CredentialBody = {client_email: body.default.email}; - if (callback) { - callback(null, credential); - } - } - }); - } - }); + return credential; + } + + const isGCE = await this._checkIsGCE(); + if (!isGCE) { + throw new Error('Unknown error.'); + } + + // For GCE, return the service account details from the metadata server + const result = await this.transporter.request({ + url: + 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/?recursive=true', + headers: {'Metadata-Flavor': 'Google'} + }); + + if (!result.data || !result.data.default || !result.data.default.email) { + throw new Error('Failure from metadata server.'); } + + // Callback with the body + const credential: + CredentialBody = {client_email: result.data.default.email}; + return credential; } } diff --git a/src/auth/jwtaccess.ts b/src/auth/jwtaccess.ts index a5b7f907..07cd7324 100644 --- a/src/auth/jwtaccess.ts +++ b/src/auth/jwtaccess.ts @@ -14,13 +14,10 @@ * limitations under the License. */ -import * as http from 'http'; import * as jws from 'jws'; import * as stream from 'stream'; - import {JWTInput} from './credentials'; - -const noop = Function.prototype; +import {RequestMetadataCallback, RequestMetadataResponse} from './oauth2client'; export class JWTAccess { email?: string|null; @@ -57,65 +54,77 @@ export class JWTAccess { * Get a non-expired access token, after refreshing if necessary * * @param {string} authURI the URI being authorized - * @param {function} metadataCb a callback invoked with the jwt - * request metadata. + * @param {function} metadataCb a callback invoked with the jwt request metadata. + * @returns a Promise that resolves with the request metadata response */ - getRequestMetadata( - authURI: string, - metadataCb: - (err: Error|null, headers?: http.IncomingHttpHeaders|null) => void) { - const iat = Math.floor(new Date().getTime() / 1000); - const exp = iat + 3600; // 3600 seconds = 1 hour + getRequestMetadata(authURI: string): RequestMetadataResponse; + getRequestMetadata(authURI: string, callback: RequestMetadataCallback): void; + getRequestMetadata(authURI: string, callback?: RequestMetadataCallback): + void|RequestMetadataResponse { + try { + const iat = Math.floor(new Date().getTime() / 1000); + const exp = iat + 3600; // 3600 seconds = 1 hour - // The payload used for signed JWT headers has: - // iss == sub == - // aud == - const payload = {iss: this.email, sub: this.email, aud: authURI, exp, iat}; - const assertion = { - header: {alg: 'RS256'} as jws.Header, - payload, - secret: this.key - }; + // The payload used for signed JWT headers has: + // iss == sub == + // aud == + const payload = + {iss: this.email, sub: this.email, aud: authURI, exp, iat}; + const assertion = { + header: {alg: 'RS256'} as jws.Header, + payload, + secret: this.key + }; - // Sign the jwt and invoke metadataCb with it. - return this._signJWT(assertion, (err, signedJWT) => { - if (!err) { - return metadataCb(null, {Authorization: 'Bearer ' + signedJWT}); + // Sign the jwt and invoke metadataCb with it. + const signedJWT = + jws.sign({header: {alg: 'RS256'}, payload, secret: this.key}); + const result = { + res: null, + headers: {Authorization: 'Bearer ' + signedJWT} + }; + if (callback) { + callback(null, result.headers, null); } else { - return metadataCb(err, null); + return result; } - }); + } catch (e) { + if (callback) { + callback(e); + } else { + throw e; + } + } } - - /** * Create a JWTAccess credentials instance using the given input options. * @param {object=} json The input object. * @param {function=} callback Optional callback. */ - fromJSON(json: JWTInput, callback?: (err: Error) => void) { - const done = callback || noop; - if (!json) { - done(new Error( - 'Must pass in a JSON object containing the service account auth settings.')); - return; - } - if (!json.client_email) { - done(new Error( - 'The incoming JSON object does not contain a client_email field')); - return; - } - if (!json.private_key) { - done(new Error( - 'The incoming JSON object does not contain a private_key field')); - return; + fromJSON(json: JWTInput, callback?: (err: Error|null) => void) { + try { + if (!json) { + throw new Error( + 'Must pass in a JSON object containing the service account auth settings.'); + } + if (!json.client_email) { + throw new Error( + 'The incoming JSON object does not contain a client_email field'); + } + if (!json.private_key) { + throw new Error( + 'The incoming JSON object does not contain a private_key field'); + } + // Extract the relevant information from the json key file. + this.email = json.client_email; + this.key = json.private_key; + this.projectId = json.project_id; + if (callback) callback(null); + } catch (e) { + if (callback) return callback(e); + throw e; } - // Extract the relevant information from the json key file. - this.email = json.client_email; - this.key = json.private_key; - this.projectId = json.project_id; - done(); } /** @@ -123,46 +132,38 @@ export class JWTAccess { * @param {object=} inputStream The input stream. * @param {function=} callback Optional callback. */ - fromStream(inputStream: stream.Readable, callback?: (err: Error) => void) { - const done = callback || noop; - if (!inputStream) { - setImmediate(() => { - done(new Error( - 'Must pass in a stream containing the service account auth settings.')); - }); - return; + fromStream(inputStream: stream.Readable): Promise; + fromStream(inputStream: stream.Readable, callback: (err?: Error) => void): + void; + fromStream(inputStream: stream.Readable, callback?: (err?: Error) => void): + void|Promise { + if (callback) { + this.fromStreamAsync(inputStream).then(r => callback()).catch(callback); + } else { + return this.fromStreamAsync(inputStream); } - let s = ''; - inputStream.setEncoding('utf8'); - inputStream.on('data', (chunk) => { - s += chunk; - }); - inputStream.on('end', () => { - try { - const data = JSON.parse(s); - this.fromJSON(data, callback); - } catch (err) { - done(err); - } - }); } - /** - * Sign the JWT object, returning any errors in the callback. - * - * signedJwtFn is a callback function(err, signedJWT); it is called with an - * error if there is an exception during signing. - * - * @param {object} assertion The assertion to sign - * @param {Function} signedJwtFn fn(err, signedJWT) - */ - private _signJWT( - assertion: jws.SignOptions, - signedJwtFn: (err: Error|null, signedJwt?: {}) => void) { - try { - return signedJwtFn(null, jws.sign(assertion)); - } catch (err) { - return signedJwtFn(err); - } + private fromStreamAsync(inputStream: stream.Readable): Promise { + return new Promise((resolve, reject) => { + if (!inputStream) { + reject(new Error( + 'Must pass in a stream containing the service account auth settings.')); + } + let s = ''; + inputStream.setEncoding('utf8'); + inputStream.on('data', (chunk) => { + s += chunk; + }); + inputStream.on('end', () => { + try { + const data = JSON.parse(s); + this.fromJSON(data); + resolve(); + } catch (err) { + reject(err); + } + }); + }); } } diff --git a/src/auth/jwtclient.ts b/src/auth/jwtclient.ts index 651060e3..07a11864 100644 --- a/src/auth/jwtclient.ts +++ b/src/auth/jwtclient.ts @@ -15,15 +15,13 @@ */ import {GoogleToken, TokenOptions} from 'gtoken'; -import * as request from 'request'; import * as stream from 'stream'; import {Credentials, JWTInput} from './credentials'; import {JWTAccess} from './jwtaccess'; -import {OAuth2Client} from './oauth2client'; +import {GetTokenResponse, OAuth2Client, RequestMetadataResponse} from './oauth2client'; const isString = require('lodash.isstring'); -const noop = Function.prototype; export class JWT extends OAuth2Client { email?: string; @@ -71,18 +69,16 @@ export class JWT extends OAuth2Client { * Obtains the metadata to be sent with the request. * * @param {string} optUri the URI being authorized. - * @param {function} metadataCb */ - getRequestMetadata( - optUri: string|null, - metadataCb: (err: Error|null, result?: {}|null) => void) { - if (this.createScopedRequired() && optUri) { + protected async getRequestMetadataAsync(url?: string|null): + Promise { + if (this.createScopedRequired() && url) { // no scopes have been set, but a uri has been provided. Use JWTAccess // credentials. const alt = new JWTAccess(this.email, this.key); - return alt.getRequestMetadata(optUri, metadataCb); + return alt.getRequestMetadata(url); } else { - return super.getRequestMetadata(optUri, metadataCb); + return super.getRequestMetadataAsync(url); } } @@ -107,47 +103,45 @@ export class JWT extends OAuth2Client { /** * Get the initial access token using gToken. * @param {function=} callback Optional callback. + * @returns Promise that resolves with credentials */ - authorize(callback?: (err: Error|null, result: Credentials) => void) { - const done = callback || noop; - this.refreshToken(null, (err, result) => { - if (err) { - done(err); - } else if (!result) { - done(new Error('No result returned')); - } else { - this.credentials = result; - this.credentials.refresh_token = 'jwt-placeholder'; - this.key = this.gtoken.key; - this.email = this.gtoken.iss; - done(null, result); - } - }); + authorize(): Promise; + authorize(callback: (err: Error|null, result?: Credentials) => void): void; + authorize(callback?: (err: Error|null, result?: Credentials) => void): + Promise|void { + if (callback) { + this.authorizeAsync().then(r => callback(null, r)).catch(callback); + } else { + return this.authorizeAsync(); + } + } + + private async authorizeAsync(): Promise { + const result = await this.refreshToken(); + if (!result) { + throw new Error('No result returned'); + } + this.credentials = result.tokens; + this.credentials.refresh_token = 'jwt-placeholder'; + this.key = this.gtoken.key; + this.email = this.gtoken.iss; + return result.tokens; } /** * Refreshes the access token. * @param {object=} ignored - * @param {function=} callback Optional callback. * @private */ - refreshToken( - ignored: string|null, - callback?: (err: Error, credentials?: Credentials) => void) { - const done = callback || noop; - return this.createGToken((err, newGToken) => { - if (err) { - return done(err); - } else { - return newGToken.getToken((err2?: Error|null, token?: string|null) => { - return done(err2, { - access_token: token, - token_type: 'Bearer', - expiry_date: newGToken.expiresAt - }); - }); - } - }); + async refreshToken(refreshToken?: string|null): Promise { + const newGToken = this.createGToken(); + const token = await newGToken.getToken(); + const tokens = { + access_token: token, + token_type: 'Bearer', + expiry_date: newGToken.expiresAt + }; + return {res: null, tokens}; } /** @@ -155,28 +149,32 @@ export class JWT extends OAuth2Client { * @param {object=} json The input object. * @param {function=} callback Optional callback. */ - fromJSON(json: JWTInput, callback?: (err?: Error) => void) { - const done = callback || noop; - if (!json) { - done(new Error( - 'Must pass in a JSON object containing the service account auth settings.')); - return; - } - if (!json.client_email) { - done(new Error( - 'The incoming JSON object does not contain a client_email field')); - return; - } - if (!json.private_key) { - done(new Error( - 'The incoming JSON object does not contain a private_key field')); - return; + fromJSON(json: JWTInput, callback?: (err?: Error) => void): void { + try { + if (!json) { + throw new Error( + 'Must pass in a JSON object containing the service account auth settings.'); + } + if (!json.client_email) { + throw new Error( + 'The incoming JSON object does not contain a client_email field'); + } + if (!json.private_key) { + throw new Error( + 'The incoming JSON object does not contain a private_key field'); + } + // Extract the relevant information from the json key file. + this.email = json.client_email; + this.key = json.private_key; + this.projectId = json.project_id; + } catch (e) { + if (callback) { + callback(e); + } else { + throw e; + } } - // Extract the relevant information from the json key file. - this.email = json.client_email; - this.key = json.private_key; - this.projectId = json.project_id; - done(); + if (callback) callback(); } /** @@ -184,28 +182,39 @@ export class JWT extends OAuth2Client { * @param {object=} inputStream The input stream. * @param {function=} callback Optional callback. */ + fromStream(inputStream: stream.Readable): Promise; fromStream( - inputStream: stream.Readable, callback: (err?: Error|null) => void) { - const done = callback || noop; - if (!inputStream) { - setImmediate(() => { - done(new Error( - 'Must pass in a stream containing the service account auth settings.')); - }); - return; + inputStream: stream.Readable, callback: (err?: Error|null) => void): void; + fromStream( + inputStream: stream.Readable, + callback?: (err?: Error|null) => void): void|Promise { + if (callback) { + this.fromStreamAsync(inputStream).then(r => callback()).catch(callback); + } else { + return this.fromStreamAsync(inputStream); } - let s = ''; - inputStream.setEncoding('utf8'); - inputStream.on('data', (chunk) => { - s += chunk; - }); - inputStream.on('end', () => { - try { - const data = JSON.parse(s); - this.fromJSON(data, callback); - } catch (err) { - done(err); + } + + private fromStreamAsync(inputStream: stream.Readable) { + return new Promise((resolve, reject) => { + if (!inputStream) { + throw new Error( + 'Must pass in a stream containing the service account auth settings.'); } + let s = ''; + inputStream.setEncoding('utf8'); + inputStream.on('data', (chunk) => { + s += chunk; + }); + inputStream.on('end', () => { + try { + const data = JSON.parse(s); + this.fromJSON(data); + resolve(); + } catch (e) { + reject(e); + } + }); }); } @@ -215,16 +224,19 @@ export class JWT extends OAuth2Client { * @param {function=} callback - Optional callback to be invoked after * initialization. */ - fromAPIKey(apiKey: string, callback?: (err: Error) => void) { - const done = callback || noop; + fromAPIKey(apiKey: string, callback?: (err?: Error) => void): void { if (!isString(apiKey)) { - setImmediate(() => { - done(new Error('Must provide an API Key string.')); - }); - return; + const e = new Error('Must provide an API Key string.'); + if (callback) { + return setImmediate(callback, e); + } else { + throw e; + } } this.apiKey = apiKey; - done(null); + if (callback) { + return callback(); + } } /** @@ -232,20 +244,16 @@ export class JWT extends OAuth2Client { * @param {function=} callback Callback. * @private */ - private createGToken( - callback: (err: Error|null, token: GoogleToken) => void) { - if (this.gtoken) { - return callback(null, this.gtoken); - } else { - const opts = { + private createGToken() { + if (!this.gtoken) { + this.gtoken = new GoogleToken({ iss: this.email, sub: this.subject, scope: this.scopes, keyFile: this.keyFile, key: this.key - } as TokenOptions; - this.gtoken = new GoogleToken(opts); - return callback(null, this.gtoken); + } as TokenOptions); } + return this.gtoken; } } diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index a43bc1f8..ce75c4e3 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -14,17 +14,16 @@ * limitations under the License. */ +import {AxiosError, AxiosPromise, AxiosRequestConfig, AxiosResponse} from 'axios'; +import * as http from 'http'; import * as querystring from 'querystring'; -import * as request from 'request'; import {PemVerifier} from './../pemverifier'; -import {BodyResponseCallback, RequestError} from './../transporters'; +import {BodyResponseCallback} from './../transporters'; import {AuthClient} from './authclient'; -import {Credentials} from './credentials'; +import {CredentialRequest, Credentials} from './credentials'; import {LoginTicket} from './loginticket'; -const merge = require('lodash.merge'); - export interface GenerateAuthUrlOpts { response_type?: string; client_id?: string; @@ -37,12 +36,58 @@ export interface AuthClientOpts { tokenUrl?: string; } -export interface OAuthTokens { - expires_in: number; - expiry_date: number; +export interface GetTokenCallback { + (err: AxiosError|null, token?: Credentials|null, + res?: AxiosResponse|null): void; +} + +export interface GetTokenResponse { + tokens: Credentials; + res: AxiosResponse|null; +} + +export interface GetAccessTokenCallback { + (err: AxiosError|null, token?: string|null, res?: AxiosResponse|null): void; +} + +export interface GetAccessTokenResponse { + token?: string|null; + res?: AxiosResponse|null; } -const noop = Function.prototype; +export interface RefreshAccessTokenCallback { + (err: AxiosError|null, credentials?: Credentials|null, + res?: AxiosResponse|null): void; +} + +export interface RefreshAccessTokenResponse { + credentials: Credentials; + res: AxiosResponse|null; +} + +export interface RequestMetadataResponse { + headers: http.IncomingHttpHeaders; + res?: AxiosResponse|null; +} + +export interface RequestMetadataCallback { + (err: AxiosError|null, headers?: http.IncomingHttpHeaders, + res?: AxiosResponse|null): void; +} + +export interface GetFederatedSignonCertsCallback { + // tslint:disable-next-line no-any + (err: AxiosError|null, certs?: any, + response?: AxiosResponse|null): void; +} + +export interface FederatedSignonCertsResponse { + // tslint:disable-next-line no-any + certs: any; + res?: AxiosResponse|null; +} + +export interface RevokeCredentialsResult { success: boolean; } export class OAuth2Client extends AuthClient { private redirectUri?: string; @@ -146,8 +191,21 @@ export class OAuth2Client extends AuthClient { * @param {string} code The authorization code. * @param {function=} callback Optional callback fn. */ - getToken(code: string, callback?: BodyResponseCallback) { - const uri = this.opts.tokenUrl || OAuth2Client.GOOGLE_OAUTH2_TOKEN_URL_; + getToken(code: string): Promise; + getToken(code: string, callback: GetTokenCallback): void; + getToken(code: string, callback?: GetTokenCallback): + Promise|void { + if (callback) { + this.getTokenAsync(code) + .then(r => callback(null, r.tokens, r.res)) + .catch(e => callback(e, null, (e as AxiosError).response)); + } else { + return this.getTokenAsync(code); + } + } + + private async getTokenAsync(code: string): Promise { + const url = this.opts.tokenUrl || OAuth2Client.GOOGLE_OAUTH2_TOKEN_URL_; const values = { code, client_id: this._clientId, @@ -156,18 +214,18 @@ export class OAuth2Client extends AuthClient { grant_type: 'authorization_code' }; - this.transporter.request( - {method: 'POST', uri, form: values, json: true}, - (err, body, response) => { - const tokens = body as OAuthTokens; - if (!err && tokens && tokens.expires_in) { - tokens.expiry_date = - ((new Date()).getTime() + (tokens.expires_in * 1000)); - delete tokens.expires_in; - } - const done = callback || noop; - done(err, tokens, response); - }); + const res = await this.transporter.request( + {method: 'POST', url, data: values}); + + console.log(res.data); + const tokens = res.data as Credentials; + if (res.data && res.data.expires_in) { + tokens.expiry_date = + ((new Date()).getTime() + (res.data.expires_in * 1000)); + delete (tokens as CredentialRequest).expires_in; + } + + return {tokens, res}; } /** @@ -176,11 +234,10 @@ export class OAuth2Client extends AuthClient { * @param {function=} callback Optional callback. * @private */ - protected refreshToken( - refreshToken?: string|null, - callback?: BodyResponseCallback): request.Request|void { - const uri = this.opts.tokenUrl || OAuth2Client.GOOGLE_OAUTH2_TOKEN_URL_; - const values = { + protected async refreshToken(refreshToken?: string| + null): Promise { + const url = this.opts.tokenUrl || OAuth2Client.GOOGLE_OAUTH2_TOKEN_URL_; + const data = { refresh_token: refreshToken, client_id: this._clientId, client_secret: this._clientSecret, @@ -188,18 +245,16 @@ export class OAuth2Client extends AuthClient { }; // request for new token - return this.transporter.request( - {method: 'POST', uri, form: values, json: true}, - (err, body, response) => { - const tokens = body as OAuthTokens; - if (!err && tokens && tokens.expires_in) { - tokens.expiry_date = - ((new Date()).getTime() + (tokens.expires_in * 1000)); - delete tokens.expires_in; - } - const done = callback || noop; - done(err, tokens, response); - }); + const res = await this.transporter.request( + {method: 'POST', url, data}); + const tokens = res.data as Credentials; + // TODO: de-duplicate this code from a few spots + if (res.data && res.data.expires_in) { + tokens.expiry_date = + ((new Date()).getTime() + (res.data.expires_in * 1000)); + delete (tokens as CredentialRequest).expires_in; + } + return {tokens, res}; } /** @@ -208,26 +263,29 @@ export class OAuth2Client extends AuthClient { * @deprecated use getRequestMetadata instead. * @param {function} callback callback */ - refreshAccessToken( - callback: - (err: Error|null, credentials: Credentials|null, - response?: request.RequestResponse|null) => void) { + + refreshAccessToken(): Promise; + refreshAccessToken(callback: RefreshAccessTokenCallback): void; + refreshAccessToken(callback?: RefreshAccessTokenCallback): + Promise|void { + if (callback) { + this.refreshAccessTokenAsync() + .then(r => callback(null, r.credentials, r.res)) + .catch(callback); + } else { + return this.refreshAccessTokenAsync(); + } + } + + private async refreshAccessTokenAsync() { if (!this.credentials.refresh_token) { - callback(new Error('No refresh token is set.'), null); - return; - } - - this.refreshToken( - this.credentials.refresh_token, (err, result, response) => { - if (err) { - callback(err, null, response); - } else { - const tokens = result as Credentials; - tokens.refresh_token = this.credentials.refresh_token; - this.credentials = tokens; - callback(null, this.credentials, response); - } - }); + throw new Error('No refresh token is set.'); + } + const r = await this.refreshToken(this.credentials.refresh_token); + const tokens = r.tokens as Credentials; + tokens.refresh_token = this.credentials.refresh_token; + this.credentials = tokens; + return {credentials: this.credentials, res: r.res}; } /** @@ -235,38 +293,42 @@ export class OAuth2Client extends AuthClient { * * @param {function} callback Callback to call with the access token */ - getAccessToken( - callback: - (err: Error|null, accessToken?: string|null, - response?: request.RequestResponse|null) => void) { + getAccessToken(): Promise; + getAccessToken(callback: GetAccessTokenCallback): void; + getAccessToken(callback?: GetAccessTokenCallback): + Promise|void { + if (callback) { + this.getAccessTokenAsync() + .then(r => callback(null, r.token, r.res)) + .catch(callback); + } else { + return this.getAccessTokenAsync(); + } + } + + private async getAccessTokenAsync(): Promise { const expiryDate = this.credentials.expiry_date; // if no expiry time, assume it's not expired const isTokenExpired = expiryDate ? expiryDate <= (new Date()).getTime() : false; - if (!this.credentials.access_token && !this.credentials.refresh_token) { - return callback(new Error('No access or refresh token is set.'), null); + throw new Error('No access or refresh token is set.'); } const shouldRefresh = !this.credentials.access_token || isTokenExpired; if (shouldRefresh && this.credentials.refresh_token) { if (!this.credentials.refresh_token) { - return callback(new Error('No refresh token is set.'), null); + throw new Error('No refresh token is set.'); } - this.refreshAccessToken((err, tokens, response) => { - if (err) { - return callback(err, null, response); - } - if (!tokens || (tokens && !tokens.access_token)) { - return callback( - new Error('Could not refresh access token.'), null, response); - } - return callback(null, tokens.access_token, response); - }); + const r = await this.refreshAccessToken(); + if (!r.credentials || (r.credentials && !r.credentials.access_token)) { + throw new Error('Could not refresh access token.'); + } + return {token: r.credentials.access_token, res: r.res}; } else { - return callback(null, this.credentials.access_token, null); + return {token: this.credentials.access_token}; } } @@ -285,16 +347,24 @@ export class OAuth2Client extends AuthClient { * @param {string} optUri the Uri being authorized * @param {function} metadataCb the func described above */ - getRequestMetadata( - optUri: string|null, - metadataCb: - (err: Error|null, headers?: {}|null, - response?: request.RequestResponse|null) => void) { - const thisCreds = this.credentials; + getRequestMetadata(url?: string|null): Promise; + getRequestMetadata(url: string|null, callback: RequestMetadataCallback): void; + getRequestMetadata(url: string|null, callback?: RequestMetadataCallback): + Promise|void { + if (callback) { + this.getRequestMetadataAsync(url) + .then(r => callback(null, r.headers, r.res)) + .catch(callback); + } else { + return this.getRequestMetadataAsync(url); + } + } + protected async getRequestMetadataAsync(url?: string|null): + Promise { + const thisCreds = this.credentials; if (!thisCreds.access_token && !thisCreds.refresh_token && !this.apiKey) { - return metadataCb( - new Error('No access, refresh token or API key is set.'), null); + throw new Error('No access, refresh token or API key is set.'); } // if no expiry time, assume it's not expired @@ -307,36 +377,34 @@ export class OAuth2Client extends AuthClient { const headers = { Authorization: thisCreds.token_type + ' ' + thisCreds.access_token }; - return metadataCb(null, headers, null); + return {headers}; } if (this.apiKey) { - return metadataCb(null, {}, null); + return {headers: {}}; } - - return this.refreshToken(thisCreds.refresh_token, (err, body, response) => { - // If the error code is 403 or 404, go to the else so the error - // message is replaced. Otherwise, return the error. - const tokens = body as Credentials; - if (err && (err as RequestError).code !== 403 && - (err as RequestError).code !== 404) { - return metadataCb(err, null, response); - } else { - if (!tokens || (tokens && !tokens.access_token)) { - return metadataCb( - new Error('Could not refresh access token.'), null, response); - } - - const credentials = this.credentials; - credentials.token_type = credentials.token_type || 'Bearer'; - tokens.refresh_token = credentials.refresh_token; - this.credentials = tokens; - const headers = { - Authorization: credentials.token_type + ' ' + tokens.access_token - }; - return metadataCb(err, headers, response); + let r: GetTokenResponse|null = null; + let tokens: Credentials|null = null; + try { + r = await this.refreshToken(thisCreds.refresh_token); + tokens = r.tokens; + } catch (err) { + const e = err as AxiosError; + if (e.response && + (e.response.status === 403 || e.response.status === 404)) { + e.message = 'Could not refresh access token.'; } - }); + throw e; + } + + const credentials = this.credentials; + credentials.token_type = credentials.token_type || 'Bearer'; + tokens.refresh_token = credentials.refresh_token; + this.credentials = tokens; + const headers = { + Authorization: credentials.token_type + ' ' + tokens.access_token + }; + return {headers, res: r.res}; } /** @@ -344,27 +412,54 @@ export class OAuth2Client extends AuthClient { * @param {string} token The existing token to be revoked. * @param {function=} callback Optional callback fn. */ - revokeToken(token: string, callback?: BodyResponseCallback) { - this.transporter.request( - { - uri: OAuth2Client.GOOGLE_OAUTH2_REVOKE_URL_ + '?' + - querystring.stringify({token}), - json: true - }, - callback); + revokeToken(token: string): AxiosPromise; + revokeToken( + token: string, + callback: BodyResponseCallback): void; + revokeToken( + token: string, callback?: BodyResponseCallback): + AxiosPromise|void { + const opts = { + url: OAuth2Client.GOOGLE_OAUTH2_REVOKE_URL_ + '?' + + querystring.stringify({token}) + }; + if (callback) { + this.transporter.request(opts) + .then(res => { + callback(null, res); + }) + .catch(callback); + } else { + return this.transporter.request(opts); + } } + /** * Revokes access token and clears the credentials object * @param {Function=} callback callback */ - revokeCredentials(callback: BodyResponseCallback) { + revokeCredentials(): AxiosPromise; + revokeCredentials(callback: BodyResponseCallback): + void; + revokeCredentials(callback?: BodyResponseCallback): + AxiosPromise|void { + if (callback) { + this.revokeCredentialsAsync() + .then(res => callback(null, res)) + .catch(callback); + } else { + return this.revokeCredentialsAsync(); + } + } + + private async revokeCredentialsAsync() { const token = this.credentials.access_token; this.credentials = {}; if (token) { - this.revokeToken(token, callback); + return this.revokeToken(token); } else { - callback(new RequestError('No access token to revoke.'), null, null); + throw new Error('No access token to revoke.'); } } @@ -377,87 +472,54 @@ export class OAuth2Client extends AuthClient { * @param {function} callback callback. * @return {Request} Request object */ - request(opts: request.Options, callback?: BodyResponseCallback) { - // Callbacks will close over this to ensure that we only retry once - let retry = true; - const unusedUri: string|null = null; - - // Declare authCb upfront to avoid the linter complaining about use before - // declaration. - let authCb: BodyResponseCallback; - - // Hook the callback routine to call the _postRequest method. - const postRequestCb = - (err: Error|null, body: {}|null, - resp?: request.RequestResponse|null) => { - const statusCode = resp && resp.statusCode; - // Automatically retry 401 and 403 responses - // if err is set and is unrelated to response - // then getting credentials failed, and retrying won't help - if (retry && (statusCode === 401 || statusCode === 403) && - (!err || (err as RequestError).code === statusCode)) { - /* It only makes sense to retry once, because the retry is intended - * to handle expiration-related failures. If refreshing the token - * does not fix the failure, then refreshing again probably won't - * help */ - retry = false; - // Force token refresh - this.refreshAccessToken(() => { - this.getRequestMetadata(unusedUri, authCb); - }); - } else { - this.postRequest(err, body, resp, callback); - } - }; - - authCb = (err, headers, response) => { - if (err) { - postRequestCb(err, null, response); - return null; - } else { - if (headers) { - opts.headers = opts.headers || {}; - opts.headers.Authorization = (headers as { - Authorization: string; - }).Authorization; - } - if (this.apiKey) { - if (opts.qs) { - opts.qs = merge({}, opts.qs, {key: this.apiKey}); - } else { - opts.qs = {key: this.apiKey}; - } - } - return this._makeRequest(opts, postRequestCb); - } - }; - - return this.getRequestMetadata(unusedUri, authCb); + request(opts: AxiosRequestConfig): AxiosPromise; + request(opts: AxiosRequestConfig, callback: BodyResponseCallback): void; + request(opts: AxiosRequestConfig, callback?: BodyResponseCallback): + AxiosPromise|void { + if (callback) { + this.requestAsync(opts).then(r => callback(null, r)).catch(e => { + const err = e as AxiosError; + const body = err.response ? err.response.data : null; + return callback(e, err.response); + }); + } else { + return this.requestAsync(opts); + } } - /** - * Makes a request without paying attention to refreshing or anything - * Assumes that all credentials are set correctly. - * @param {object} opts Options for request - * @param {Function} callback callback function - * @return {Request} The request object created - */ - _makeRequest(opts: request.Options, callback: BodyResponseCallback) { - return this.transporter.request(opts, callback); - } + protected async requestAsync(opts: AxiosRequestConfig, retry = false): + Promise> { + let r2: AxiosResponse; + try { + const r = await this.getRequestMetadataAsync(null); + if (r.headers && r.headers.Authorization) { + opts.headers = opts.headers || {}; + opts.headers.Authorization = r.headers.Authorization; + } - /** - * Allows inheriting classes to inspect and alter the request result. - * @param {object} err Error result. - * @param {object} result The result. - * @param {object} result The HTTP response. - * @param {Function} callback The callback. - * @private - */ - protected postRequest( - err: Error|null, result: {}|null, response?: request.RequestResponse|null, - callback?: BodyResponseCallback) { - if (callback) callback(err, result, response); + if (this.apiKey) { + opts.params = Object.assign(opts.params || {}, {key: this.apiKey}); + } + r2 = await this.transporter.request(opts); + } catch (e) { + const res = (e as AxiosError).response; + if (res) { + const statusCode = res.status; + // Automatically retry 401 and 403 responses if err is set and is + // unrelated to response then getting credentials failed, and retrying + // won't help + if (!retry && (statusCode === 401 || statusCode === 403)) { + /* It only makes sense to retry once, because the retry is intended + * to handle expiration-related failures. If refreshing the token + * does not fix the failure, then refreshing again probably won't + * help */ + await this.refreshAccessTokenAsync(); + return this.requestAsync(opts, true); + } + } + throw e; + } + return r2; } /** @@ -466,31 +528,34 @@ export class OAuth2Client extends AuthClient { * @param {(string|Array.)} audience The audience to verify against the ID Token * @param {function=} callback Callback supplying GoogleLogin if successful */ + verifyIdToken(idToken: string, audience: string|string[]): + Promise; verifyIdToken( idToken: string, audience: string|string[], - callback: (err: Error|null, login?: LoginTicket|null) => void) { - if (!idToken || !callback) { - throw new Error( - 'The verifyIdToken method requires both ' + - 'an ID Token and a callback method'); - } - - this.getFederatedSignonCerts(((err: Error, certs: {}) => { - if (err) { - callback(err, null); - } - let login; - try { - login = this.verifySignedJwtWithCerts( - idToken, certs, audience, - OAuth2Client.ISSUERS_); - } catch (err) { - callback(err); - return; - } - - callback(null, login); - }).bind(this)); + callback: (err: Error|null, login?: LoginTicket|null) => void): void; + verifyIdToken( + idToken: string, audience: string|string[], + callback?: (err: Error|null, login?: LoginTicket|null) => void): + void|Promise { + if (callback) { + this.verifyIdTokenAsync(idToken, audience) + .then(r => callback(null, r)) + .catch(callback); + } else { + return this.verifyIdTokenAsync(idToken, audience); + } + } + + private async verifyIdTokenAsync(idToken: string, audience: string|string[]): + Promise { + if (!idToken) { + throw new Error('The verifyIdToken method requires an ID Token'); + } + + const certs = await this.getFederatedSignonCertsAsync(); + const login = this.verifySignedJwtWithCerts( + idToken, certs, audience, OAuth2Client.ISSUERS_); + return login; } /** @@ -499,47 +564,49 @@ export class OAuth2Client extends AuthClient { * are PEM encoded certificates. * @param {function=} callback Callback supplying the certificates */ - getFederatedSignonCerts(callback: BodyResponseCallback) { + getFederatedSignonCerts(): Promise; + getFederatedSignonCerts(callback: GetFederatedSignonCertsCallback): void; + getFederatedSignonCerts(callback?: GetFederatedSignonCertsCallback): + Promise|void { + if (callback) { + this.getFederatedSignonCertsAsync() + .then(r => callback(null, r.certs, r.res)) + .catch(callback); + } else { + return this.getFederatedSignonCertsAsync(); + } + } + + async getFederatedSignonCertsAsync(): Promise { const nowTime = (new Date()).getTime(); if (this.certificateExpiry && (nowTime < this.certificateExpiry.getTime())) { - callback(null, this.certificateCache); - return; - } - - this.transporter.request( - { - method: 'GET', - uri: OAuth2Client.GOOGLE_OAUTH2_FEDERATED_SIGNON_CERTS_URL_, - json: true - }, - (err, body, response) => { - if (err) { - callback( - new RequestError( - 'Failed to retrieve verification certificates: ' + err), - null, response); - return; - } - - const cacheControl = - response ? response.headers['cache-control'] : undefined; - let cacheAge = -1; - if (cacheControl) { - const pattern = new RegExp('max-age=([0-9]*)'); - const regexResult = pattern.exec(cacheControl as string); - if (regexResult && regexResult.length === 2) { - // Cache results with max-age (in seconds) - cacheAge = Number(regexResult[1]) * 1000; // milliseconds - } - } - - const now = new Date(); - this.certificateExpiry = - cacheAge === -1 ? null : new Date(now.getTime() + cacheAge); - this.certificateCache = body; - callback(null, body, response); - }); + return {certs: this.certificateCache}; + } + let res: AxiosResponse; + try { + res = await this.transporter.request( + {url: OAuth2Client.GOOGLE_OAUTH2_FEDERATED_SIGNON_CERTS_URL_}); + } catch (e) { + throw new Error('Failed to retrieve verification certificates: ' + e); + } + + const cacheControl = res ? res.headers['cache-control'] : undefined; + let cacheAge = -1; + if (cacheControl) { + const pattern = new RegExp('max-age=([0-9]*)'); + const regexResult = pattern.exec(cacheControl as string); + if (regexResult && regexResult.length === 2) { + // Cache results with max-age (in seconds) + cacheAge = Number(regexResult[1]) * 1000; // milliseconds + } + } + + const now = new Date(); + this.certificateExpiry = + cacheAge === -1 ? null : new Date(now.getTime() + cacheAge); + this.certificateCache = res.data; + return {certs: res.data, res}; } /** diff --git a/src/auth/refreshclient.ts b/src/auth/refreshclient.ts index bdb5097e..7e699cd5 100644 --- a/src/auth/refreshclient.ts +++ b/src/auth/refreshclient.ts @@ -14,12 +14,9 @@ * limitations under the License. */ -import * as request from 'request'; import * as stream from 'stream'; - -import {BodyResponseCallback} from './../transporters'; import {JWTInput} from './credentials'; -import {OAuth2Client} from './oauth2client'; +import {GetTokenResponse, OAuth2Client} from './oauth2client'; export class UserRefreshClient extends OAuth2Client { // TODO: refactor tests to make this private @@ -46,9 +43,9 @@ export class UserRefreshClient extends OAuth2Client { * @param {function=} callback Optional callback. * @private */ - protected refreshToken(ignored: null, callback?: BodyResponseCallback): - request.Request|void { - return super.refreshToken(this._refreshToken, callback); + protected async refreshToken(refreshToken?: string| + null): Promise { + return super.refreshToken(this._refreshToken); } /** @@ -58,45 +55,38 @@ export class UserRefreshClient extends OAuth2Client { * @param {function=} callback Optional callback. */ fromJSON(json: JWTInput, callback?: (err?: Error) => void) { - if (!json) { - if (callback) { - callback(new Error( - 'Must pass in a JSON object containing the user refresh token')); + try { + if (!json) { + throw new Error( + 'Must pass in a JSON object containing the user refresh token'); } - return; - } - if (json.type !== 'authorized_user') { - if (callback) { - callback(new Error( - 'The incoming JSON object does not have the "authorized_user" type')); + if (json.type !== 'authorized_user') { + throw new Error( + 'The incoming JSON object does not have the "authorized_user" type'); } - return; - } - if (!json.client_id) { - if (callback) { - callback(new Error( - 'The incoming JSON object does not contain a client_id field')); + if (!json.client_id) { + throw new Error( + 'The incoming JSON object does not contain a client_id field'); } - return; - } - if (!json.client_secret) { - if (callback) { - callback(new Error( - 'The incoming JSON object does not contain a client_secret field')); + if (!json.client_secret) { + throw new Error( + 'The incoming JSON object does not contain a client_secret field'); } - return; - } - if (!json.refresh_token) { + if (!json.refresh_token) { + throw new Error( + 'The incoming JSON object does not contain a refresh_token field'); + } + this._clientId = json.client_id; + this._clientSecret = json.client_secret; + this._refreshToken = json.refresh_token; + this.credentials.refresh_token = json.refresh_token; + } catch (e) { if (callback) { - callback(new Error( - 'The incoming JSON object does not contain a refresh_token field')); + callback(e); + } else { + throw e; } - return; } - this._clientId = json.client_id; - this._clientSecret = json.client_secret; - this._refreshToken = json.refresh_token; - this.credentials.refresh_token = json.refresh_token; if (callback) callback(); } @@ -106,28 +96,38 @@ export class UserRefreshClient extends OAuth2Client { * @param {object=} inputStream The input stream. * @param {function=} callback Optional callback. */ - fromStream(inputStream: stream.Readable, callback?: (err?: Error) => void) { - if (!inputStream) { - if (callback) { - setImmediate( - callback, - new Error( - 'Must pass in a stream containing the user refresh token.')); - } - return; + fromStream(inputStream: stream.Readable): Promise; + fromStream(inputStream: stream.Readable, callback: (err?: Error) => void): + void; + fromStream(inputStream: stream.Readable, callback?: (err?: Error) => void): + void|Promise { + if (callback) { + this.fromStreamAsync(inputStream).then(r => callback()).catch(callback); + } else { + return this.fromStreamAsync(inputStream); } - let s = ''; - inputStream.setEncoding('utf8'); - inputStream.on('data', (chunk) => { - s += chunk; - }); - inputStream.on('end', () => { - try { - const data = JSON.parse(s); - this.fromJSON(data, callback); - } catch (err) { - if (callback) callback(err); + } + + private async fromStreamAsync(inputStream: stream.Readable): Promise { + return new Promise((resolve, reject) => { + if (!inputStream) { + return reject(new Error( + 'Must pass in a stream containing the user refresh token.')); } + let s = ''; + inputStream.setEncoding('utf8'); + inputStream.on('data', (chunk) => { + s += chunk; + }); + inputStream.on('end', () => { + try { + const data = JSON.parse(s); + this.fromJSON(data); + return resolve(); + } catch (err) { + return reject(err); + } + }); }); } } diff --git a/src/options.ts b/src/options.ts new file mode 100644 index 00000000..0a4c38da --- /dev/null +++ b/src/options.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * 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. + */ + +// Accepts an options object passed from the user to the API. In the +// previous version of the API, it referred to a `Request` options object. +// Now it refers to an Axiox Request Config object. This is here to help +// ensure users don't pass invalid options when they upgrade from 0.x to 1.x. +// tslint:disable-next-line no-any +export function validate(options: any) { + const vpairs = [ + {invalid: 'uri', expected: 'url'}, {invalid: 'json', expected: 'data'}, + {invalid: 'qs', expected: 'params'} + ]; + for (const pair of vpairs) { + if (options[pair.invalid]) { + const e = `'${ + pair.invalid}' is not a valid configuration option. Please use '${ + pair.expected}' instead. This library is using Axios for requests. Please see https://github.com/axios/axios to learn more about the valid request options.`; + throw new Error(e); + } + } +} diff --git a/src/transporters.ts b/src/transporters.ts index e1da0f92..794a9fa0 100644 --- a/src/transporters.ts +++ b/src/transporters.ts @@ -14,34 +14,27 @@ * limitations under the License. */ -import * as request from 'request'; +import axios, {AxiosError, AxiosPromise, AxiosRequestConfig, AxiosResponse} from 'axios'; +import {validate} from './options'; // tslint:disable-next-line no-var-requires const pkg = require('../../package.json'); export interface Transporter { - request(opts: request.Options, callback?: BodyResponseCallback): - request.Request; + request(opts: AxiosRequestConfig): AxiosPromise; + request(opts: AxiosRequestConfig, callback?: BodyResponseCallback): + void; + request(opts: AxiosRequestConfig, callback?: BodyResponseCallback): + AxiosPromise|void; } -export interface BodyResponseCallback { +export interface BodyResponseCallback { // The `body` object is a truly dynamic type. It must be `any`. // tslint:disable-next-line no-any - (err: Error|null, body?: any, res?: request.RequestResponse|null): void; + (err: Error|null, res?: AxiosResponse|null): void; } -export class RequestError extends Error { - code?: number; - errors: Error[]; -} - -export interface BodyResponse { - error?: string|{ - code?: number; - message?: string; - errors: Error[]; - }; -} +export interface RequestError extends AxiosError { errors: Error[]; } export class DefaultTransporter { /** @@ -51,10 +44,10 @@ export class DefaultTransporter { /** * Configures request options before making a request. - * @param {object} opts Options to configure. - * @return {object} Configured options. + * @param opts AxiosRequestConfig options. + * @return Configured options. */ - configure(opts: request.Options): request.Options { + configure(opts: AxiosRequestConfig = {}): AxiosRequestConfig { // set transporter user agent opts.headers = opts.headers || {}; if (!opts.headers['User-Agent']) { @@ -69,66 +62,70 @@ export class DefaultTransporter { } /** - * Makes a request with given options and invokes callback. - * @param {object} opts Options. - * @param {Function=} callback Optional callback. - * @return {Request} Request object + * Makes a request using Axios with given options. + * @param opts AxiosRequestConfig options. + * @param callback optional callback that contains AxiosResponse object. + * @return AxiosPromise, assuming no callback is passed. */ - request(opts: request.Options, callback?: BodyResponseCallback) { + request(opts: AxiosRequestConfig): AxiosPromise; + request(opts: AxiosRequestConfig, callback?: BodyResponseCallback): + void; + request(opts: AxiosRequestConfig, callback?: BodyResponseCallback): + AxiosPromise|void { + // ensure the user isn't passing in request-style options opts = this.configure(opts); - const uri = (opts as request.OptionsWithUri).uri as string || - (opts as request.OptionsWithUrl).url as string; - return request(uri, opts, this.wrapCallback_(callback)); + try { + validate(opts); + } catch (e) { + if (callback) { + return callback(e); + } else { + throw e; + } + } + + if (callback) { + axios(opts) + .then(r => { + callback(null, r); + }) + .catch(e => { + callback(this.processError(e)); + }); + } else { + return axios(opts).catch(e => { + throw this.processError(e); + }); + } } - /** - * Wraps the response callback. - * @param {Function=} callback Optional callback. - * @return {Function} Wrapped callback function. - * @private - */ - private wrapCallback_(callback?: BodyResponseCallback): - request.RequestCallback { - return (err: RequestError, res: request.RequestResponse, - // the body is either a string or a JSON object - // tslint:disable-next-line no-any - body: string|any) => { - if (err || !body) { - return callback && callback(err, body, res); - } - // Only and only application/json responses should - // be decoded back to JSON, but there are cases API back-ends - // responds without proper content-type. - try { - body = JSON.parse(body as string); - } catch (err) { - /* no op */ - } - if (body && body.error && res.statusCode !== 200) { - if (typeof body.error === 'string') { - err = new RequestError(body.error); - (err as RequestError).code = res.statusCode; - } else if (Array.isArray(body.error.errors)) { - err = new RequestError( - body.error.errors.map((err2: Error) => err2.message).join('\n')); - (err as RequestError).code = body.error.code; - (err as RequestError).errors = body.error.errors; - } else { - err = new RequestError(body.error.message); - (err as RequestError).code = body.error.code || res.statusCode; - } - body = null; - } else if (res.statusCode && res.statusCode >= 400) { - // Consider all 4xx and 5xx responses errors. - err = new RequestError(body); - (err as RequestError).code = res.statusCode; - body = null; - } - if (callback) { - callback(err, body, res); + /** + * Changes the error to include details from the body. + */ + private processError(e: AxiosError): RequestError { + const res = e.response; + const err = e as RequestError; + const body = res ? res.data : null; + if (res && body && body.error && res.status !== 200) { + if (typeof body.error === 'string') { + err.message = body.error; + err.code = res.status.toString(); + } else if (Array.isArray(body.error.errors)) { + err.message = + body.error.errors.map((err2: Error) => err2.message).join('\n'); + err.code = body.error.code; + err.errors = body.error.errors; + } else { + err.message = body.error.message; + err.code = body.error.code || res.status; } - }; + } else if (res && res.status >= 400) { + // Consider all 4xx and 5xx responses errors. + err.message = body; + err.code = res.status.toString(); + } + return err; } } diff --git a/test/test.compute.ts b/test/test.compute.ts index 448e1763..bc41f49e 100644 --- a/test/test.compute.ts +++ b/test/test.compute.ts @@ -15,8 +15,8 @@ */ import * as assert from 'assert'; +import {AxiosRequestConfig} from 'axios'; import * as nock from 'nock'; -import * as request from 'request'; import {Compute} from '../src/auth/computeclient'; import {Credentials} from '../src/auth/credentials'; @@ -25,7 +25,6 @@ import {GoogleAuth} from '../src/auth/googleauth'; nock.disableNetConnect(); describe('Initial credentials', () => { - it('should create a dummy refresh token string', () => { // It is important that the compute client is created with a refresh token // value filled in, or else the rest of the logic will not work. @@ -36,6 +35,10 @@ describe('Initial credentials', () => { }); describe('Compute auth client', () => { + afterEach(() => { + nock.cleanAll(); + }); + // set up compute client. let compute: Compute; beforeEach(() => { @@ -44,29 +47,23 @@ describe('Compute auth client', () => { }); it('should get an access token for the first request', done => { - const scope = - nock('http://metadata.google.internal') - .get( - '/computeMetadata/v1beta1/instance/service-accounts/default/token') - .reply(200, {access_token: 'abc123', expires_in: 10000}); - compute.request({uri: 'http://foo'}, () => { + nock('http://metadata.google.internal') + .get('/computeMetadata/v1beta1/instance/service-accounts/default/token') + .reply(200, {access_token: 'abc123', expires_in: 10000}); + compute.request({url: 'http://foo'}, () => { assert.equal(compute.credentials.access_token, 'abc123'); - scope.done(); done(); }); }); it('should refresh if access token has expired', (done) => { - const scope = - nock('http://metadata.google.internal') - .get( - '/computeMetadata/v1beta1/instance/service-accounts/default/token') - .reply(200, {access_token: 'abc123', expires_in: 10000}); + nock('http://metadata.google.internal') + .get('/computeMetadata/v1beta1/instance/service-accounts/default/token') + .reply(200, {access_token: 'abc123', expires_in: 10000}); compute.credentials.access_token = 'initial-access-token'; compute.credentials.expiry_date = (new Date()).getTime() - 10000; - compute.request({uri: 'http://foo'}, () => { + compute.request({url: 'http://foo'}, () => { assert.equal(compute.credentials.access_token, 'abc123'); - scope.done(); done(); }); }); @@ -79,7 +76,7 @@ describe('Compute auth client', () => { .reply(200, {access_token: 'abc123', expires_in: 10000}); compute.credentials.access_token = 'initial-access-token'; compute.credentials.expiry_date = (new Date()).getTime() + 10000; - compute.request({uri: 'http://foo'}, () => { + compute.request({url: 'http://foo'}, () => { assert.equal(compute.credentials.access_token, 'initial-access-token'); assert.equal(false, scope.isDone()); nock.cleanAll(); @@ -104,58 +101,58 @@ describe('Compute auth client', () => { expiry_date: (new Date(9999, 1, 1)).getTime() }; - // Mock the _makeRequest method to return a 403. - compute._makeRequest = (opts, callback) => { - callback( - null, 'a weird response body', - {statusCode: 403} as request.RequestResponse); - return {} as request.Request; - }; + nock('http://foo').get('/').twice().reply(403, 'a weird response body'); + nock('http://metadata.google.internal') + .get( + '/computeMetadata/v1beta1/instance/service-accounts/default/token') + .reply(403, 'a weird response body'); - compute.request(({} as request.OptionsWithUrl), (err, result, response) => { + compute.request({url: 'http://foo'}, (err, response) => { assert(response); - assert.equal(403, response ? response.statusCode : 0); - assert.equal( + assert.equal(403, response ? response.status : 0); + const expected = 'A Forbidden error was returned while attempting to retrieve an access ' + - 'token for the Compute Engine built-in service account. This may be because the ' + - 'Compute Engine instance does not have the correct permission scopes specified.', - err ? err.message : null); + 'token for the Compute Engine built-in service account. This may be because the ' + + 'Compute Engine instance does not have the correct permission scopes specified. ' + + 'Could not refresh access token.'; + assert.equal(expected, err ? err.message : null); done(); }); }); - it('should return a helpful message on request response.statusCode 404', (done) => { - // Mock the credentials object. - compute.credentials = { - refresh_token: 'hello', - access_token: 'goodbye', - expiry_date: (new Date(9999, 1, 1)).getTime() - }; + it('should return a helpful message on request response.statusCode 404', + (done) => { + // Mock the credentials object. + compute.credentials = { + refresh_token: 'hello', + access_token: 'goodbye', + expiry_date: (new Date(9999, 1, 1)).getTime() + }; - // Mock the _makeRequest method to return a 404. - compute._makeRequest = (opts, callback) => { - callback( - null, 'a weird response body', - {statusCode: 404} as request.RequestResponse); - return {} as request.Request; - }; + // Mock the request method to return a 404. + nock('http://foo') + .get('/') + .twice() + .reply(404, 'a weird response body'); - compute.request(({} as request.OptionsWithUri), (err, result, response) => { - assert.equal(404, response ? response.statusCode : 0); - assert.equal( - 'A Not Found error was returned while attempting to retrieve an access' + - 'token for the Compute Engine built-in service account. This may be because the ' + - 'Compute Engine instance does not have any permission scopes specified.', - err ? err.message : null); - done(); - }); - }); + compute.request({url: 'http://foo'}, (err, response) => { + assert.equal(404, response ? response.status : 0); + assert.equal( + 'A Not Found error was returned while attempting to retrieve an access' + + 'token for the Compute Engine built-in service account. This may be because the ' + + 'Compute Engine instance does not have any permission scopes specified. ' + + 'a weird response body', + err ? err.message : null); + done(); + }); + }); it('should return a helpful message on token refresh response.statusCode 403', (done) => { nock('http://metadata.google.internal') .get( '/computeMetadata/v1beta1/instance/service-accounts/default/token') + .twice() .reply(403, 'a weird response body'); // Mock the credentials object with a null access token, to force a @@ -166,43 +163,44 @@ describe('Compute auth client', () => { expiry_date: 1 }; - compute.request(({} as request.OptionsWithUri), (err, result, response) => { - assert.equal(403, response ? response.statusCode : null); - assert.equal( + compute.request({}, (err, response) => { + assert.equal(403, response ? response.status : null); + const expected = 'A Forbidden error was returned while attempting to retrieve an access ' + - 'token for the Compute Engine built-in service account. This may be because the ' + - 'Compute Engine instance does not have the correct permission scopes specified. ' + - 'Could not refresh access token.', - err ? err.message : null); + 'token for the Compute Engine built-in service account. This may be because the ' + + 'Compute Engine instance does not have the correct permission scopes specified. ' + + 'Could not refresh access token.'; + assert.equal(expected, err ? err.message : null); nock.cleanAll(); done(); }); }); - it('should return a helpful message on token refresh response.statusCode 404', done => { - nock('http://metadata.google.internal') - .get( - '/computeMetadata/v1beta1/instance/service-accounts/default/token') - .reply(404, 'a weird body'); + it('should return a helpful message on token refresh response.statusCode 404', + done => { + nock('http://metadata.google.internal') + .get( + '/computeMetadata/v1beta1/instance/service-accounts/default/token') + .reply(404, 'a weird body'); - // Mock the credentials object with a null access token, to force a - // refresh. - compute.credentials = { - refresh_token: 'hello', - access_token: undefined, - expiry_date: 1 - } as Credentials; - - compute.request(({} as request.OptionsWithUri), (err, result, response) => { - assert.equal(404, response ? response.statusCode : null); - assert.equal( - 'A Not Found error was returned while attempting to retrieve an access' + - 'token for the Compute Engine built-in service account. This may be because the ' + - 'Compute Engine instance does not have any permission scopes specified. Could not ' + - 'refresh access token.', - err ? err.message : null); - done(); - }); - }); + // Mock the credentials object with a null access token, to force a + // refresh. + compute.credentials = { + refresh_token: 'hello', + access_token: undefined, + expiry_date: 1 + } as Credentials; + + compute.request({}, (err, response) => { + assert.equal(404, response ? response.status : null); + assert.equal( + 'A Not Found error was returned while attempting to retrieve an access' + + 'token for the Compute Engine built-in service account. This may be because the ' + + 'Compute Engine instance does not have any permission scopes specified. Could not ' + + 'refresh access token.', + err ? err.message : null); + done(); + }); + }); }); }); diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 74716f2c..75b7cffe 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -15,20 +15,36 @@ */ import * as assert from 'assert'; +import {AxiosPromise, AxiosRequestConfig, AxiosResponse} from 'axios'; import * as fs from 'fs'; +import * as http from 'http'; import * as nock from 'nock'; import * as path from 'path'; -import * as request from 'request'; +import * as stream from 'stream'; -import {Compute} from '../src/auth/computeclient'; import {GoogleAuth} from '../src/auth/googleauth'; import {JWT} from '../src/auth/jwtclient'; -import {OAuth2Client} from '../src/auth/oauth2client'; import {UserRefreshClient} from '../src/auth/refreshclient'; import {BodyResponseCallback, DefaultTransporter} from '../src/transporters'; nock.disableNetConnect(); +afterEach(() => { + nock.cleanAll(); +}); + +function createIsGCENock(isGCE = true) { + nock('http://metadata.google.internal').get('/').reply(200, null, { + 'metadata-flavor': 'Google' + }); +} + +function createGetProjectIdNock(projectId: string) { + nock('http://169.254.169.254') + .get('/computeMetadata/v1/project/project-id') + .reply(200, projectId); +} + // Mocks the transporter class to simulate GCE. class MockTransporter extends DefaultTransporter { isGCE: boolean; @@ -43,25 +59,39 @@ class MockTransporter extends DefaultTransporter { this.throwError = throwError; this.executionCount = 0; } - request(options: request.OptionsWithUri, callback: BodyResponseCallback) { - if (options.method === 'GET' && - options.uri === 'http://metadata.google.internal') { + request(opts: AxiosRequestConfig): AxiosPromise; + request(opts: AxiosRequestConfig, callback?: BodyResponseCallback): + void; + request(opts: AxiosRequestConfig, callback?: BodyResponseCallback): + AxiosPromise|void { + if (opts.url === 'http://metadata.google.internal') { this.executionCount += 1; let err = null; - const response = {headers: {} as request.Headers}; + const response = {headers: {} as http.IncomingHttpHeaders} as + AxiosResponse; if (this.throwError) { err = new Error('blah'); } else if (this.isGCE) { response.headers['metadata-flavor'] = 'Google'; } - callback(err, null, response as request.RequestResponse); - return {} as request.Request; + if (callback) { + return callback(err, response); + } else { + return Promise.resolve(response); + } } else { - throw new Error('unexpected request'); + const err = new Error('unexpected request'); + if (callback) { + return callback(err); + } else { + throw err; + } } } } + + // Creates a standard JSON auth object for testing. function createJwtJSON() { return { @@ -92,19 +122,6 @@ function pathJoin(item1: string, item2: string) { return item1 + ':' + item2; } -// Returns the value. -function returns(value: {}) { - return () => { - return value; - }; -} - -function callsBack(err: {}|null, value?: {}) { - return (callback: Function) => { - callback(err, value); - }; -} - // Blocks the GOOGLE_APPLICATION_CREDENTIALS by default. This is necessary in // case it is actually set on the host machine executing the test. function blockGoogleApplicationCredentialEnvironmentVariable(auth: GoogleAuth) { @@ -139,26 +156,8 @@ function insertWellKnownFilePathIntoAuth( }; } -const noop = () => undefined; - -// Executes the doneCallback after the nTH call. -function doneWhen(doneCallback: Function, count: number) { - let i = 0; - - return () => { - ++i; - - if (i === count) { - doneCallback(); - } else if (i > count) { - throw new Error('Called too many times. Test error?'); - } - }; -} - describe('GoogleAuth', () => { describe('.fromJson', () => { - it('should error on null json', (done) => { const auth = new GoogleAuth(); // Test verifies invalid parameter tests, which requires cast to any. @@ -200,9 +199,6 @@ describe('GoogleAuth', () => { insertEnvironmentVariableIntoAuth( auth, 'GCLOUD_PROJECT', STUB_PROJECT); }); - afterEach(() => { - nock.cleanAll(); - }); describe('With no added query string parameters', () => { it('should make a request with the api key', (done) => { @@ -223,11 +219,11 @@ describe('GoogleAuth', () => { { url: BASE_URL + ENDPOINT, method: 'POST', - json: '{"test": true}' + data: {'test': true} }, - (err2, body) => { + (err2, res) => { assert.strictEqual(err2, null); - assert.strictEqual(RESPONSE_BODY, body); + assert.strictEqual(RESPONSE_BODY, res!.data); fakeService.done(); done(); }); @@ -258,12 +254,12 @@ describe('GoogleAuth', () => { { url: BASE_URL + ENDPOINT, method: 'POST', - json: '{"test": true}', - qs: OTHER_QS_PARAM + data: {'test': true}, + params: OTHER_QS_PARAM }, - (err2, body) => { + (err2, res) => { assert.strictEqual(err2, null); - assert.strictEqual(RESPONSE_BODY, body); + assert.strictEqual(RESPONSE_BODY, res!.data); fakeService.done(); done(); }); @@ -275,7 +271,6 @@ describe('GoogleAuth', () => { }); describe('JWT token', () => { - it('should error on empty json', (done) => { const auth = new GoogleAuth(); auth.fromJSON({}, (err) => { @@ -311,10 +306,7 @@ describe('GoogleAuth', () => { const auth = new GoogleAuth(); auth.fromJSON(json, (err, result) => { assert.equal(null, err); - assert(result); - if (result) { - assert.equal(json.client_email, (result as JWT).email); - } + assert.equal(json.client_email, (result as JWT).email); done(); }); }); @@ -324,10 +316,7 @@ describe('GoogleAuth', () => { const auth = new GoogleAuth(); auth.fromJSON(json, (err, result) => { assert.equal(null, err); - assert(result); - if (result) { - assert.equal(json.private_key, (result as JWT).key); - } + assert.equal(json.private_key, (result as JWT).key); done(); }); }); @@ -337,10 +326,7 @@ describe('GoogleAuth', () => { const auth = new GoogleAuth(); auth.fromJSON(json, (err, result) => { assert.equal(null, err); - assert(result); - if (result) { - assert.equal(null, (result as JWT).scopes); - } + assert.equal(null, (result as JWT).scopes); done(); }); }); @@ -350,10 +336,7 @@ describe('GoogleAuth', () => { const auth = new GoogleAuth(); auth.fromJSON(json, (err, result) => { assert.equal(null, err); - assert(result); - if (result) { - assert.equal(null, (result as JWT).subject); - } + assert.equal(null, (result as JWT).subject); done(); }); }); @@ -363,10 +346,7 @@ describe('GoogleAuth', () => { const auth = new GoogleAuth(); auth.fromJSON(json, (err, result) => { assert.equal(null, err); - assert(result); - if (result) { - assert.equal(null, (result as JWT).keyFile); - } + assert.equal(null, (result as JWT).keyFile); done(); }); }); @@ -420,7 +400,6 @@ describe('GoogleAuth', () => { }); describe('.fromStream', () => { - it('should error on null stream', (done) => { const auth = new GoogleAuth(); // Test verifies invalid parameter tests, which requires cast to any. @@ -444,16 +423,13 @@ describe('GoogleAuth', () => { const auth = new GoogleAuth(); auth.fromStream(stream, (err, result) => { assert.equal(null, err); - assert(result); const jwt = result as JWT; - if (jwt) { - // Ensure that the correct bits were pulled from the stream. - assert.equal(json.private_key, jwt.key); - assert.equal(json.client_email, jwt.email); - assert.equal(null, jwt.keyFile); - assert.equal(null, jwt.subject); - assert.equal(null, jwt.scope); - } + // Ensure that the correct bits were pulled from the stream. + assert.equal(json.private_key, jwt.key); + assert.equal(json.client_email, jwt.email); + assert.equal(null, jwt.keyFile); + assert.equal(null, jwt.subject); + assert.equal(null, jwt.scope); done(); }); }); @@ -471,155 +447,154 @@ describe('GoogleAuth', () => { const auth = new GoogleAuth(); auth.fromStream(stream, (err, result) => { assert.ifError(err); - assert(result); // Ensure that the correct bits were pulled from the stream. - if (result) { - const rc = result as UserRefreshClient; - assert.equal(json.client_id, rc._clientId); - assert.equal(json.client_secret, rc._clientSecret); - assert.equal(json.refresh_token, rc._refreshToken); - } + const rc = result as UserRefreshClient; + assert.equal(json.client_id, rc._clientId); + assert.equal(json.client_secret, rc._clientSecret); + assert.equal(json.refresh_token, rc._refreshToken); done(); }); }); }); describe('._getApplicationCredentialsFromFilePath', () => { - - it('should not error on valid symlink', (done) => { + it('should not error on valid symlink', async () => { const auth = new GoogleAuth(); - auth._getApplicationCredentialsFromFilePath( - './test/fixtures/goodlink', (err) => { - assert.equal(false, err instanceof Error); - done(); - }); + await auth._getApplicationCredentialsFromFilePath( + './test/fixtures/goodlink'); }); - it('should error on invalid symlink', (done) => { + it('should error on invalid symlink', async () => { const auth = new GoogleAuth(); - auth._getApplicationCredentialsFromFilePath( - './test/fixtures/badlink', (err) => { - assert.equal(true, err instanceof Error); - done(); - }); + try { + await auth._getApplicationCredentialsFromFilePath( + './test/fixtures/badlink'); + } catch (e) { + return; + } + assert.fail('failed to throw'); }); - it('should error on valid link to invalid data', (done) => { + it('should error on valid link to invalid data', async () => { const auth = new GoogleAuth(); - auth._getApplicationCredentialsFromFilePath( - './test/fixtures/emptylink', (err) => { - assert.equal(true, err instanceof Error); - done(); - }); + try { + await auth._getApplicationCredentialsFromFilePath( + './test/fixtures/emptylink'); + } catch (e) { + return; + } + assert.fail('failed to throw'); }); - it('should error on null file path', (done) => { + it('should error on null file path', async () => { const auth = new GoogleAuth(); - (auth as - // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any - any) - ._getApplicationCredentialsFromFilePath(null, (err: Error) => { - assert.equal(true, err instanceof Error); - done(); - }); + try { + // Test verifies invalid parameter tests, which requires cast to any. + // tslint:disable-next-line no-any + await (auth as any)._getApplicationCredentialsFromFilePath(null); + } catch (e) { + return; + } + assert.fail('failed to throw'); }); - it('should error on empty file path', (done) => { + it('should error on empty file path', async () => { const auth = new GoogleAuth(); - auth._getApplicationCredentialsFromFilePath('', (err) => { - assert.equal(true, err instanceof Error); - done(); - }); + try { + await auth._getApplicationCredentialsFromFilePath(''); + } catch (e) { + return; + } + assert.fail('failed to throw'); }); - it('should error on non-string file path', (done) => { + it('should error on non-string file path', async () => { const auth = new GoogleAuth(); - auth._getApplicationCredentialsFromFilePath( - // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any - (2 as any), (err: Error|null) => { - assert.equal(true, err instanceof Error); - done(); - }); + try { + // Test verifies invalid parameter tests, which requires cast to any. + // tslint:disable-next-line no-any + await auth._getApplicationCredentialsFromFilePath(2 as any); + } catch (e) { + return; + } + assert.fail('failed to throw'); }); - it('should error on invalid file path', (done) => { + it('should error on invalid file path', async () => { const auth = new GoogleAuth(); - auth._getApplicationCredentialsFromFilePath( - './nonexistantfile.json', (err) => { - - assert.equal(true, err instanceof Error); - done(); - }); + try { + await auth._getApplicationCredentialsFromFilePath( + './nonexistantfile.json'); + } catch (e) { + return; + } + assert.fail('failed to throw'); }); - it('should error on directory', (done) => { + it('should error on directory', async () => { // Make sure that the following path actually does point to a directory. const directory = './test/fixtures'; assert.equal(true, fs.lstatSync(directory).isDirectory()); - - // Execute. const auth = new GoogleAuth(); - auth._getApplicationCredentialsFromFilePath(directory, (err) => { - - assert.equal(true, err instanceof Error); - done(); - }); + try { + await auth._getApplicationCredentialsFromFilePath(directory); + } catch (e) { + return; + } + assert.fail('failed to throw'); }); - it('should handle errors thrown from createReadStream', (done) => { + it('should handle errors thrown from createReadStream', async () => { // Set up a mock to throw from the createReadStream method. const auth = new GoogleAuth(); auth._createReadStream = () => { - throw new Error('Hans and Chewbacca'); + throw new Error('Han and Chewbacca'); }; - // Execute. - auth._getApplicationCredentialsFromFilePath( - './test/fixtures/private.json', (err) => { - assert.equal( - true, - stringEndsWith(err ? err.message : '', 'Hans and Chewbacca')); - done(); - }); + try { + await auth._getApplicationCredentialsFromFilePath( + './test/fixtures/private.json'); + } catch (e) { + assert.equal(true, stringEndsWith(e.message, 'Han and Chewbacca')); + return; + } + assert.fail('failed to throw'); }); - it('should handle errors thrown from fromStream', (done) => { + it('should handle errors thrown from fromStream', async () => { // Set up a mock to throw from the fromStream method. const auth = new GoogleAuth(); auth.fromStream = () => { throw new Error('Darth Maul'); }; - - // Execute. - auth._getApplicationCredentialsFromFilePath( - './test/fixtures/private.json', (err) => { - - assert.equal( - true, stringEndsWith(err ? err.message : '', 'Darth Maul')); - done(); - }); + try { + await auth._getApplicationCredentialsFromFilePath( + './test/fixtures/private.json'); + } catch (e) { + assert(stringEndsWith(e.message, 'Darth Maul')); + return; + } + assert.fail('failed to throw'); }); - it('should handle errors passed from fromStream', (done) => { + it('should handle errors passed from fromStream', async () => { // Set up a mock to return an error from the fromStream method. const auth = new GoogleAuth(); - auth.fromStream = (stream, callback) => { - if (callback) callback(new Error('Princess Leia')); + auth.fromStream = (streamInput: stream.Readable) => { + throw new Error('Princess Leia'); }; - // Execute. - auth._getApplicationCredentialsFromFilePath( - './test/fixtures/private.json', (err) => { - - assert.equal( - true, stringEndsWith(err ? err.message : '', 'Princess Leia')); - done(); - }); + try { + await auth._getApplicationCredentialsFromFilePath( + './test/fixtures/private.json'); + } catch (e) { + assert(stringEndsWith(e.message, 'Princess Leia')); + return; + } + assert.fail('failed to throw'); }); - it('should correctly read the file and create a valid JWT', (done) => { + it('should correctly read the file and create a valid JWT', async () => { // Read the contents of the file into a json object. const fileContents = fs.readFileSync('./test/fixtures/private.json', 'utf-8'); @@ -627,83 +602,53 @@ describe('GoogleAuth', () => { // Now pass the same path to the auth loader. const auth = new GoogleAuth(); - auth._getApplicationCredentialsFromFilePath( - './test/fixtures/private.json', (err, result) => { - assert.equal(null, err); - assert(result); - const jwt = result as JWT; - if (result) { - assert.equal(json.private_key, jwt.key); - assert.equal(json.client_email, jwt.email); - assert.equal(null, jwt.keyFile); - assert.equal(null, jwt.subject); - assert.equal(null, jwt.scope); - } - done(); - }); + const result = await auth._getApplicationCredentialsFromFilePath( + './test/fixtures/private.json'); + assert(result); + const jwt = result as JWT; + assert.equal(json.private_key, jwt.key); + assert.equal(json.client_email, jwt.email); + assert.equal(null, jwt.keyFile); + assert.equal(null, jwt.subject); + assert.equal(null, jwt.scope); }); }); describe('._tryGetApplicationCredentialsFromEnvironmentVariable', () => { - - it('should return false when env const is not set', (done) => { + it('should return null when env const is not set', async () => { // Set up a mock to return a null path string. const auth = new GoogleAuth(); insertEnvironmentVariableIntoAuth(auth, 'GOOGLE_APPLICATION_CREDENTIALS'); - - // The test ends successfully after 1 step has completed. - const step = doneWhen(done, 1); - - // Execute. - const handled = - auth._tryGetApplicationCredentialsFromEnvironmentVariable(() => { - step(); // This should not get called. - }); - - assert.equal(false, handled); - step(); // This should get called. + const client = + await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); + assert.equal(client, null); }); - it('should return false when env const is empty string', (done) => { + it('should return null when env const is empty string', async () => { // Set up a mock to return an empty path string. const auth = new GoogleAuth(); insertEnvironmentVariableIntoAuth( auth, 'GOOGLE_APPLICATION_CREDENTIALS', ''); - - // The test ends successfully after 1 step has completed. - const step = doneWhen(done, 1); - - // Execute. - const handled = - auth._tryGetApplicationCredentialsFromEnvironmentVariable(() => { - step(); // This should not get called. - }); - - assert.equal(false, handled); - step(); // This should get called. + const client = + await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); + assert.equal(client, null); }); - it('should handle invalid environment variable', (done) => { + it('should handle invalid environment variable', async () => { // Set up a mock to return a path to an invalid file. const auth = new GoogleAuth(); insertEnvironmentVariableIntoAuth( auth, 'GOOGLE_APPLICATION_CREDENTIALS', './nonexistantfile.json'); - // The test ends successfully after 2 steps have completed. - const step = doneWhen(done, 2); - - // Execute. - const handled = - auth._tryGetApplicationCredentialsFromEnvironmentVariable((err) => { - assert.equal(true, err instanceof Error); - step(); - }); - - assert.equal(true, handled); - step(); + try { + await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); + } catch (e) { + return; + } + assert.fail('failed to throw'); }); - it('should handle valid environment variable', (done) => { + it('should handle valid environment variable', async () => { // Set up a mock to return path to a valid credentials file. const auth = new GoogleAuth(); insertEnvironmentVariableIntoAuth( @@ -715,33 +660,20 @@ describe('GoogleAuth', () => { fs.readFileSync('./test/fixtures/private.json', 'utf-8'); const json = JSON.parse(fileContents); - // The test ends successfully after 2 steps have completed. - const step = doneWhen(done, 2); - // Execute. - const handled = auth._tryGetApplicationCredentialsFromEnvironmentVariable( - (err, result) => { - assert(result); - assert.equal(null, err); - const jwt = result as JWT; - if (result) { - assert.equal(json.private_key, jwt.key); - assert.equal(json.client_email, jwt.email); - assert.equal(null, jwt.keyFile); - assert.equal(null, jwt.subject); - assert.equal(null, jwt.scope); - } - step(); - }); - - assert.equal(true, handled); - step(); + const result = + await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); + const jwt = result as JWT; + assert.equal(json.private_key, jwt.key); + assert.equal(json.client_email, jwt.email); + assert.equal(null, jwt.keyFile); + assert.equal(null, jwt.subject); + assert.equal(null, jwt.scope); }); }); describe('._tryGetApplicationCredentialsFromWellKnownFile', () => { - - it('should build the correct directory for Windows', () => { + it('should build the correct directory for Windows', async () => { let correctLocation = false; // Set up mocks. @@ -752,17 +684,19 @@ describe('GoogleAuth', () => { auth._osPlatform = () => 'win32'; auth._fileExists = () => true; - auth._getApplicationCredentialsFromFilePath = (filePath) => { + auth._getApplicationCredentialsFromFilePath = (filePath: string) => { if (filePath === 'foo:gcloud:application_default_credentials.json') { correctLocation = true; } + return Promise.resolve({} as JWT); }; // Execute. - const handled = auth._tryGetApplicationCredentialsFromWellKnownFile(); + const result = + await auth._tryGetApplicationCredentialsFromWellKnownFile(); - assert.equal(true, handled); - assert.equal(true, correctLocation); + assert(result); + assert(correctLocation); }); it('should build the correct directory for non-Windows', () => { @@ -776,21 +710,22 @@ describe('GoogleAuth', () => { auth._osPlatform = () => 'linux'; auth._fileExists = () => true; - auth._getApplicationCredentialsFromFilePath = (filePath) => { + auth._getApplicationCredentialsFromFilePath = (filePath: string) => { if (filePath === 'foo:.config:gcloud:application_default_credentials.json') { correctLocation = true; } + return Promise.resolve({} as JWT); }; // Execute. - const handled = auth._tryGetApplicationCredentialsFromWellKnownFile(); + const client = auth._tryGetApplicationCredentialsFromWellKnownFile(); - assert.equal(true, handled); - assert.equal(true, correctLocation); + assert(client); + assert(correctLocation); }); - it('should fail on Windows when APPDATA is not defined', (done) => { + it('should fail on Windows when APPDATA is not defined', async () => { // Set up mocks. const auth = new GoogleAuth(); blockGoogleApplicationCredentialEnvironmentVariable(auth); @@ -798,22 +733,16 @@ describe('GoogleAuth', () => { auth._pathJoin = pathJoin; auth._osPlatform = () => 'win32'; auth._fileExists = () => true; - auth._getApplicationCredentialsFromFilePath = noop; - - // The test ends successfully after 1 step has completed. - const step = doneWhen(done, 1); - - // Execute. - const handled = - auth._tryGetApplicationCredentialsFromWellKnownFile(() => { - step(); // Should not get called. - }); - - assert.equal(false, handled); - step(); // Should get called. + auth._getApplicationCredentialsFromFilePath = + (filePath: string): Promise => { + return Promise.resolve({} as JWT); + }; + const result = + await auth._tryGetApplicationCredentialsFromWellKnownFile(); + assert.equal(null, result); }); - it('should fail on non-Windows when HOME is not defined', (done) => { + it('should fail on non-Windows when HOME is not defined', async () => { // Set up mocks. const auth = new GoogleAuth(); blockGoogleApplicationCredentialEnvironmentVariable(auth); @@ -821,22 +750,18 @@ describe('GoogleAuth', () => { auth._pathJoin = pathJoin; auth._osPlatform = () => 'linux'; auth._fileExists = () => true; - auth._getApplicationCredentialsFromFilePath = noop; - - // The test ends successfully after 1 step has completed. - const step = doneWhen(done, 1); + auth._getApplicationCredentialsFromFilePath = + (filePath: string): Promise => { + return Promise.resolve({} as JWT); + }; // Execute. - const handled = - auth._tryGetApplicationCredentialsFromWellKnownFile(() => { - step(); // Should not get called. - }); - - assert.equal(false, handled); - step(); // Should get called. + const result = + await auth._tryGetApplicationCredentialsFromWellKnownFile(); + assert.equal(null, result); }); - it('should fail on Windows when file does not exist', (done) => { + it('should fail on Windows when file does not exist', async () => { // Set up mocks. const auth = new GoogleAuth(); blockGoogleApplicationCredentialEnvironmentVariable(auth); @@ -844,22 +769,18 @@ describe('GoogleAuth', () => { auth._pathJoin = pathJoin; auth._osPlatform = () => 'win32'; auth._fileExists = () => false; - auth._getApplicationCredentialsFromFilePath = noop; - - // The test ends successfully after 1 step has completed. - const step = doneWhen(done, 1); + auth._getApplicationCredentialsFromFilePath = + (filePath: string): Promise => { + return Promise.resolve({} as JWT); + }; // Execute. - const handled = - auth._tryGetApplicationCredentialsFromWellKnownFile(() => { - step(); // Should not get called. - }); - - assert.equal(false, handled); - step(); // Should get called. + const result = + await auth._tryGetApplicationCredentialsFromWellKnownFile(); + assert.equal(null, result); }); - it('should fail on non-Windows when file does not exist', (done) => { + it('should fail on non-Windows when file does not exist', async () => { // Set up mocks. const auth = new GoogleAuth(); blockGoogleApplicationCredentialEnvironmentVariable(auth); @@ -867,23 +788,19 @@ describe('GoogleAuth', () => { auth._pathJoin = pathJoin; auth._osPlatform = () => 'linux'; auth._fileExists = () => false; - auth._getApplicationCredentialsFromFilePath = noop; - - // The test ends successfully after 1 step has completed. - const step = doneWhen(done, 1); + auth._getApplicationCredentialsFromFilePath = + (filePath: string): Promise => { + return Promise.resolve({} as JWT); + }; // Execute. - const handled = - auth._tryGetApplicationCredentialsFromWellKnownFile(() => { - step(); // Should not get called. - }); - - assert.equal(false, handled); - step(); // Should get called. + const result = + await auth._tryGetApplicationCredentialsFromWellKnownFile(); + assert.equal(null, result); }); }); - it('should succeeds on Windows', (done) => { + it('should succeeds on Windows', async () => { // Set up mocks. const auth = new GoogleAuth(); blockGoogleApplicationCredentialEnvironmentVariable(auth); @@ -891,32 +808,16 @@ describe('GoogleAuth', () => { auth._pathJoin = pathJoin; auth._osPlatform = () => 'win32'; auth._fileExists = () => true; - - auth._getApplicationCredentialsFromFilePath = (filePath, callback) => { - if (callback) { - const client = new JWT('hello'); - callback(null, client); - } + auth._getApplicationCredentialsFromFilePath = (filePath: string) => { + return Promise.resolve(new JWT('hello')); }; - // The test ends successfully after 2 steps have completed. - const step = doneWhen(done, 2); - // Execute. - const handled = - auth._tryGetApplicationCredentialsFromWellKnownFile((err, result) => { - assert.equal(null, err); - assert(result); - if (result) { - assert.equal('hello', (result as JWT).email); - } - step(); - }); - assert.equal(true, handled); - step(); + const result = await auth._tryGetApplicationCredentialsFromWellKnownFile(); + assert.equal('hello', (result as JWT)!.email); }); - it('should succeeds on non-Windows', (done) => { + it('should succeeds on non-Windows', async () => { // Set up mocks. const auth = new GoogleAuth(); blockGoogleApplicationCredentialEnvironmentVariable(auth); @@ -925,28 +826,16 @@ describe('GoogleAuth', () => { auth._osPlatform = () => 'linux'; auth._fileExists = () => true; - auth._getApplicationCredentialsFromFilePath = (filePath, callback) => { - if (callback) { - callback(null, new JWT('hello')); - } + auth._getApplicationCredentialsFromFilePath = (filePath: string) => { + return Promise.resolve(new JWT('hello')); }; - // The test ends successfully after 2 steps have completed. - const step = doneWhen(done, 2); - // Execute. - const handled = - auth._tryGetApplicationCredentialsFromWellKnownFile((err, result) => { - assert.equal(null, err); - assert.equal('hello', (result as JWT).email); - step(); - }); - - assert.equal(true, handled); - step(); + const result = await auth._tryGetApplicationCredentialsFromWellKnownFile(); + assert.equal('hello', (result as JWT).email); }); - it('should pass along a failure on Windows', (done) => { + it('should pass along a failure on Windows', async () => { // Set up mocks. const auth = new GoogleAuth(); blockGoogleApplicationCredentialEnvironmentVariable(auth); @@ -955,27 +844,22 @@ describe('GoogleAuth', () => { auth._osPlatform = () => 'win32'; auth._fileExists = () => true; - auth._getApplicationCredentialsFromFilePath = (filePath, callback) => { - if (callback) callback(new Error('hello')); + auth._getApplicationCredentialsFromFilePath = (filePath: string) => { + throw new Error('hello'); }; - // The test ends successfully after 2 steps have completed. - const step = doneWhen(done, 2); - // Execute. - const handled = - auth._tryGetApplicationCredentialsFromWellKnownFile((err, result) => { - assert(err); - assert.equal('hello', err ? err.message : ''); - assert.equal(null, result); - step(); - }); - - assert.equal(true, handled); - step(); + try { + await auth._tryGetApplicationCredentialsFromWellKnownFile(); + } catch (e) { + assert(e); + assert.equal('hello', e.message); + return; + } + assert.fail('failed to throw'); }); - it('should pass along a failure on non-Windows', (done) => { + it('should pass along a failure on non-Windows', async () => { // Set up mocks. const auth = new GoogleAuth(); blockGoogleApplicationCredentialEnvironmentVariable(auth); @@ -984,33 +868,24 @@ describe('GoogleAuth', () => { auth._osPlatform = () => 'linux'; auth._fileExists = () => true; - auth._getApplicationCredentialsFromFilePath = (filePath, callback) => { - if (callback) callback(new Error('hello')); + auth._getApplicationCredentialsFromFilePath = (filePath: string) => { + throw new Error('hello'); }; - // The test ends successfully after 2 steps have completed. - const step = doneWhen(done, 2); - // Execute. - const handled = - auth._tryGetApplicationCredentialsFromWellKnownFile((err, result) => { - assert.equal('hello', err ? err.message : ''); - assert.equal(null, result); - step(); - }); - - assert.equal(true, handled); - step(); + try { + await auth._tryGetApplicationCredentialsFromWellKnownFile(); + } catch (e) { + assert.equal('hello', e.message); + return; + } + assert.fail('failed to throw'); }); describe('.getDefaultProjectId', () => { - it('should return a new projectId the first time and a cached projectId the second time', - (done) => { - + async () => { const fixedProjectId = 'my-awesome-project'; - // The test ends successfully after 3 steps have completed. - const step = doneWhen(done, 3); // Create a function which will set up a GoogleAuth instance to match // on an environment variable json file, but not on anything else. @@ -1019,8 +894,7 @@ describe('GoogleAuth', () => { creds, 'GCLOUD_PROJECT', fixedProjectId); creds._fileExists = () => false; - creds._checkIsGCE = - (callback: (err: Error|null, isGCE?: boolean) => void) => false; + creds._checkIsGCE = async () => Promise.resolve(false); }; // Set up a new GoogleAuth and prepare it for local environment @@ -1029,578 +903,515 @@ describe('GoogleAuth', () => { setUpAuthForEnvironmentVariable(auth); // Ask for credentials, the first time. - auth.getDefaultProjectId((err, projectId) => { - assert.equal(null, err); - assert.equal(projectId, fixedProjectId); + const projectId = await auth.getDefaultProjectId(); + assert.equal(projectId, fixedProjectId); - // Manually change the value of the cached projectId - auth.cachedProjectId = 'monkey'; + // Manually change the value of the cached projectId + auth.cachedProjectId = 'monkey'; - // Step 1 has completed. - step(); + // Ask for projectId again, from the same auth instance. We expect a + // cached instance this time. + const projectId2 = await auth.getDefaultProjectId(); - // Ask for projectId again, from the same auth instance. We expect a - // cached instance this time. - auth.getDefaultProjectId((err2, projectId2) => { - assert.equal(null, err2); + // Make sure we get the changed cached projectId back + assert.equal('monkey', projectId2); - // Make sure we get the changed cached projectId back - assert.equal('monkey', projectId2); + // Now create a second GoogleAuth instance, and ask for projectId. + // We should get a new projectId instance this time. + const auth2 = new GoogleAuth(); + setUpAuthForEnvironmentVariable(auth2); - // Now create a second GoogleAuth instance, and ask for projectId. - // We should get a new projectId instance this time. - const auth2 = new GoogleAuth(); - setUpAuthForEnvironmentVariable(auth2); + const projectId3 = await auth2.getDefaultProjectId(); + assert.equal(projectId3, fixedProjectId); - // Step 2 has completed. - step(); - - auth2.getDefaultProjectId((err3, projectId3) => { - assert.equal(null, err3); - assert.equal(projectId3, fixedProjectId); - - // Make sure we get a new (non-cached) projectId instance back. - // Test verifies invalid parameter tests, which requires cast to - // any. - // tslint:disable-next-line no-any - assert.equal((projectId3 as any).specialTestBit, undefined); - - // Step 3 has completed. - step(); - }); - }); - }); + // Make sure we get a new (non-cached) projectId instance back. + // Test verifies invalid parameter tests, which requires cast to + // any. + // tslint:disable-next-line no-any + assert.equal((projectId3 as any).specialTestBit, undefined); }); + }); - it('should use GCLOUD_PROJECT environment variable when it is set', - (done) => { - const fixedProjectId = 'my-awesome-project'; + it('should use GCLOUD_PROJECT environment variable when it is set', + (done) => { + const fixedProjectId = 'my-awesome-project'; - const auth = new GoogleAuth(); - insertEnvironmentVariableIntoAuth( - auth, 'GCLOUD_PROJECT', fixedProjectId); + const auth = new GoogleAuth(); + insertEnvironmentVariableIntoAuth( + auth, 'GCLOUD_PROJECT', fixedProjectId); - // Execute. - auth.getDefaultProjectId((err, projectId) => { - assert.equal(err, null); - assert.equal(projectId, fixedProjectId); - done(); - }); + // Execute. + auth.getDefaultProjectId((err, projectId) => { + assert.equal(err, null); + assert.equal(projectId, fixedProjectId); + done(); }); + }); - it('should use GOOGLE_CLOUD_PROJECT environment variable when it is set', - (done) => { - const fixedProjectId = 'my-awesome-project'; + it('should use GOOGLE_CLOUD_PROJECT environment variable when it is set', + (done) => { + const fixedProjectId = 'my-awesome-project'; - const auth = new GoogleAuth(); - insertEnvironmentVariableIntoAuth( - auth, 'GOOGLE_CLOUD_PROJECT', fixedProjectId); + const auth = new GoogleAuth(); + insertEnvironmentVariableIntoAuth( + auth, 'GOOGLE_CLOUD_PROJECT', fixedProjectId); - // Execute. - auth.getDefaultProjectId((err, projectId) => { - assert.equal(err, null); - assert.equal(projectId, fixedProjectId); - done(); - }); + // Execute. + auth.getDefaultProjectId((err, projectId) => { + assert.equal(err, null); + assert.equal(projectId, fixedProjectId); + done(); }); - - it('should use GOOGLE_APPLICATION_CREDENTIALS file when it is available', - (done) => { - const fixedProjectId = 'my-awesome-project'; - - const auth = new GoogleAuth(); - insertEnvironmentVariableIntoAuth( - auth, 'GOOGLE_APPLICATION_CREDENTIALS', - path.join(__dirname, '../../test/fixtures/private2.json')); - - // Execute. - auth.getDefaultProjectId((err, projectId) => { - assert.ifError(err); - assert.equal(projectId, fixedProjectId); - done(); - }); + }); + + it('should use GOOGLE_APPLICATION_CREDENTIALS file when it is available', + (done) => { + const fixedProjectId = 'my-awesome-project'; + + const auth = new GoogleAuth(); + insertEnvironmentVariableIntoAuth( + auth, 'GOOGLE_APPLICATION_CREDENTIALS', + path.join(__dirname, '../../test/fixtures/private2.json')); + + // Execute. + auth.getDefaultProjectId((err, projectId) => { + assert.ifError(err); + assert.equal(projectId, fixedProjectId); + done(); }); - - it('should use well-known file when it is available and env vars are not set', - (done) => { - const fixedProjectId = 'my-awesome-project'; - - // Set up the creds. - // * Environment variable is not set. - // * Well-known file is set up to point to private2.json - // * Running on GCE is set to true. - const auth = new GoogleAuth(); - blockGoogleApplicationCredentialEnvironmentVariable(auth); - auth._getSDKDefaultProjectId = (callback) => { - callback( - null, JSON.stringify({core: {project: fixedProjectId}}), null); - }; - - // Execute. - auth.getDefaultProjectId((err, projectId) => { - assert.equal(err, null); - assert.equal(projectId, fixedProjectId); - done(); + }); + + it('should use well-known file when it is available and env vars are not set', + (done) => { + const fixedProjectId = 'my-awesome-project'; + + // Set up the creds. + // * Environment variable is not set. + // * Well-known file is set up to point to private2.json + // * Running on GCE is set to true. + const auth = new GoogleAuth(); + blockGoogleApplicationCredentialEnvironmentVariable(auth); + auth._getSDKDefaultProjectId = () => { + return Promise.resolve({ + stdout: JSON.stringify({core: {project: fixedProjectId}}), + stderr: null }); - }); + }; - it('should use GCE when well-known file and env const are not set', - (done) => { - const fixedProjectId = 'my-awesome-project'; - const auth = new GoogleAuth(); - blockGoogleApplicationCredentialEnvironmentVariable(auth); - auth._getSDKDefaultProjectId = (callback) => { - callback(null, '', null); - }; - // TODO: Provide a proper mock. - // tslint:disable-next-line no-any - (auth as any).transporter = { - request: (opts: {}, callback?: BodyResponseCallback) => { - return callback ? callback( - null, fixedProjectId, - {body: fixedProjectId, statusCode: 200} as - request.RequestResponse) : - null; - }, - }; - - // Execute. - auth.getDefaultProjectId((err, projectId) => { - assert.equal(err, null); - assert.equal(projectId, fixedProjectId); - done(); - }); + // Execute. + auth.getDefaultProjectId((err, projectId) => { + assert.equal(err, null); + assert.equal(projectId, fixedProjectId); + done(); }); - }); - - describe('.getApplicationDefault', () => { - - it('should return a new credential the first time and a cached credential the second time', - (done) => { - - // The test ends successfully after 3 steps have completed. - const step = doneWhen(done, 3); - - // Create a function which will set up a GoogleAuth instance to match - // on an environment variable json file, but not on anything else. - const setUpAuthForEnvironmentVariable = (creds: GoogleAuth) => { - insertEnvironmentVariableIntoAuth( - creds, 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/private.json'); - - creds._fileExists = () => false; - creds._checkIsGCE = - (callback: (err: Error|null, isGCE?: boolean) => void) => false; - }; - - // Set up a new GoogleAuth and prepare it for local environment - // variable handling. - const auth = new GoogleAuth(); - setUpAuthForEnvironmentVariable(auth); - - // Ask for credentials, the first time. - auth.getApplicationDefault((err, result) => { - assert.equal(null, err); - assert.notEqual(null, result); - - // Capture the returned credential. - const cachedCredential = result; - - // Make sure our special test bit is not set yet, indicating that - // this is a new credentials instance. - // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any - assert.equal(null, (cachedCredential as any).specialTestBit); - - // Now set the special test bit. - // Test verifies invalid parameter tests, which requires cast to any. - // tslint:disable-next-line no-any - (cachedCredential as any).specialTestBit = 'monkey'; - - // Step 1 has completed. - step(); - - // Ask for credentials again, from the same auth instance. We expect - // a cached instance this time. - auth.getApplicationDefault((err2, result2) => { - assert.equal(null, err2); - assert.notEqual(null, result2); - - // Make sure the special test bit is set on the credentials we got - // back, indicating that we got cached credentials. Also make sure - // the object instance is the same. - // Test verifies invalid parameter tests, which requires cast to - // any. - // tslint:disable-next-line no-any - assert.equal('monkey', (result2 as any).specialTestBit); - assert.equal(cachedCredential, result2); - - // Now create a second GoogleAuth instance, and ask for - // credentials. We should get a new credentials instance this time. - const auth2 = new GoogleAuth(); - setUpAuthForEnvironmentVariable(auth2); - - // Step 2 has completed. - step(); - - auth2.getApplicationDefault((err3, result3) => { - assert.equal(null, err3); - assert.notEqual(null, result3); - - // Make sure we get a new (non-cached) credential instance back. - // Test verifies invalid parameter tests, which requires cast to - // any. - // tslint:disable-next-line no-any - assert.equal(null, (result3 as any).specialTestBit); - assert.notEqual(cachedCredential, result3); - - // Step 3 has completed. - step(); - }); - }); - }); + }); + + it('should use GCE when well-known file and env const are not set', + (done) => { + const fixedProjectId = 'my-awesome-project'; + const auth = new GoogleAuth(); + blockGoogleApplicationCredentialEnvironmentVariable(auth); + auth._getSDKDefaultProjectId = () => { + return Promise.resolve({stdout: '', stderr: null}); + }; + createGetProjectIdNock(fixedProjectId); + + // Execute. + auth.getDefaultProjectId((err, projectId) => { + assert.equal(err, null); + assert.equal(projectId, fixedProjectId); + done(); }); + }); +}); - it('should use environment variable when it is set', (done) => { - // We expect private.json to be the file that is used. - const fileContents = - fs.readFileSync('./test/fixtures/private.json', 'utf-8'); - const json = JSON.parse(fileContents); - - // Set up the creds. - // * Environment variable is set up to point to private.json - // * Well-known file is set up to point to private2.json - // * Running on GCE is set to true. - const auth = new GoogleAuth(); - insertEnvironmentVariableIntoAuth( - auth, 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/private.json'); - insertEnvironmentVariableIntoAuth(auth, 'APPDATA', 'foo'); - auth._pathJoin = pathJoin; - auth._osPlatform = () => 'win32'; - auth._fileExists = () => true; +describe('.getApplicationDefault', () => { + it('should return a new credential the first time and a cached credential the second time', + async () => { + // Create a function which will set up a GoogleAuth instance to match + // on an environment variable json file, but not on anything else. + const setUpAuthForEnvironmentVariable = (creds: GoogleAuth) => { + insertEnvironmentVariableIntoAuth( + creds, 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/private.json'); + + creds._fileExists = () => false; + creds._checkIsGCE = () => Promise.resolve(false); + }; + + // Set up a new GoogleAuth and prepare it for local environment + // variable handling. + const auth = new GoogleAuth(); + setUpAuthForEnvironmentVariable(auth); + + // Ask for credentials, the first time. + const result = await auth.getApplicationDefault(); + assert.notEqual(null, result); + + // Capture the returned credential. + const cachedCredential = result.credential; + + // Make sure our special test bit is not set yet, indicating that + // this is a new credentials instance. + // Test verifies invalid parameter tests, which requires cast to any. + // tslint:disable-next-line no-any + assert.equal(null, (cachedCredential as any).specialTestBit); + + // Now set the special test bit. + // Test verifies invalid parameter tests, which requires cast to any. + // tslint:disable-next-line no-any + (cachedCredential as any).specialTestBit = 'monkey'; + + // Ask for credentials again, from the same auth instance. We expect + // a cached instance this time. + const result2 = (await auth.getApplicationDefault()).credential; + assert.notEqual(null, result2); + + // Make sure the special test bit is set on the credentials we got + // back, indicating that we got cached credentials. Also make sure + // the object instance is the same. + // Test verifies invalid parameter tests, which requires cast to + // any. + // tslint:disable-next-line no-any + assert.equal('monkey', (result2 as any).specialTestBit); + assert.equal(cachedCredential, result2); + + // Now create a second GoogleAuth instance, and ask for + // credentials. We should get a new credentials instance this time. + const auth2 = new GoogleAuth(); + setUpAuthForEnvironmentVariable(auth2); + + const result3 = (await auth2.getApplicationDefault()).credential; + assert.notEqual(null, result3); + + // Make sure we get a new (non-cached) credential instance back. + // Test verifies invalid parameter tests, which requires cast to + // any. + // tslint:disable-next-line no-any + assert.equal(null, (result3 as any).specialTestBit); + assert.notEqual(cachedCredential, result3); + }); + + it('should use environment variable when it is set', (done) => { + // We expect private.json to be the file that is used. + const fileContents = + fs.readFileSync('./test/fixtures/private.json', 'utf-8'); + const json = JSON.parse(fileContents); + + // Set up the creds. + // * Environment variable is set up to point to private.json + // * Well-known file is set up to point to private2.json + // * Running on GCE is set to true. + const auth = new GoogleAuth(); + insertEnvironmentVariableIntoAuth( + auth, 'GOOGLE_APPLICATION_CREDENTIALS', './test/fixtures/private.json'); + insertEnvironmentVariableIntoAuth(auth, 'APPDATA', 'foo'); + auth._pathJoin = pathJoin; + auth._osPlatform = () => 'win32'; + auth._fileExists = () => true; - auth._checkIsGCE = callsBack(null, true); - insertWellKnownFilePathIntoAuth( - auth, 'foo:gcloud:application_default_credentials.json', - './test/fixtures/private2.json'); + auth._checkIsGCE = () => Promise.resolve(true); + insertWellKnownFilePathIntoAuth( + auth, 'foo:gcloud:application_default_credentials.json', + './test/fixtures/private2.json'); - // Execute. - auth.getApplicationDefault((err, result) => { - const client = result as JWT; - assert.equal(null, err); - assert.equal(json.private_key, client.key); - assert.equal(json.client_email, client.email); - assert.equal(null, client.keyFile); - assert.equal(null, client.subject); - assert.equal(null, client.scope); - done(); - }); + // Execute. + auth.getApplicationDefault((err, result) => { + const client = result as JWT; + assert.equal(null, err); + assert.equal(json.private_key, client.key); + assert.equal(json.client_email, client.email); + assert.equal(null, client.keyFile); + assert.equal(null, client.subject); + assert.equal(null, client.scope); + done(); }); + }); - it('should use well-known file when it is available and env const is not set', - (done) => { - // We expect private2.json to be the file that is used. - const fileContents = - fs.readFileSync('./test/fixtures/private2.json', 'utf-8'); - const json = JSON.parse(fileContents); - - // Set up the creds. - // * Environment variable is not set. - // * Well-known file is set up to point to private2.json - // * Running on GCE is set to true. - const auth = new GoogleAuth(); - blockGoogleApplicationCredentialEnvironmentVariable(auth); - insertEnvironmentVariableIntoAuth(auth, 'APPDATA', 'foo'); - auth._pathJoin = pathJoin; - auth._osPlatform = () => 'win32'; - auth._fileExists = () => true; - auth._checkIsGCE = - (callback: (err: Error|null, isGCE?: boolean) => void) => true; - insertWellKnownFilePathIntoAuth( - auth, 'foo:gcloud:application_default_credentials.json', - './test/fixtures/private2.json'); - - // Execute. - auth.getApplicationDefault((err, result) => { - assert.equal(null, err); - const client = result as JWT; - assert.equal(json.private_key, client.key); - assert.equal(json.client_email, client.email); - assert.equal(null, client.keyFile); - assert.equal(null, client.subject); - assert.equal(null, client.scope); - done(); - }); + it('should use well-known file when it is available and env const is not set', + (done) => { + // We expect private2.json to be the file that is used. + const fileContents = + fs.readFileSync('./test/fixtures/private2.json', 'utf-8'); + const json = JSON.parse(fileContents); + + // Set up the creds. + // * Environment variable is not set. + // * Well-known file is set up to point to private2.json + // * Running on GCE is set to true. + const auth = new GoogleAuth(); + blockGoogleApplicationCredentialEnvironmentVariable(auth); + insertEnvironmentVariableIntoAuth(auth, 'APPDATA', 'foo'); + auth._pathJoin = pathJoin; + auth._osPlatform = () => 'win32'; + auth._fileExists = () => true; + auth._checkIsGCE = () => Promise.resolve(true); + insertWellKnownFilePathIntoAuth( + auth, 'foo:gcloud:application_default_credentials.json', + './test/fixtures/private2.json'); + + // Execute. + auth.getApplicationDefault((err, result) => { + assert.equal(null, err); + const client = result as JWT; + assert.equal(json.private_key, client.key); + assert.equal(json.client_email, client.email); + assert.equal(null, client.keyFile); + assert.equal(null, client.subject); + assert.equal(null, client.scope); + done(); }); - - it('should use GCE when well-known file and env const are not set', - (done) => { - // Set up the creds. - // * Environment variable is not set. - // * Well-known file is not set. - // * Running on GCE is set to true. - const auth = new GoogleAuth(); - blockGoogleApplicationCredentialEnvironmentVariable(auth); - insertEnvironmentVariableIntoAuth(auth, 'APPDATA', 'foo'); - auth._pathJoin = pathJoin; - auth._osPlatform = () => 'win32'; - auth._fileExists = () => false; - auth._checkIsGCE = callsBack(null, true); - - // Execute. - auth.getApplicationDefault((err, result) => { - assert.equal(null, err); - - // This indicates that we got a ComputeClient instance back, rather - // than a JWTClient. - assert.equal( - 'compute-placeholder', result.credentials.refresh_token); - done(); - }); + }); + + it('should use GCE when well-known file and env const are not set', + (done) => { + // Set up the creds. + // * Environment variable is not set. + // * Well-known file is not set. + // * Running on GCE is set to true. + const auth = new GoogleAuth(); + blockGoogleApplicationCredentialEnvironmentVariable(auth); + insertEnvironmentVariableIntoAuth(auth, 'APPDATA', 'foo'); + auth._pathJoin = pathJoin; + auth._osPlatform = () => 'win32'; + auth._fileExists = () => false; + auth._checkIsGCE = () => Promise.resolve(true); + + // Execute. + auth.getApplicationDefault((err, result) => { + assert.equal(null, err); + // This indicates that we got a ComputeClient instance back, rather + // than a JWTClient. + assert.equal('compute-placeholder', result!.credentials.refresh_token); + done(); }); + }); - it('should report GCE error when checking for GCE fails', (done) => { - // Set up the creds. - // * Environment variable is not set. - // * Well-known file is not set. - // * Running on GCE is set to true. - const auth = new GoogleAuth(); - blockGoogleApplicationCredentialEnvironmentVariable(auth); - insertEnvironmentVariableIntoAuth(auth, 'APPDATA', 'foo'); - auth._pathJoin = pathJoin; - auth._osPlatform = () => 'win32'; - auth._fileExists = () => false; - auth._checkIsGCE = callsBack(new Error('fake error'), undefined); + it('should report GCE error when checking for GCE fails', (done) => { + // Set up the creds. + // * Environment variable is not set. + // * Well-known file is not set. + // * Running on GCE is set to true. + const auth = new GoogleAuth(); + blockGoogleApplicationCredentialEnvironmentVariable(auth); + insertEnvironmentVariableIntoAuth(auth, 'APPDATA', 'foo'); + auth._pathJoin = pathJoin; + auth._osPlatform = () => 'win32'; + auth._fileExists = () => false; + auth._checkIsGCE = () => { + throw new Error('fake error'); + }; - // Execute. - auth.getApplicationDefault((err, result) => { - assert(err instanceof Error); - assert.equal(result, undefined); - done(); - }); + // Execute. + auth.getApplicationDefault((err, result) => { + assert(err instanceof Error); + assert.equal(result, undefined); + done(); }); + }); - it('should also get project ID', (done) => { - // We expect private.json to be the file that is used. - const fileContents = - fs.readFileSync('./test/fixtures/private.json', 'utf-8'); - const json = JSON.parse(fileContents); - const testProjectId = 'my-awesome-project'; - - // Set up the creds. - // * Environment variable is set up to point to private.json - // * Well-known file is set up to point to private2.json - // * Running on GCE is set to true. - const auth = new GoogleAuth(); - insertEnvironmentVariableIntoAuth( - auth, 'GOOGLE_APPLICATION_CREDENTIALS', - './test/fixtures/private.json'); - insertEnvironmentVariableIntoAuth(auth, 'GCLOUD_PROJECT', testProjectId); - insertEnvironmentVariableIntoAuth(auth, 'APPDATA', 'foo'); - auth._pathJoin = pathJoin; - auth._osPlatform = () => 'win32'; - auth._fileExists = () => true; - auth._checkIsGCE = - (callback: (err: Error|null, isGCE?: boolean) => void) => true; - insertWellKnownFilePathIntoAuth( - auth, 'foo:gcloud:application_default_credentials.json', - './test/fixtures/private2.json'); + it('should also get project ID', (done) => { + // We expect private.json to be the file that is used. + const fileContents = + fs.readFileSync('./test/fixtures/private.json', 'utf-8'); + const json = JSON.parse(fileContents); + const testProjectId = 'my-awesome-project'; + + // Set up the creds. + // * Environment variable is set up to point to private.json + // * Well-known file is set up to point to private2.json + // * Running on GCE is set to true. + const auth = new GoogleAuth(); + insertEnvironmentVariableIntoAuth( + auth, 'GOOGLE_APPLICATION_CREDENTIALS', './test/fixtures/private.json'); + insertEnvironmentVariableIntoAuth(auth, 'GCLOUD_PROJECT', testProjectId); + insertEnvironmentVariableIntoAuth(auth, 'APPDATA', 'foo'); + auth._pathJoin = pathJoin; + auth._osPlatform = () => 'win32'; + auth._fileExists = () => true; + auth._checkIsGCE = () => Promise.resolve(true); + insertWellKnownFilePathIntoAuth( + auth, 'foo:gcloud:application_default_credentials.json', + './test/fixtures/private2.json'); - // Execute. - auth.getApplicationDefault((err, result, projectId) => { - assert.equal(null, err); - const client = result as JWT; - assert.equal(json.private_key, client.key); - assert.equal(json.client_email, client.email); - assert.equal(projectId, testProjectId); - assert.equal(null, client.keyFile); - assert.equal(null, client.subject); - assert.equal(null, client.scope); - done(); - }); + // Execute. + auth.getApplicationDefault((err, result, projectId) => { + assert.equal(null, err); + const client = result as JWT; + assert.equal(json.private_key, client.key); + assert.equal(json.client_email, client.email); + assert.equal(projectId, testProjectId); + assert.equal(null, client.keyFile); + assert.equal(null, client.subject); + assert.equal(null, client.scope); + done(); }); }); +}); - describe('._checkIsGCE', () => { - - it('should set the _isGCE flag when running on GCE', (done) => { - const auth = new GoogleAuth(); - - // Mock the transport layer to return the correct header indicating that - // we're running on GCE. - auth.transporter = new MockTransporter(true); +describe('._checkIsGCE', () => { + it('should set the _isGCE flag when running on GCE', async () => { + const auth = new GoogleAuth(); - // Assert on the initial values. - assert.notEqual(true, auth.isGCE); + // Mock the transport layer to return the correct header indicating that + // we're running on GCE. + auth.transporter = new MockTransporter(true); - // Execute. - auth._checkIsGCE(() => { - // Assert that the flags are set. - assert.equal(true, auth.isGCE); - done(); - }); - }); + // Assert on the initial values. + assert.notEqual(true, auth.isGCE); - it('should not set the _isGCE flag when not running on GCE', (done) => { - const auth = new GoogleAuth(); + // Execute. + const isGCE = await auth._checkIsGCE(); + assert.equal(true, auth.isGCE); + }); - // Mock the transport layer to indicate that we're not running on GCE. - auth.transporter = new MockTransporter(false); + it('should not set the _isGCE flag when not running on GCE', async () => { + const auth = new GoogleAuth(); - // Assert on the initial values. - assert.notEqual(true, auth.isGCE); + // Mock the transport layer to indicate that we're not running on GCE. + auth.transporter = new MockTransporter(false); - // Execute. - auth._checkIsGCE(() => { - // Assert that the flags are set. - assert.equal(false, auth.isGCE); - done(); - }); - }); + // Assert on the initial values. + assert.notEqual(true, auth.isGCE); - it('Does not execute the second time when running on GCE', (done) => { - const auth = new GoogleAuth(); + // Execute. + const isGCE = await auth._checkIsGCE(); + // Assert that the flags are set. + assert.equal(false, auth.isGCE); + }); - // Mock the transport layer to indicate that we're not running on GCE. - auth.transporter = new MockTransporter(true); + it('Does not execute the second time when running on GCE', async () => { + const auth = new GoogleAuth(); - // Assert on the initial values. - assert.notEqual(true, auth.isGCE); - assert.equal(0, (auth.transporter as MockTransporter).executionCount); + // Mock the transport layer to indicate that we're not running on GCE. + auth.transporter = new MockTransporter(true); - // Execute. - auth._checkIsGCE(() => { - // Assert. - assert.equal(true, auth.isGCE); - assert.equal(1, (auth.transporter as MockTransporter).executionCount); - - // Execute a second time, check that we still get the correct values - // back, but the execution count has not rev'd again, indicating that we - // got the cached values this time. - auth._checkIsGCE(() => { - assert.equal(true, auth.isGCE); - assert.equal(1, (auth.transporter as MockTransporter).executionCount); - }); + // Assert on the initial values. + assert.notEqual(true, auth.isGCE); + assert.equal(0, (auth.transporter as MockTransporter).executionCount); - done(); - }); - }); + // Execute. + await auth._checkIsGCE(); + // Assert. + assert.equal(true, auth.isGCE); + assert.equal(1, (auth.transporter as MockTransporter).executionCount); + + // Execute a second time, check that we still get the correct values + // back, but the execution count has not rev'd again, indicating that we + // got the cached values this time. + const isGCE2 = await auth._checkIsGCE(); + assert.equal(true, auth.isGCE); + assert.equal(1, (auth.transporter as MockTransporter).executionCount); + }); - it('Does not execute the second time when not running on GCE', (done) => { - const auth = new GoogleAuth(); + it('Does not execute the second time when not running on GCE', async () => { + const auth = new GoogleAuth(); - // Mock the transport layer to indicate that we're not running on GCE. - auth.transporter = new MockTransporter(false); + // Mock the transport layer to indicate that we're not running on GCE. + auth.transporter = new MockTransporter(false); - // Assert on the initial values. - assert.notEqual(true, auth.isGCE); - assert.equal(0, (auth.transporter as MockTransporter).executionCount); + // Assert on the initial values. + assert.notEqual(true, auth.isGCE); + assert.equal(0, (auth.transporter as MockTransporter).executionCount); - // Execute. - auth._checkIsGCE(() => { - // Assert. - assert.equal(false, auth.isGCE); - assert.equal(1, (auth.transporter as MockTransporter).executionCount); - - // Execute a second time, check that we still get the correct values - // back, but the execution count has not rev'd again, indicating that we - // got the cached values this time. - auth._checkIsGCE(() => { - assert.equal(false, auth.isGCE); - assert.equal(1, (auth.transporter as MockTransporter).executionCount); - }); + // Execute. + await auth._checkIsGCE(); + // Assert. + assert.equal(false, auth.isGCE); + assert.equal(1, (auth.transporter as MockTransporter).executionCount); - done(); - }); + // Execute a second time, check that we still get the correct values + // back, but the execution count has not rev'd again, indicating that we + // got the cached values this time. + await auth._checkIsGCE(); + assert.equal(false, auth.isGCE); + assert.equal(1, (auth.transporter as MockTransporter).executionCount); - it('Returns false on transport error', (done2) => { - const auth2 = new GoogleAuth(); - // Mock the transport layer to indicate that we're not running on GCE, - // but also to throw an error. - auth2.transporter = new MockTransporter(true, true); + it('Returns false on transport error', async () => { + const auth2 = new GoogleAuth(); - // Assert on the initial values. - assert.notEqual(true, auth2.isGCE); + // Mock the transport layer to indicate that we're not running on GCE, + // but also to throw an error. + auth2.transporter = new MockTransporter(true, true); - // Execute. - auth2._checkIsGCE(() => { - // Assert that _isGCE is set to false due to the error. - assert.equal(false, auth2.isGCE); + // Assert on the initial values. + assert.notEqual(true, auth2.isGCE); - done2(); - }); - }); + // Execute. + await auth2._checkIsGCE(); + // Assert that _isGCE is set to false due to the error. + assert.equal(false, auth2.isGCE); }); }); + describe('.getCredentials', () => { - it('should get metadata from the server when running on GCE', (done) => { + it('should get metadata from the server when running on GCE', async () => { const auth = new GoogleAuth(); - auth.transporter = new MockTransporter(true); - auth._checkIsGCE(() => { - // Assert that the flags are set. - assert.equal(true, auth.isGCE); - done(); - }); - const response = `{ - "email":"test-creds@test-creds.iam.gserviceaccount.com", - "private_key": null - }`; - const scope = - nock('http://metadata.google.internal') - .get( - '/computeMetadata/v1/instance/service-accounts/?recursive=true') - .reply(200, response); - auth.getCredentials((err, body) => { - assert(body); - assert.equal( - body!.client_email, - 'test-creds@test-creds.iam.gserviceaccount.com'); - assert.equal(body!.private_key, null); - scope.done(); - done(); - }); + + createIsGCENock(); + const isGCE = await auth._checkIsGCE(); + + // Assert that the flags are set. + assert.equal(true, auth.isGCE); + + const response = { + default: { + email: 'test-creds@test-creds.iam.gserviceaccount.com', + private_key: null + } + }; + + nock('http://metadata.google.internal') + .get('/computeMetadata/v1/instance/service-accounts/?recursive=true') + .reply(200, response); + + const body = await auth.getCredentials(); + assert(body); + assert.equal( + body!.client_email, 'test-creds@test-creds.iam.gserviceaccount.com'); + assert.equal(body!.private_key, null); }); - it('should error if metadata server is not reachable', (done) => { + + it('should error if metadata server is not reachable', async () => { const auth = new GoogleAuth(); - auth.transporter = new MockTransporter(true); - auth._checkIsGCE(() => { - // Assert that the flags are set. - assert.equal(true, auth.isGCE); - done(); - }); - const scope = - nock('http://metadata.google.internal') - .get( - '/computeMetadata/v1/instance/service-accounts/?recursive=true') - .reply(404); - auth.getCredentials((err, body) => { - assert.equal(true, err instanceof Error); - scope.done(); - done(); - }); + createIsGCENock(); + await auth._checkIsGCE(); + // Assert that the flags are set. + assert.equal(true, auth.isGCE); + + nock('http://metadata.google.internal') + .get('/computeMetadata/v1/instance/service-accounts/?recursive=true') + .reply(404); + + try { + await auth.getCredentials(); + } catch (e) { + return; + } + throw new Error('Expected to throw'); }); - it('should error if body is empty', (done) => { + + it('should error if body is empty', async () => { const auth = new GoogleAuth(); - auth.transporter = new MockTransporter(true); - auth._checkIsGCE(() => { - // Assert that the flags are set. - assert.equal(true, auth.isGCE); - done(); - }); - const scope = - nock('http://metadata.google.internal') - .get( - '/computeMetadata/v1/instance/service-accounts/?recursive=true') - .reply(200, {}); - auth.getCredentials((err, body) => { - assert.equal(true, err instanceof Error); - scope.done(); - done(); - }); + createIsGCENock(); + await auth._checkIsGCE(); + // Assert that the flags are set. + assert.equal(true, auth.isGCE); + + nock('http://metadata.google.internal') + .get('/computeMetadata/v1/instance/service-accounts/?recursive=true') + .reply(200, {}); + + try { + await auth.getCredentials(); + } catch (e) { + return; + } + throw new Error('Expected to throw'); }); - it('should handle valid environment variable', (done) => { + + it('should handle valid environment variable', async () => { // Set up a mock to return path to a valid credentials file. const auth = new GoogleAuth(); blockGoogleApplicationCredentialEnvironmentVariable(auth); @@ -1608,23 +1419,20 @@ describe('GoogleAuth', () => { auth, 'GOOGLE_APPLICATION_CREDENTIALS', './test/fixtures/private.json'); // Execute. - auth._tryGetApplicationCredentialsFromEnvironmentVariable( - (err, result) => { - assert(result); - assert.equal(null, err); - const jwt = result as JWT; - it('should return the credentials from file', (done2) => { - auth.getCredentials((_err, body) => { - assert.notEqual(null, body); - assert.equal(jwt.email, body!.client_email); - assert.equal(jwt.key, body!.private_key); - done2(); - }); - }); - done(); - }); + const result = + await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); + assert(result); + + const jwt = result as JWT; + it('should return the credentials from file', async () => { + const body = await auth.getCredentials(); + assert.notEqual(null, body); + assert.equal(jwt.email, body!.client_email); + assert.equal(jwt.key, body!.private_key); + }); }); - it('should handle valid file path', (done) => { + + it('should handle valid file path', async () => { // Set up a mock to return path to a valid credentials file. const auth = new GoogleAuth(); blockGoogleApplicationCredentialEnvironmentVariable(auth); @@ -1632,39 +1440,35 @@ describe('GoogleAuth', () => { auth._pathJoin = pathJoin; auth._osPlatform = () => 'win32'; auth._fileExists = () => true; - auth._checkIsGCE = callsBack(null, true); + auth._checkIsGCE = () => Promise.resolve(true); insertWellKnownFilePathIntoAuth( auth, 'foo:gcloud:application_default_credentials.json', './test/fixtures/private2.json'); // Execute. - auth.getApplicationDefault((err, result) => { - assert.notEqual(true, err instanceof Error); - assert(result); - assert.equal(null, err); - const jwt = result as JWT; - it('should return the credentials from file', (done2) => { - auth.getCredentials((_err, body) => { - assert.notEqual(null, body); - assert.equal(jwt.email, body!.client_email); - assert.equal(jwt.key, body!.private_key); - done2(); - }); - }); - done(); + const result = await auth.getApplicationDefault(); + assert(result); + const jwt = result.credential as JWT; + it('should return the credentials from file', async () => { + const body = await auth.getCredentials(); + assert.notEqual(null, body); + assert.equal(jwt.email, body!.client_email); + assert.equal(jwt.key, body!.private_key); }); }); - it('should return error when env const is not set', (done) => { + + it('should return error when env const is not set', async () => { // Set up a mock to return a null path string const auth = new GoogleAuth(); - let credentialFlag: boolean; insertEnvironmentVariableIntoAuth(auth, 'GOOGLE_APPLICATION_CREDENTIALS'); - credentialFlag = - auth._tryGetApplicationCredentialsFromEnvironmentVariable(); - assert.equal(false, credentialFlag); - auth.getCredentials((_err, body) => { - assert.equal(true, _err instanceof Error); - done(); - }); + const client = + await auth._tryGetApplicationCredentialsFromEnvironmentVariable(); + assert.equal(null, client); + try { + await auth.getCredentials(); + } catch (e) { + return; + } + throw new Error('Expected to throw'); }); }); }); diff --git a/test/test.iam.ts b/test/test.iam.ts index b0c323e0..cddf071e 100644 --- a/test/test.iam.ts +++ b/test/test.iam.ts @@ -32,12 +32,10 @@ describe('.getRequestMetadata', () => { (err: Error|null, creds?: RequestMetadata) => { assert.strictEqual(err, null, 'no error was expected: got\n' + err); assert.notStrictEqual(creds, null, 'metadata should be present'); - if (creds) { - assert.strictEqual( - creds['x-goog-iam-authority-selector'], testSelector); - assert.strictEqual( - creds['x-goog-iam-authorization-token'], testToken); - } + assert.strictEqual( + creds!['x-goog-iam-authority-selector'], testSelector); + assert.strictEqual( + creds!['x-goog-iam-authorization-token'], testToken); done(); }; client.getRequestMetadata(null, expectRequestMetadata); diff --git a/test/test.jwt.ts b/test/test.jwt.ts index 75ebb39a..09b17dbd 100644 --- a/test/test.jwt.ts +++ b/test/test.jwt.ts @@ -63,7 +63,6 @@ afterEach(() => { }); describe('Initial credentials', () => { - it('should create a dummy refresh token string', () => { // It is important that the compute client is created with a refresh token // value filled in, or else the rest of the logic will not work. @@ -71,15 +70,11 @@ describe('Initial credentials', () => { const jwt = new auth.JWT(); assert.equal('jwt-placeholder', jwt.credentials.refresh_token); }); - }); describe('JWT auth client', () => { - describe('.authorize', () => { - it('should get an initial access token', done => { - const auth = new GoogleAuth(); const jwt = new auth.JWT( 'foo@serviceaccount.com', PEM_PATH, null, @@ -111,13 +106,10 @@ describe('JWT auth client', () => { done(); }); }); - }); describe('.getAccessToken', () => { - describe('when scopes are set', () => { - it('can get obtain new access token', (done) => { const auth = new GoogleAuth(); const jwt = new auth.JWT( @@ -134,15 +126,11 @@ describe('JWT auth client', () => { done(); }); }); - }); - }); describe('.getRequestMetadata', () => { - describe('when scopes are set', () => { - it('can obtain new access token', (done) => { const auth = new GoogleAuth(); const jwt = new auth.JWT( @@ -153,27 +141,21 @@ describe('JWT auth client', () => { const wantedToken = 'abc123'; const want = 'Bearer ' + wantedToken; - const retValue = 'dummy'; createGTokenMock({access_token: wantedToken}); - const res = jwt.getRequestMetadata(null, (err, result) => { + jwt.getRequestMetadata(null, (err, result) => { assert.strictEqual(null, err, 'no error was expected: got\n' + err); const got = result as { Authorization: string; }; - assert(got); - if (got) { - assert.strictEqual( - want, got.Authorization, - 'the authorization header was wrong: ' + got.Authorization); - } + assert.strictEqual( + want, got.Authorization, + 'the authorization header was wrong: ' + got.Authorization); done(); }); }); - }); describe('when scopes are not set, but a uri is provided', () => { - it('gets a jwt header access token', (done) => { const keys = keypair(1024 /* bitsize of private key */); const email = 'foo@serviceaccount.com'; @@ -185,8 +167,7 @@ describe('JWT auth client', () => { jwt.credentials = {refresh_token: 'jwt-placeholder'}; const testUri = 'http:/example.com/my_test_service'; - const retValue = 'dummy'; - const res = jwt.getRequestMetadata(testUri, (err, result) => { + jwt.getRequestMetadata(testUri, (err, result) => { const got = result as { Authorization: string; }; @@ -198,17 +179,12 @@ describe('JWT auth client', () => { assert.strictEqual(email, payload.sub); assert.strictEqual(testUri, payload.aud); done(); - return retValue; }); - assert.strictEqual(res, retValue); }); - }); - }); describe('.request', () => { - it('should refresh token if missing access token', (done) => { const auth = new GoogleAuth(); const jwt = new auth.JWT( @@ -218,7 +194,7 @@ describe('JWT auth client', () => { jwt.credentials = {refresh_token: 'jwt-placeholder'}; createGTokenMock({access_token: 'abc123'}); - jwt.request({uri: 'http://bar'}, () => { + jwt.request({url: 'http://bar'}, () => { assert.equal('abc123', jwt.credentials.access_token); done(); }); @@ -237,14 +213,14 @@ describe('JWT auth client', () => { }; createGTokenMock({access_token: 'abc123'}); - jwt.request({uri: 'http://bar'}, () => { + jwt.request({url: 'http://bar'}, () => { assert.equal('abc123', jwt.credentials.access_token); done(); }); }); it('should refresh token if the server returns 403', (done) => { - nock('http://example.com').log(console.log).get('/access').reply(403); + nock('http://example.com').get('/access').twice().reply(403); const auth = new GoogleAuth(); const jwt = new auth.JWT( @@ -259,7 +235,7 @@ describe('JWT auth client', () => { createGTokenMock({access_token: 'abc123'}); - jwt.request({uri: 'http://example.com/access'}, () => { + jwt.request({url: 'http://example.com/access'}, () => { assert.equal('abc123', jwt.credentials.access_token); done(); }); @@ -268,7 +244,6 @@ describe('JWT auth client', () => { it('should not refresh if not expired', (done) => { const scope = nock('https://accounts.google.com') - .log(console.log) .post('/o/oauth2/token', '*') .reply(200, {access_token: 'abc123', expires_in: 10000}); @@ -283,7 +258,7 @@ describe('JWT auth client', () => { expiry_date: (new Date()).getTime() + 5000 }; - jwt.request({uri: 'http://bar'}, () => { + jwt.request({url: 'http://bar'}, () => { assert.equal('initial-access-token', jwt.credentials.access_token); assert.equal(false, scope.isDone()); done(); @@ -293,7 +268,6 @@ describe('JWT auth client', () => { it('should assume access token is not expired', (done) => { const scope = nock('https://accounts.google.com') - .log(console.log) .post('/o/oauth2/token', '*') .reply(200, {access_token: 'abc123', expires_in: 10000}); @@ -307,16 +281,15 @@ describe('JWT auth client', () => { refresh_token: 'jwt-placeholder' }; - jwt.request({uri: 'http://bar'}, () => { + jwt.request({url: 'http://bar'}, () => { assert.equal('initial-access-token', jwt.credentials.access_token); assert.equal(false, scope.isDone()); done(); }); }); - }); - it('should return expiry_date in milliseconds', (done) => { + it('should return expiry_date in milliseconds', async () => { const auth = new GoogleAuth(); const jwt = new auth.JWT( 'foo@serviceaccount.com', PEM_PATH, null, ['http://bar', 'http://foo'], @@ -325,19 +298,13 @@ describe('JWT auth client', () => { jwt.credentials = {refresh_token: 'jwt-placeholder'}; createGTokenMock({access_token: 'token', expires_in: 100}); - jwt.refreshToken(null, (err, creds) => { - const dateInMillis = (new Date()).getTime(); - assert(creds); - if (creds && creds.expiry_date) { - const expiryDate = new Date(creds.expiry_date); - assert.equal( - dateInMillis.toString().length, - creds.expiry_date.toString().length); - } - done(); - }); + const result = await jwt.refreshToken(null); + const creds = result.tokens; + const dateInMillis = (new Date()).getTime(); + const expiryDate = new Date(creds.expiry_date!); + assert.equal( + dateInMillis.toString().length, creds.expiry_date!.toString().length); }); - }); describe('.createScoped', () => { @@ -375,13 +342,10 @@ describe('.createScoped', () => { ['http://bar', 'http://foo'], 'bar@subjectaccount.com'); const clone = jwt.createScoped(['gorilla', 'chimpanzee', 'orangutan']); - assert(clone.scopes); - if (clone.scopes) { - assert.equal(3, clone.scopes.length); - assert.equal('gorilla', clone.scopes[0]); - assert.equal('chimpanzee', clone.scopes[1]); - assert.equal('orangutan', clone.scopes[2]); - } + assert.equal(3, clone.scopes!.length); + assert.equal('gorilla', clone.scopes![0]); + assert.equal('chimpanzee', clone.scopes![1]); + assert.equal('orangutan', clone.scopes![2]); }); it('should handle null scope', () => { @@ -421,7 +385,6 @@ describe('.createScoped', () => { const clone = jwt.createScoped('hi'); assert.notEqual(jwt, clone); }); - }); describe('.createScopedRequired', () => { @@ -474,7 +437,6 @@ describe('.createScopedRequired', () => { it('should return false when scopes is not an array or a string, but can be used as a string', () => { - const auth2 = new GoogleAuth(); const jwt = new auth2.JWT( 'foo@serviceaccount.com', '/path/to/key.pem', null, '2', @@ -567,7 +529,6 @@ describe('.fromJson', () => { done(); }); }); - }); describe('.fromStream', () => { @@ -610,7 +571,6 @@ describe('.fromStream', () => { done(); }); }); - }); describe('.fromAPIKey', () => { @@ -643,7 +603,7 @@ describe('.fromAPIKey', () => { it('should set the .apiKey property on the instance', (done) => { jwt.fromAPIKey(KEY, (err) => { assert.strictEqual(jwt.apiKey, KEY); - assert.strictEqual(err, null); + assert.equal(err, null); done(); }); }); diff --git a/test/test.jwtaccess.ts b/test/test.jwtaccess.ts index 1156ef98..c2bab1ce 100644 --- a/test/test.jwtaccess.ts +++ b/test/test.jwtaccess.ts @@ -37,7 +37,6 @@ function createJSON() { } describe('.getRequestMetadata', () => { - it('create a signed JWT token as the access token', (done) => { const keys = keypair(1024 /* bitsize of private key */); const testUri = 'http:/example.com/my_test_service'; @@ -51,21 +50,18 @@ describe('.getRequestMetadata', () => { assert.strictEqual(null, err, 'no error was expected: got\n' + err); assert.notStrictEqual( null, headers, 'an creds object should be present'); - if (headers) { - const decoded = jws.decode( - (headers.Authorization as string).replace('Bearer ', '')); - const payload = JSON.parse(decoded.payload); - assert.strictEqual(email, payload.iss); - assert.strictEqual(email, payload.sub); - assert.strictEqual(testUri, payload.aud); - } + const decoded = jws.decode( + (headers!.Authorization as string).replace('Bearer ', '')); + const payload = JSON.parse(decoded.payload); + assert.strictEqual(email, payload.iss); + assert.strictEqual(email, payload.sub); + assert.strictEqual(testUri, payload.aud); done(); return retValue; }; const res = client.getRequestMetadata(testUri, expectAuth); assert.strictEqual(res, retValue); }); - }); describe('.createScopedRequired', () => { @@ -135,7 +131,6 @@ describe('.fromJson', () => { done(); }); }); - }); describe('.fromStream', () => { @@ -174,5 +169,4 @@ describe('.fromStream', () => { done(); }); }); - }); diff --git a/test/test.loginticket.ts b/test/test.loginticket.ts index 469188f4..1d255a06 100644 --- a/test/test.loginticket.ts +++ b/test/test.loginticket.ts @@ -18,7 +18,6 @@ import * as assert from 'assert'; import {LoginTicket} from '../src/auth/loginticket'; describe('LoginTicket', () => { - it('should return null userId even if no payload', () => { const ticket = new LoginTicket(); assert.equal(ticket.getUserId(), null); diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index 047fbb4f..b5c17d49 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -15,12 +15,12 @@ */ import * as assert from 'assert'; +import {AxiosRequestConfig} from 'axios'; import * as crypto from 'crypto'; import * as fs from 'fs'; import * as nock from 'nock'; import * as path from 'path'; import * as qs from 'querystring'; -import * as request from 'request'; import * as url from 'url'; import {GoogleAuth} from '../src/auth/googleauth'; @@ -28,7 +28,6 @@ import {GoogleAuth} from '../src/auth/googleauth'; nock.disableNetConnect(); describe('OAuth2 client', () => { - const CLIENT_ID = 'CLIENT_ID'; const CLIENT_SECRET = 'CLIENT_SECRET'; const REDIRECT_URI = 'REDIRECT'; @@ -855,12 +854,8 @@ describe('OAuth2 client', () => { const auth = new GoogleAuth(); const oauth2client = new auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI); - oauth2client.request(({} as request.OptionsWithUri), (err, result) => { - assert(err); - if (err) { - assert.equal( - err.message, 'No access, refresh token or API key is set.'); - } + oauth2client.request({}, (err, result) => { + assert.equal(err!.message, 'No access, refresh token or API key is set.'); assert.equal(result, null); done(); }); @@ -871,10 +866,7 @@ describe('OAuth2 client', () => { const oauth2client = new auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI); oauth2client.refreshAccessToken((err, result) => { - assert(err); - if (err) { - assert.equal(err.message, 'No refresh token is set.'); - } + assert.equal(err!.message, 'No refresh token is set.'); assert.equal(result, null); done(); }); @@ -902,7 +894,7 @@ describe('OAuth2 client', () => { oauth2client.credentials = {refresh_token: 'refresh-token-placeholder'}; - oauth2client.request({uri: 'http://example.com'}, () => { + oauth2client.request({url: 'http://example.com'}, () => { assert.equal('abc123', oauth2client.credentials.access_token); done(); }); @@ -919,7 +911,7 @@ describe('OAuth2 client', () => { expiry_date: (new Date()).getTime() - 1000 }; - oauth2client.request({uri: 'http://example.com'}, () => { + oauth2client.request({url: 'http://example.com'}, () => { assert.equal('abc123', oauth2client.credentials.access_token); done(); }); @@ -936,7 +928,7 @@ describe('OAuth2 client', () => { expiry_date: (new Date()).getTime() + 1000 }; - oauth2client.request({uri: 'http://example.com'}, () => { + oauth2client.request({url: 'http://example.com'}, () => { assert.equal( 'initial-access-token', oauth2client.credentials.access_token); assert.equal(false, scope.isDone()); @@ -954,7 +946,7 @@ describe('OAuth2 client', () => { refresh_token: 'refresh-token-placeholder' }; - oauth2client.request({uri: 'http://example.com'}, () => { + oauth2client.request({url: 'http://example.com'}, () => { assert.equal( 'initial-access-token', oauth2client.credentials.access_token); assert.equal(false, scope.isDone()); @@ -977,7 +969,7 @@ describe('OAuth2 client', () => { refresh_token: 'refresh-token-placeholder' }; - oauth2client.request({uri: 'http://example.com/access'}, () => { + oauth2client.request({url: 'http://example.com/access'}, () => { assert.equal('abc123', oauth2client.credentials.access_token); done(); }); @@ -996,7 +988,7 @@ describe('OAuth2 client', () => { oauth2client.credentials = {access_token: 'abc', refresh_token: 'abc'}; oauth2client.revokeCredentials((err, result) => { assert.equal(err, null); - assert.equal(result.success, true); + assert.equal(result!.data!.success, true); assert.equal(JSON.stringify(oauth2client.credentials), '{}'); scope.done(); done(); @@ -1010,10 +1002,7 @@ describe('OAuth2 client', () => { new auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI); oauth2client.credentials = {refresh_token: 'abc'}; oauth2client.revokeCredentials((err, result) => { - assert(err); - if (err) { - assert.equal(err.message, 'No access token to revoke.'); - } + assert.equal(err!.message, 'No access token to revoke.'); assert.equal(result, null); assert.equal(JSON.stringify(oauth2client.credentials), '{}'); done(); @@ -1034,8 +1023,8 @@ describe('OAuth2 client', () => { const oauth2client = new auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI); oauth2client.getToken('code here', (err, tokens) => { - assert(tokens.expiry_date >= now + (10 * 1000)); - assert(tokens.expiry_date <= now + (15 * 1000)); + assert(tokens!.expiry_date! >= now + (10 * 1000)); + assert(tokens!.expiry_date! <= now + (15 * 1000)); scope.done(); done(); }); diff --git a/test/test.refresh.ts b/test/test.refresh.ts index e3de2f73..778b9a57 100644 --- a/test/test.refresh.ts +++ b/test/test.refresh.ts @@ -33,7 +33,6 @@ function createJSON() { } describe('.fromJson', () => { - it('should error on null json', (done) => { const auth = new GoogleAuth(); const refresh = new auth.UserRefreshClient(); @@ -127,7 +126,6 @@ describe('.fromJson', () => { }); describe('.fromStream', () => { - it('should error on null stream', (done) => { const auth = new GoogleAuth(); const refresh = new auth.UserRefreshClient(); diff --git a/test/test.transporters.ts b/test/test.transporters.ts index 510b3d0d..c04539fd 100644 --- a/test/test.transporters.ts +++ b/test/test.transporters.ts @@ -15,8 +15,9 @@ */ import * as assert from 'assert'; +import {AxiosRequestConfig} from 'axios'; import * as nock from 'nock'; -import * as request from 'request'; + import {DefaultTransporter, RequestError} from '../src/transporters'; // tslint:disable-next-line no-var-requires @@ -25,17 +26,13 @@ const version = require('../../package.json').version; nock.disableNetConnect(); describe('Transporters', () => { - const defaultUserAgentRE = 'google-api-nodejs-client/\\d+.\\d+.\\d+'; const transporter = new DefaultTransporter(); it('should set default client user agent if none is set', () => { - const opts = transporter.configure(({} as request.OptionsWithUrl)); + const opts = transporter.configure(); const re = new RegExp(defaultUserAgentRE); - assert(opts.headers); - if (opts.headers) { - assert(re.test(opts.headers['User-Agent'])); - } + assert(re.test(opts.headers!['User-Agent'])); }); it('should append default client user agent to the existing user agent', @@ -44,10 +41,7 @@ describe('Transporters', () => { const opts = transporter.configure( {headers: {'User-Agent': applicationName}, url: ''}); const re = new RegExp(applicationName + ' ' + defaultUserAgentRE); - assert(opts.headers); - if (opts.headers) { - assert(re.test(opts.headers['User-Agent'])); - } + assert(re.test(opts.headers!['User-Agent'])); }); it('should not append default client user agent to the existing user agent more than once', @@ -56,10 +50,7 @@ describe('Transporters', () => { 'MyTestApplication-1.0 google-api-nodejs-client/' + version; const opts = transporter.configure( {headers: {'User-Agent': applicationName}, url: ''}); - assert(opts.headers); - if (opts.headers) { - assert.equal(opts.headers['User-Agent'], applicationName); - } + assert.equal(opts.headers!['User-Agent'], applicationName); }); it('should create a single error from multiple response errors', (done) => { @@ -71,15 +62,12 @@ describe('Transporters', () => { transporter.request( { - uri: 'http://example.com/api', + url: 'http://example.com/api', }, (error) => { - assert(error); - if (error) { - assert(error.message === 'Error 1\nError 2'); - assert.equal((error as RequestError).code, 500); - assert.equal((error as RequestError).errors.length, 2); - } + assert(error!.message === 'Error 1\nError 2'); + assert.equal((error as RequestError).code, 500); + assert.equal((error as RequestError).errors.length, 2); done(); }); }); @@ -89,14 +77,24 @@ describe('Transporters', () => { transporter.request( { - uri: 'http://example.com/api', + url: 'http://example.com/api', }, (error) => { - assert(error); - if (error) { - assert(error.message === 'Not found'); - assert.equal((error as RequestError).code, 404); - } + assert(error!.message === 'Not found'); + assert.equal((error as RequestError).code, 404); + done(); + }); + }); + + it('should return an error if you try to use request config options', (done) => { + const expected = + '\'uri\' is not a valid configuration option. Please use \'url\' instead. This library is using Axios for requests. Please see https://github.com/axios/axios to learn more about the valid request options.'; + transporter.request( + { + uri: 'http://example.com/api', + } as AxiosRequestConfig, + (error) => { + assert.equal(error!.message, expected); done(); }); });