From dbea249a36a5ddb774602de234cd0c84cd5736a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 13 Jan 2021 14:58:36 +0100 Subject: [PATCH 1/8] chore(deps): add openid-client --- package-lock.json | 261 +++++++++++++++++++++++++++++++++++++++++++--- package.json | 1 + 2 files changed, 249 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 47ca062c3c..11672e5e4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1865,12 +1865,36 @@ } } }, + "@sindresorhus/is": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.0.tgz", + "integrity": "sha512-FyD2meJpDPjyNQejSjvnhpgI/azsQkA4lGbuu5BQZfjvJ9cbRZXzeWL2HceCekW4lixO9JPesIIQkSoLjeJHNQ==" + }, + "@szmarczak/http-timer": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.5.tgz", + "integrity": "sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ==", + "requires": { + "defer-to-connect": "^2.0.0" + } + }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "dev": true }, + "@types/cacheable-request": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.1.tgz", + "integrity": "sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ==", + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "*", + "@types/node": "*", + "@types/responselike": "*" + } + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -1882,6 +1906,11 @@ "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==", "dev": true }, + "@types/http-cache-semantics": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz", + "integrity": "sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A==" + }, "@types/json-schema": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", @@ -1894,6 +1923,14 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "@types/keyv": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.1.tgz", + "integrity": "sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw==", + "requires": { + "@types/node": "*" + } + }, "@types/minimist": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.1.tgz", @@ -1903,8 +1940,7 @@ "@types/node": { "version": "12.12.53", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.53.tgz", - "integrity": "sha512-51MYTDTyCziHb70wtGNFRwB4l+5JNvdqzFSkbDvpbftEgVUBEE+T5f7pROhWMp/fxp07oNIEQZd5bbfAH22ohQ==", - "dev": true + "integrity": "sha512-51MYTDTyCziHb70wtGNFRwB4l+5JNvdqzFSkbDvpbftEgVUBEE+T5f7pROhWMp/fxp07oNIEQZd5bbfAH22ohQ==" }, "@types/normalize-package-data": { "version": "2.4.0", @@ -1937,6 +1973,14 @@ "safe-buffer": "*" } }, + "@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "requires": { + "@types/node": "*" + } + }, "@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", @@ -2225,7 +2269,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, "requires": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" @@ -2864,6 +2907,11 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" }, + "base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -3208,6 +3256,32 @@ "unset-value": "^1.0.0" } }, + "cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==" + }, + "cacheable-request": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.1.tgz", + "integrity": "sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^2.0.0" + }, + "dependencies": { + "normalize-url": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" + } + } + }, "call-bind": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", @@ -3370,8 +3444,7 @@ "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" }, "cli-highlight": { "version": "2.1.4", @@ -3484,6 +3557,21 @@ } } }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "requires": { + "mimic-response": "^1.0.0" + }, + "dependencies": { + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + } + } + }, "coa": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", @@ -4325,6 +4413,11 @@ "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", "dev": true }, + "defer-to-connect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.0.tgz", + "integrity": "sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg==" + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -4670,7 +4763,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "requires": { "once": "^1.4.0" } @@ -6587,7 +6679,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, "requires": { "pump": "^3.0.0" } @@ -6687,6 +6778,39 @@ } } }, + "got": { + "version": "11.8.1", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.1.tgz", + "integrity": "sha512-9aYdZL+6nHmvJwHALLwKSUZ0hMwGaJGYv3hoPLPgnT8BoBXm1SjnZeky+91tfwJaDzun2s4RsBRy48IEYv2q2Q==", + "requires": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.1", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "dependencies": { + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + } + }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + } + } + }, "graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", @@ -6966,6 +7090,11 @@ } } }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, "http-errors": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", @@ -7020,6 +7149,22 @@ "sshpk": "^1.7.0" } }, + "http2-wrapper": { + "version": "1.0.0-beta.5.2", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.0-beta.5.2.tgz", + "integrity": "sha512-xYz9goEyBnC8XwXDTuC/MZ6t+MrKVQZOk4s7+PaDkwIsQd8IwqvM+0M6bA/2lvG8GHXcPdf+MejTUeO2LCPCeQ==", + "requires": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "dependencies": { + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" + } + } + }, "https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", @@ -7114,8 +7259,7 @@ "indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" }, "indexes-of": { "version": "1.0.1", @@ -7591,6 +7735,11 @@ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", "dev": true }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -7724,6 +7873,14 @@ "safe-buffer": "^5.0.1" } }, + "keyv": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.3.tgz", + "integrity": "sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==", + "requires": { + "json-buffer": "3.0.1" + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -7949,11 +8106,15 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" } @@ -7968,6 +8129,11 @@ "semver": "^5.6.0" } }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -12809,6 +12975,11 @@ } } }, + "object-hash": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.1.1.tgz", + "integrity": "sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ==" + }, "object-inspect": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", @@ -13007,6 +13178,11 @@ "has": "^1.0.3" } }, + "oidc-token-hash": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.0.tgz", + "integrity": "sha512-8Yr4CZSv+Tn8ZkN3iN2i2w2G92mUKClp4z7EGUfdsERiYSbj7P4i/NHm72ft+aUdsiFx9UdIPSTwbyzQ6C4URg==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -13024,6 +13200,31 @@ "mimic-fn": "^2.1.0" } }, + "openid-client": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-4.2.2.tgz", + "integrity": "sha512-aifblOWaE4nT7fZ/ax/5Ohzs9VrJOtxVvhuAMVF4QsPVNgLWDyGprPQXDZf7obEyaShzNlyv7aoIDPEVFO/XZQ==", + "requires": { + "base64url": "^3.0.1", + "got": "^11.8.0", + "jose": "^2.0.2", + "lru-cache": "^6.0.0", + "make-error": "^1.3.6", + "object-hash": "^2.0.1", + "oidc-token-hash": "^5.0.0", + "p-any": "^3.0.0" + }, + "dependencies": { + "jose": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.3.tgz", + "integrity": "sha512-L+RlDgjO0Tk+Ki6/5IXCSEnmJCV8iMFZoBuEgu2vPQJJ4zfG/k3CAqZUMKDYNRHIDyy0QidJpOvX0NgpsAqFlw==", + "requires": { + "@panva/asn1.js": "^1.0.0" + } + } + } + }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -13044,6 +13245,20 @@ "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", "dev": true }, + "p-any": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-any/-/p-any-3.0.0.tgz", + "integrity": "sha512-5rqbqfsRWNb0sukt0awwgJMlaep+8jV45S15SKKB34z4UuzjcofIfnriCBhWjZP2jbVtjt9yRl7buB6RlKsu9w==", + "requires": { + "p-cancelable": "^2.0.0", + "p-some": "^5.0.0" + } + }, + "p-cancelable": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz", + "integrity": "sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg==" + }, "p-filter": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz", @@ -13102,6 +13317,15 @@ "retry": "^0.12.0" } }, + "p-some": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-some/-/p-some-5.0.0.tgz", + "integrity": "sha512-Js5XZxo6vHjB9NOYAzWDYAIyyiPvva0DWESAIWIK7uhSpGsyg5FwUPxipU/SOQx5x9EqhOh545d1jo6cVkitig==", + "requires": { + "aggregate-error": "^3.0.0", + "p-cancelable": "^2.0.0" + } + }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -14549,7 +14773,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -15152,6 +15375,11 @@ "path-parse": "^1.0.6" } }, + "resolve-alpn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.0.0.tgz", + "integrity": "sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA==" + }, "resolve-from": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", @@ -15236,6 +15464,14 @@ } } }, + "responselike": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz", + "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==", + "requires": { + "lowercase-keys": "^2.0.0" + } + }, "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", @@ -17926,8 +18162,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yargonaut": { "version": "1.1.4", diff --git a/package.json b/package.json index 8d1d392dcf..a110965eed 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "jsonwebtoken": "^8.5.1", "nodemailer": "^6.4.16", "oauth": "^0.9.15", + "openid-client": "^4.2.2", "preact": "^10.4.1", "preact-render-to-string": "^5.1.7", "querystring": "^0.2.0", From ecdfd31afb1e5ba6d19b0831a6a60857661e5991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 13 Jan 2021 23:04:57 +0100 Subject: [PATCH 2/8] refactor: split oauth 1/2 client --- src/server/lib/oauth/client.js | 100 ++++++----------------- src/server/lib/oauth/client.legacy.js | 110 ++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 74 deletions(-) create mode 100644 src/server/lib/oauth/client.legacy.js diff --git a/src/server/lib/oauth/client.js b/src/server/lib/oauth/client.js index f3f6a97145..85270e3ce5 100644 --- a/src/server/lib/oauth/client.js +++ b/src/server/lib/oauth/client.js @@ -1,81 +1,33 @@ -import { OAuth, OAuth2 } from 'oauth' -import querystring from 'querystring' -import logger from '../../../lib/logger' -import { sign as jwtSign } from 'jsonwebtoken' +import { Issuer } from 'openid-client' /** - * @TODO Refactor to remove dependancy on 'oauth' package - * It is already quite monkey patched, we don't use all the features and and it - * would be easier to maintain if all the code was native to next-auth. + * Creates an OAuth 2.0, OpenID Connect compatible client */ -export default function oAuthClient (provider) { - if (provider.version?.startsWith('2.')) { - // Handle OAuth v2.x - const authorizationUrl = new URL(provider.authorizationUrl) - const basePath = authorizationUrl.origin - const authorizePath = authorizationUrl.pathname - const accessTokenPath = new URL(provider.accessTokenUrl).pathname - const oauth2Client = new OAuth2( - provider.clientId, - provider.clientSecret, - basePath, - authorizePath, - accessTokenPath, - provider.headers - ) - oauth2Client.getOAuthAccessToken = getOAuth2AccessToken - oauth2Client.get = getOAuth2 - return oauth2Client - } - // Handle OAuth v1.x - const oauth1Client = new OAuth( - provider.requestTokenUrl, - provider.accessTokenUrl, - provider.clientId, - provider.clientSecret, - provider.version || '1.0', - provider.callbackUrl, - provider.encoding || 'HMAC-SHA1' - ) - - // Promisify get() and getOAuth2AccessToken() for OAuth1 - const originalGet = oauth1Client.get - oauth1Client.get = (...args) => { - return new Promise((resolve, reject) => { - originalGet(...args, (error, result) => { - if (error) { - return reject(error) - } - resolve(result) - }) - }) - } - const originalGetOAuth1AccessToken = oauth1Client.getOAuthAccessToken - oauth1Client.getOAuthAccessToken = (...args) => { - return new Promise((resolve, reject) => { - originalGetOAuth1AccessToken(...args, (error, accessToken, refreshToken, results) => { - if (error) { - return reject(error) - } - resolve({ accessToken, refreshToken, results }) - }) - }) - } +export default function getOAuthClient (provider) { + const issuer = new Issuer({ + issuer: provider.id, + authorization_endpoint: provider.authorizationUrl, + userinfo_endpoint: provider.profileUrl, + token_endpoint: provider.accessTokenUrl + }) - const originalGetOAuthRequestToken = oauth1Client.getOAuthRequestToken - oauth1Client.getOAuthRequestToken = (...args) => { - return new Promise((resolve, reject) => { - originalGetOAuthRequestToken(...args, (error, oauthToken) => { - if (error) { - return reject(error) - } - resolve(oauthToken) - }) - }) - } - return oauth1Client + const client = new issuer.Client({ + client_id: provider.clientId, + client_secret: provider.clientSecret, + redirect_uris: [provider.callbackUrl], + response_types: ['code'] + }) + + return client } +/// LEGACY OAuth 2.0 implementation downwards /// +// TODO: handle all edge cases below in the code above +// https://github.com/panva/node-openid-client/blob/master/docs/README.md +const querystring = require('querystring') +const logger = require('../../../lib/logger') +const { sign: jwtSign } = require('jsonwebtoken') + /** * @TODO Refactor monkey patching in OAuth2.getOAuthAccessToken() and OAuth2.get() * These methods have been forked from `node-oauth` to fix bugs; it may make @@ -87,7 +39,7 @@ export default function oAuthClient (provider) { /** * Ported from https://github.com/ciaranj/node-oauth/blob/a7f8a1e21c362eb4ed2039431fb9ac2ae749f26a/lib/oauth2.js */ -async function getOAuth2AccessToken (code, provider) { +export async function getOAuth2AccessToken (code, provider) { const url = provider.accessTokenUrl const params = { ...provider.params } const headers = { ...provider.headers } @@ -175,7 +127,7 @@ async function getOAuth2AccessToken (code, provider) { * 18/08/2020 @robertcraigie added results parameter to pass data to an optional request preparer. * e.g. see providers/bungie */ -async function getOAuth2 (provider, accessToken, results) { +export async function getOAuth2 (provider, accessToken, results) { let url = provider.profileUrl const headers = { ...provider.headers } diff --git a/src/server/lib/oauth/client.legacy.js b/src/server/lib/oauth/client.legacy.js new file mode 100644 index 0000000000..93b914961b --- /dev/null +++ b/src/server/lib/oauth/client.legacy.js @@ -0,0 +1,110 @@ +import { OAuth as OAuth1 } from 'oauth' +import logger from 'src/lib/logger' + +/** + * Creates an OAuth 1.x compatible client + * @deprecated This method is implemented with 'oauth' and is being phased out. + */ +export default function getOAuthClientLegacy (provider) { + if (!provider.version?.startsWith('1')) { + throw new Error('You are using a legacy method to retrieve an OAuth 1 client for an OAuth 2 or OIDC client.') + } + const client = new OAuth1( + provider.requestTokenUrl, + provider.accessTokenUrl, + provider.clientId, + provider.clientSecret, + provider.version || '1.0', + provider.callbackUrl, + provider.encoding || 'HMAC-SHA1' + ) + + // Promisify get() and getOAuth2AccessToken() for OAuth1 + const originalGet = client.get + client.get = (...args) => { + return new Promise((resolve, reject) => { + originalGet(...args, (error, result) => { + if (error) { + return reject(error) + } + resolve(result) + }) + }) + } + const originalGetOAuth1AccessToken = client.getOAuthAccessToken + client.getOAuthAccessToken = (...args) => { + return new Promise((resolve, reject) => { + originalGetOAuth1AccessToken(...args, (error, accessToken, refreshToken, results) => { + if (error) { + return reject(error) + } + resolve({ accessToken, refreshToken, results }) + }) + }) + } + + const originalGetOAuthRequestToken = client.getOAuthRequestToken + client.getOAuthRequestToken = (...args) => { + return new Promise((resolve, reject) => { + originalGetOAuthRequestToken(...args, (error, oauthToken) => { + if (error) { + return reject(error) + } + resolve(oauthToken) + }) + }) + } + client.getProfile = getProfile + + return client +} + +/** + * //6/30/2020 @geraldnolan added userData parameter to attach additional data to the profileData object + * Returns profile, raw profile and auth provider details + */ +async function getProfile ({ tokens: { accessToken, refreshToken }, provider }) { + let profileData + try { + profileData = await this.get(provider.profileUrl, accessToken, refreshToken) + + // Convert profileData into an object if it's a string + if (typeof profileData === 'string' || profileData instanceof String) { + profileData = JSON.parse(profileData) + } + + logger.debug('PROFILE_DATA', profileData) + + const profile = await provider.profile(profileData) + // Return profile, raw profile and auth provider details + return { + profile: { + ...profile, + email: profile.email?.toLowerCase() ?? null + }, + account: { + provider: provider.id, + type: provider.type, + id: profile.id, + refreshToken, + accessToken, + accessTokenExpires: null + }, + OAuthProfile: profileData + } + } catch (error) { + // If we didn't get a response either there was a problem with the provider + // response *or* the user cancelled the action with the provider. + // + // Unfortuately, we can't tell which - at least not in a way that works for + // all providers, so we return an empty object; the user should then be + // redirected back to the sign up page. We log the error to help developers + // who might be trying to debug this when configuring a new provider. + logger.error('OAUTH_PARSE_PROFILE_ERROR', { error, profileData }) + return { + profile: null, + account: null, + OAuthProfile: profileData + } + } +} From 502e8ddc18181772d44fdbe390ac49d83d71c8c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 13 Jan 2021 23:12:37 +0100 Subject: [PATCH 3/8] refactor: use openid-client for signin --- src/server/lib/signin/oauth.js | 88 +++++++++++++++++++++++----------- src/server/routes/signin.js | 9 ++-- 2 files changed, 63 insertions(+), 34 deletions(-) diff --git a/src/server/lib/signin/oauth.js b/src/server/lib/signin/oauth.js index 99f79c1c50..03a2fe917c 100644 --- a/src/server/lib/signin/oauth.js +++ b/src/server/lib/signin/oauth.js @@ -1,39 +1,69 @@ -import oAuthClient from '../oauth/client' +import getOAuthClient from '../oauth/client' +import getOAuthClientLegacy from '../oauth/client.legacy' import { createHash } from 'crypto' import logger from '../../../lib/logger' -export default async function oauth (provider, csrfToken) { - const { callbackUrl } = provider - const client = oAuthClient(provider) - if (provider.version?.startsWith('2.')) { - // Handle OAuth v2.x - let url = client.getAuthorizeUrl({ - redirect_uri: callbackUrl, - scope: provider.scope, - // A hash of the NextAuth.js CSRF token is used as the state - state: createHash('sha256').update(csrfToken).digest('hex'), - ...provider.authorizationParams - }) +/** + * Returns an OAuth `/authorization` url with params. + * @param {import("next").NextApiRequest} req + * @docs https://tools.ietf.org/html/rfc6749#section-4.1.1 (/authorize) + * @docs https://tools.ietf.org/html/rfc7636#section-4.3 (PKCE) + */ +export default async function getAuthorizationUrl (req) { + try { + const { provider } = req.options + const { callbackUrl } = provider - // If the authorizationUrl specified in the config has query parameters on it - // make sure they are included in the URL we return. - // - // This is a fix for an open issue with the oAuthClient library we are using - // which inadvertantly strips them. - // - // https://github.com/ciaranj/node-oauth/pull/193 - if (provider.authorizationUrl.includes('?')) { - const parseUrl = new URL(provider.authorizationUrl) - const baseUrl = `${parseUrl.origin}${parseUrl.pathname}?` - url = url.replace(baseUrl, provider.authorizationUrl + '&') - } + // Handle OAuth v2.x and OIDC + if (provider.version?.startsWith('2.')) { + const { csrfToken } = provider + const client = getOAuthClient(provider) - return url - } + const url = new URL(provider.authorizationUrl) + const authorizeParams = Object.fromEntries(url.searchParams.entries()) - try { + /** @type {import("openid-client").AuthorizationParameters} */ + const params = { + redirect_uri: provider.callbackUrl, + scope: provider.scope, + // Preserve params from authorizationUrl + ...authorizeParams, + // Makes it possible to defined params as an object for the provider + ...provider.authorizationParams + } + + switch (provider.verification) { + case 'pkce': + // TODO: handle PKCE + // const codeChallenge generators.codeChallenge(codeVerifier) + // logger.debug('OAUTH_AUTHORIZATION_URL', { + // message: 'PKCE code_challenge being sent', codeChallenge + // }) + // params.code_challenge = codeChallenge + break + case 'state': { + const state = createHash('sha256').update(csrfToken).digest('hex') + logger.debug('OAUTH_AUTHORIZATION_URL', { + message: 'State being sent', state + }) + params.state = state + break + } + case 'none': + default: + break + } + + logger.debug('OAUTH_AUTHORIZATION_URL', { params, provider }) + return client.authorizationUrl(params) + } + + // Handle OAuth 1.x + const client = getOAuthClientLegacy(provider) const oAuthToken = await client.getOAuthRequestToken(callbackUrl) - return `${provider.authorizationUrl}?oauth_token=${oAuthToken}` + const url = `${provider.authorizationUrl}?oauth_token=${oAuthToken}` + logger.debug('OAUTH_AUTHORIZATION_URL', { url, provider }) + return url } catch (error) { logger.error('GET_AUTHORISATION_URL_ERROR', error) throw error diff --git a/src/server/routes/signin.js b/src/server/routes/signin.js index e5e323911f..4159392f3b 100644 --- a/src/server/routes/signin.js +++ b/src/server/routes/signin.js @@ -1,4 +1,4 @@ -import oAuthSignin from '../lib/signin/oauth' +import getAuthorizationUrl from '../lib/signin/oauth' import emailSignin from '../lib/signin/email' import logger from '../../lib/logger' @@ -9,8 +9,7 @@ export default async function signin (req, res) { baseUrl, basePath, adapter, - callbacks, - csrfToken + callbacks } = req.options if (!provider.type) { @@ -19,8 +18,8 @@ export default async function signin (req, res) { if (provider.type === 'oauth' && req.method === 'POST') { try { - const oAuthSigninUrl = await oAuthSignin(provider, csrfToken) - return res.redirect(oAuthSigninUrl) + const authorizationUrl = await getAuthorizationUrl(req) + return res.redirect(authorizationUrl) } catch (error) { logger.error('SIGNIN_OAUTH_ERROR', error) return res.redirect(`${baseUrl}${basePath}/error?error=OAuthSignin`) From 2569ab9dd3eec8cb0929386f275d5cb042484e8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Thu, 14 Jan 2021 00:58:56 +0100 Subject: [PATCH 4/8] docs(ts): add some internal types --- package.json | 3 +- src/server/index.d.ts | 60 ++++++++++++++++++++++++++++++ src/server/lib/oauth/callback.d.ts | 21 +++++++++++ 3 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 src/server/index.d.ts create mode 100644 src/server/lib/oauth/callback.d.ts diff --git a/package.json b/package.json index a110965eed..206faf964a 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,8 @@ "ignore": [ "test/", "pages/", - "components/" + "components/", + "**/*/*.d.ts" ] } } diff --git a/src/server/index.d.ts b/src/server/index.d.ts new file mode 100644 index 0000000000..ea7f8b16e0 --- /dev/null +++ b/src/server/index.d.ts @@ -0,0 +1,60 @@ +import {NextApiRequest} from "next" +import {defaultCookies} from "./lib/cookie" + +export interface Provider { + id: string + name: string + type: string + clientId: string + clientSecret: string + version?: string + scope?: string + accessTokenUrl?: string + authorizationUrl?: string + profileUrl?: string + profile?(profile: {}): Promise<{}> + verifications?: ("state" | "pkce")[] +} + +export interface NextApiRequestWithOptions extends NextApiRequest { + options: { + debug?: boolean + theme?: 'auto' | 'light' | 'dark' + adapter?: {} + provider?: Provider + baseUrl: string + basePath: string + secret: string + cookies: ReturnType + callbackUrl: string + pages: {} + jwt: { + secret: string, + maxAge: number, + async encode(): any, + async decode(): any, + encryption: boolean + } + events: { + signIn?(): Promise + signOut?(): Promise + createUser?(): Promise + updateUser?(): Promise + linkAccount?(): Promise + session?(): Promise + erro?(): Promise + } + callbacks: { + signIn(): Promise + jwt(): Promise<{}> + session(): Promise<{}> + redirect(): Promise + } + session: { + jwt: boolean, + maxAge: number + updateAge: number + } + csrfToken: string + } +} \ No newline at end of file diff --git a/src/server/lib/oauth/callback.d.ts b/src/server/lib/oauth/callback.d.ts new file mode 100644 index 0000000000..3dfdc4b137 --- /dev/null +++ b/src/server/lib/oauth/callback.d.ts @@ -0,0 +1,21 @@ +export interface HandleOAuthCallbackResultObject { + /** + * Profile data returned by the `profile` provider option + * @docs https://next-auth.js.org/configuration/providers#oauth-provider-options + */ + profile?: Record + /** Contains tokens (access_token, refresh_token, id_token), and ... */ + account?: { + accessToken: string + accessTokenExpires: Date | string | null + refreshToken?: string + idToken?: string + [key: string]: any + } + /** Raw profile returned from the OAuth provider */ + OAuthProfile?: { + /** Returned by the Apple provider */ + user?: Record + [key: string]: any + } +} \ No newline at end of file From f29b45bf877561d0118629cb9f97cc0159c7a445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Thu, 14 Jan 2021 00:59:29 +0100 Subject: [PATCH 5/8] docs: add error code explanations --- www/docs/errors.md | 5 +++++ www/docs/getting-started/rest-api.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/www/docs/errors.md b/www/docs/errors.md index 7fd13f8f9a..17b7708ff0 100644 --- a/www/docs/errors.md +++ b/www/docs/errors.md @@ -41,6 +41,9 @@ These errors are displayed on the terminal. #### OAUTH_CALLBACK_HANDLER_ERROR +The process of exchanging an authorization code for tokens (access_token, refresh_token, id_token) and userinfo failed. +https://www.oauth.com/oauth2-servers/server-side-apps/authorization-code + --- ### Signin / Callback @@ -51,6 +54,8 @@ These errors are displayed on the terminal. #### CALLBACK_OAUTH_ERROR +The callback handler at url /api/auth/callback/:provider failed while processing an OAuth callback request. + #### SIGNIN_EMAIL_ERROR #### CALLBACK_EMAIL_ERROR diff --git a/www/docs/getting-started/rest-api.md b/www/docs/getting-started/rest-api.md index 97c940e665..42e6e65c4a 100644 --- a/www/docs/getting-started/rest-api.md +++ b/www/docs/getting-started/rest-api.md @@ -15,7 +15,7 @@ Starts an OAuth signin flow for the specified provider. The POST submission requires CSRF token from `/api/auth/csrf`. -#### GET /api/auth/callback/:provider +#### GET/POST /api/auth/callback/:provider Handles returning requests from OAuth services during sign in. From cd7d6df3c5144aa91225740709597da78597dcd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Thu, 14 Jan 2021 01:01:15 +0100 Subject: [PATCH 6/8] fix: access csrfToken correctly --- src/server/lib/signin/oauth.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/server/lib/signin/oauth.js b/src/server/lib/signin/oauth.js index 03a2fe917c..4930887ada 100644 --- a/src/server/lib/signin/oauth.js +++ b/src/server/lib/signin/oauth.js @@ -5,18 +5,17 @@ import logger from '../../../lib/logger' /** * Returns an OAuth `/authorization` url with params. - * @param {import("next").NextApiRequest} req + * @param {import("../../index").NextApiRequestWithOptions} req * @docs https://tools.ietf.org/html/rfc6749#section-4.1.1 (/authorize) * @docs https://tools.ietf.org/html/rfc7636#section-4.3 (PKCE) */ export default async function getAuthorizationUrl (req) { try { - const { provider } = req.options + const { provider, csrfToken, secret } = req.options const { callbackUrl } = provider // Handle OAuth v2.x and OIDC if (provider.version?.startsWith('2.')) { - const { csrfToken } = provider const client = getOAuthClient(provider) const url = new URL(provider.authorizationUrl) From 7430a9257ed48087ba5b7b104af724583d547615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Thu, 14 Jan 2021 01:02:01 +0100 Subject: [PATCH 7/8] feat: support pkce and state verification --- src/providers/github.js | 7 ++++++- src/server/lib/signin/oauth.js | 35 +++++++++++++++------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/providers/github.js b/src/providers/github.js index 0b9df2e55b..b343068bdf 100644 --- a/src/providers/github.js +++ b/src/providers/github.js @@ -1,3 +1,7 @@ +/** + * @param {import("next-auth").Provider} options + * @returns {import("next-auth").Provider} + */ export default (options) => { return { id: 'github', @@ -8,7 +12,7 @@ export default (options) => { accessTokenUrl: 'https://github.com/login/oauth/access_token', authorizationUrl: 'https://github.com/login/oauth/authorize', profileUrl: 'https://api.github.com/user', - profile: (profile) => { + profile (profile) { return { id: profile.id, name: profile.name || profile.login, @@ -16,6 +20,7 @@ export default (options) => { image: profile.avatar_url } }, + verifications: ['state', 'pkce'], ...options } } diff --git a/src/server/lib/signin/oauth.js b/src/server/lib/signin/oauth.js index 4930887ada..9bf70d1781 100644 --- a/src/server/lib/signin/oauth.js +++ b/src/server/lib/signin/oauth.js @@ -2,6 +2,7 @@ import getOAuthClient from '../oauth/client' import getOAuthClientLegacy from '../oauth/client.legacy' import { createHash } from 'crypto' import logger from '../../../lib/logger' +import { generators } from 'openid-client' /** * Returns an OAuth `/authorization` url with params. @@ -31,26 +32,20 @@ export default async function getAuthorizationUrl (req) { ...provider.authorizationParams } - switch (provider.verification) { - case 'pkce': - // TODO: handle PKCE - // const codeChallenge generators.codeChallenge(codeVerifier) - // logger.debug('OAUTH_AUTHORIZATION_URL', { - // message: 'PKCE code_challenge being sent', codeChallenge - // }) - // params.code_challenge = codeChallenge - break - case 'state': { - const state = createHash('sha256').update(csrfToken).digest('hex') - logger.debug('OAUTH_AUTHORIZATION_URL', { - message: 'State being sent', state - }) - params.state = state - break - } - case 'none': - default: - break + if (provider.verifications?.includes('pkce')) { + const codeChallenge = generators.codeChallenge(secret) + logger.debug('OAUTH_AUTHORIZATION_URL', { + message: 'PKCE code_challenge being sent', codeChallenge + }) + params.code_challenge_method = 'S256' + params.code_challenge = codeChallenge + } + if (provider.verifications?.includes('state')) { + const state = createHash('sha256').update(csrfToken).digest('hex') + logger.debug('OAUTH_AUTHORIZATION_URL', { + message: 'State being sent', state + }) + params.state = state } logger.debug('OAUTH_AUTHORIZATION_URL', { params, provider }) From 3b083818617268907340b887c7a9bd469eefab6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Thu, 14 Jan 2021 01:02:33 +0100 Subject: [PATCH 8/8] refactor: use openid-client in callback handlers --- src/lib/errors.js | 18 ++- src/server/lib/oauth/callback.js | 204 ++++++++----------------------- src/server/routes/callback.js | 193 ++++++++++++++--------------- 3 files changed, 155 insertions(+), 260 deletions(-) diff --git a/src/lib/errors.js b/src/lib/errors.js index f73e977826..c01c0234ac 100644 --- a/src/lib/errors.js +++ b/src/lib/errors.js @@ -1,8 +1,7 @@ -class UnknownError extends Error { +export class UnknownError extends Error { constructor (message) { super(message) this.name = 'UnknownError' - this.message = message } toJSON () { @@ -16,26 +15,25 @@ class UnknownError extends Error { } } -class CreateUserError extends UnknownError { +export class CreateUserError extends UnknownError { constructor (message) { super(message) this.name = 'CreateUserError' - this.message = message } } // Thrown when an Email address is already associated with an account // but the user is trying an OAuth account that is not linked to it. -class AccountNotLinkedError extends UnknownError { +export class AccountNotLinkedError extends UnknownError { constructor (message) { super(message) this.name = 'AccountNotLinkedError' - this.message = message } } -module.exports = { - UnknownError, - CreateUserError, - AccountNotLinkedError +export class OAuthCallbackHandlerError extends UnknownError { + constructor (message) { + super(message) + this.name = 'OAuthCallbackHandlerError' + } } diff --git a/src/server/lib/oauth/callback.js b/src/server/lib/oauth/callback.js index 5fa25dd731..bd13d11d7c 100644 --- a/src/server/lib/oauth/callback.js +++ b/src/server/lib/oauth/callback.js @@ -1,169 +1,73 @@ import { createHash } from 'crypto' -import { decode as jwtDecode } from 'jsonwebtoken' -import oAuthClient from './client' +import getOAuthClientLegacy from './client.legacy' +import getOAuthClient from './client' import logger from '../../../lib/logger' -class OAuthCallbackError extends Error { - constructor (message) { - super(message) - this.name = 'OAuthCallbackError' - this.message = message - } -} +import { OAuthCallbackHandlerError } from '../../../lib/errors' -export default async function oAuthCallback (req) { - const { provider, csrfToken } = req.options - const client = oAuthClient(provider) +/** + * Handles exchange of the authorization code + * for OAuth tokens, fetches the profile data + * from the /userinfo endpoint + * @docs https://tools.ietf.org/html/rfc6749#section-4.1.3 + * @docs https://www.oauth.com/oauth2-servers/signing-in-with-google/verifying-the-user-info/ + * + * @param {import('../../index').NextApiRequestWithOptions} req + * @returns {Promise} + */ +export default async function handleOAuthCallback (req) { + const { body, options: { provider, csrfToken, secret } } = req + try { + if (body?.error) throw body.error + if (provider.version?.startsWith('2.')) { + const client = getOAuthClient(provider) - if (provider.version?.startsWith('2.')) { - // The "user" object is specific to the Apple provider and is provided on first sign in - // e.g. {"name":{"firstName":"Johnny","lastName":"Appleseed"},"email":"johnny.appleseed@nextauth.com"} - let { code, user, state } = req.query // eslint-disable-line camelcase - // For OAuth 2.0 flows, check state returned and matches expected value - // (a hash of the NextAuth.js CSRF token). - // - // Apple does not support state verification. - if (provider.id !== 'apple') { - const expectedState = createHash('sha256').update(csrfToken).digest('hex') - if (state !== expectedState) { - throw new OAuthCallbackError('Invalid state returned from OAuth provider') - } - } + const params = client.callbackParams(req) - if (req.method === 'POST') { - try { - const body = JSON.parse(JSON.stringify(req.body)) - if (body.error) { - throw new Error(body.error) - } + /** @type {import("openid-client").OAuthCallbackChecks} */ + const checks = { + response_type: 'code' + } - code = body.code - user = body.user != null ? JSON.parse(body.user) : null - } catch (error) { - logger.error('OAUTH_CALLBACK_HANDLER_ERROR', error, req.body, provider.id, code) - throw error + if (provider.verifications?.includes('pkce')) { + checks.code_verifier = secret + } + if (provider.verifications?.includes('state')) { + checks.state = createHash('sha256').update(csrfToken).digest('hex') } - } - // REVIEW: Is this used by any of the providers? - // Pass authToken in header by default (unless 'useAuthTokenHeader: false' is set) - if (Object.prototype.hasOwnProperty.call(provider, 'useAuthTokenHeader')) { - client.useAuthorizationHeaderforGET(provider.useAuthTokenHeader) - } else { - client.useAuthorizationHeaderforGET(true) - } + const tokens = await client.oauthCallback(provider.callbackUrl, params, checks) + const profile = await client.userinfo(tokens.access_token) - try { - const { accessToken, refreshToken, results } = await client.getOAuthAccessToken(code, provider) - const tokens = { accessToken, refreshToken, idToken: results.id_token } - let profileData - if (provider.idToken) { - // If we don't have an ID Token most likely the user hit a cancel - // button when signing in (or the provider is misconfigured). - // - // Unfortunately, we can't tell which, so we can't treat it as an - // error, so instead we just returning nothing, which will cause the - // user to be redirected back to the sign in page. - if (!results?.id_token) { - throw new OAuthCallbackError() - } + // The "user" object is specific to the Apple provider and is provided on first sign in + // e.g. {"name":{"firstName":"Johnny","lastName":"Appleseed"},"email":"johnny.appleseed@nextauth.com"} + const user = JSON.parse(body?.user ?? null) ?? req.query.user + if (user) { + profile.user = user + } - // Support services that use OpenID ID Tokens to encode profile data - profileData = decodeIdToken(results.id_token) - } else { - profileData = await client.get(provider, accessToken, results) + const result = { + profile: provider.profile(profile), + account: { + provider: provider.id, + type: provider.type, + // REVIEW: Why provider AND id? + id: provider.id, + ...tokens + }, + OAuthProfile: profile } - return _getProfile({ profileData, provider, tokens, user }) - } catch (error) { - logger.error('OAUTH_GET_ACCESS_TOKEN_ERROR', error, provider.id, code) - throw error + return result } - } - try { // Handle OAuth v1.x - const { - oauth_token: oauthToken, oauth_verifier: oauthVerifier - } = req.query - const { accessToken, refreshToken, results } = await client.getOAuthAccessToken(oauthToken, null, oauthVerifier) - const profileData = await client.get( - provider.profileUrl, - accessToken, - refreshToken - ) - - const tokens = { - accessToken, refreshToken, idToken: results.id_token - } + const client = getOAuthClientLegacy(provider) + const { oauth_token: oauthToken, oauth_verifier: oauthVerifier } = req.query + const { accessToken, refreshToken } = await client.getOAuthAccessToken(oauthToken, null, oauthVerifier) - return _getProfile({ - profileData, tokens, provider - }) + return client.getProfile({ tokens: { accessToken, refreshToken }, provider }) } catch (error) { - logger.error('OAUTH_V1_GET_ACCESS_TOKEN_ERROR', error) - throw error - } -} - -/** - * //6/30/2020 @geraldnolan added userData parameter to attach additional data to the profileData object - * Returns profile, raw profile and auth provider details - */ -async function _getProfile ({ - profileData, tokens: { accessToken, refreshToken, idToken }, provider, user -}) { - try { - // Convert profileData into an object if it's a string - if (typeof profileData === 'string' || profileData instanceof String) { - profileData = JSON.parse(profileData) - } - - // If a user object is supplied (e.g. Apple provider) add it to the profile object - if (user != null) { - profileData.user = user - } - - profileData.idToken = idToken - - logger.debug('PROFILE_DATA', profileData) - - const profile = await provider.profile(profileData) - // Return profile, raw profile and auth provider details - return { - profile: { - ...profile, - email: profile.email?.toLowerCase() ?? null - }, - account: { - provider: provider.id, - type: provider.type, - id: profile.id, - refreshToken, - accessToken, - accessTokenExpires: null - }, - OAuthProfile: profileData - } - } catch (exception) { - // If we didn't get a response either there was a problem with the provider - // response *or* the user cancelled the action with the provider. - // - // Unfortuately, we can't tell which - at least not in a way that works for - // all providers, so we return an empty object; the user should then be - // redirected back to the sign up page. We log the error to help developers - // who might be trying to debug this when configuring a new provider. - logger.error('OAUTH_PARSE_PROFILE_ERROR', exception, profileData) - return { - profile: null, - account: null, - OAuthProfile: profileData - } - } -} - -function decodeIdToken (idToken) { - if (!idToken) { - throw new OAuthCallbackError('Missing JWT ID Token') + logger.error('OAUTH_CALLBACK_HANDLER_ERROR', error) + throw new OAuthCallbackHandlerError('OAuth callback handler failed') } - return jwtDecode(idToken, { json: true }) } diff --git a/src/server/routes/callback.js b/src/server/routes/callback.js index 1def4c52a4..bfa3e4a7a1 100644 --- a/src/server/routes/callback.js +++ b/src/server/routes/callback.js @@ -1,23 +1,20 @@ -import oAuthCallback from '../lib/oauth/callback' +import handleOAuthCallback from '../lib/oauth/callback' import callbackHandler from '../lib/callback-handler' import * as cookie from '../lib/cookie' import logger from '../../lib/logger' import dispatchEvent from '../lib/dispatch-event' -/** Handle callbacks from login services */ +/** + * Handle callbacks from login services. + * Supports OAuth, Email and Credentials + * + * @param {import('../index').NextApiRequestWithOptions} req + * @param {import('next').NextApiResponse} res + */ export default async function callback (req, res) { const { - provider, - adapter, - baseUrl, - basePath, - secret, - cookies, - callbackUrl, - pages, - jwt, - events, - callbacks, + provider, adapter, baseUrl, basePath, secret, cookies, + callbackUrl, pages, jwt, events, callbacks, session: { jwt: useJwtSession, maxAge: sessionMaxAge @@ -29,105 +26,101 @@ export default async function callback (req, res) { if (provider.type === 'oauth') { try { - const { profile, account, OAuthProfile } = await oAuthCallback(req) - try { - // Make it easier to debug when adding a new provider - logger.debug('OAUTH_CALLBACK_RESPONSE', { profile, account, OAuthProfile }) - - // If we don't have a profile object then either something went wrong - // or the user cancelled signin in. We don't know which, so we just - // direct the user to the signup page for now. We could do something - // else in future. - // - // Note: In oAuthCallback an error is logged with debug info, so it - // should at least be visible to developers what happened if it is an - // error with the provider. - if (!profile) { - return res.redirect(`${baseUrl}${basePath}/signin`) - } + const { profile, account, OAuthProfile } = await handleOAuthCallback(req) + + // Make it easier to debug when adding a new provider + logger.debug('OAUTH_CALLBACK_RESPONSE', { profile, account, OAuthProfile }) + + // If we don't have a profile object then either something went wrong + // or the user cancelled signin in. We don't know which, so we just + // direct the user to the signup page for now. We could do something + // else in future. + // + // Note: In oAuthCallback an error is logged with debug info, so it + // should at least be visible to developers what happened if it is an + // error with the provider. + if (!profile) { + return res.redirect(`${baseUrl}${basePath}/signin`) + } - // Check if user is allowed to sign in - // Attempt to get Profile from OAuth provider details before invoking - // signIn callback - but if no user object is returned, that is fine - // (that just means it's a new user signing in for the first time). - let userOrProfile = profile - if (adapter) { - const { getUserByProviderAccountId } = await adapter.getAdapter(req.options) - const userFromProviderAccountId = await getUserByProviderAccountId(account.provider, account.id) - if (userFromProviderAccountId) { - userOrProfile = userFromProviderAccountId - } + // Check if user is allowed to sign in + // Attempt to get Profile from OAuth provider details before invoking + // signIn callback - but if no user object is returned, that is fine + // (that just means it's a new user signing in for the first time). + let userOrProfile = profile + if (adapter) { + const { getUserByProviderAccountId } = await adapter.getAdapter(req.options) + const userFromProviderAccountId = await getUserByProviderAccountId(account.provider, account.id) + if (userFromProviderAccountId) { + userOrProfile = userFromProviderAccountId } + } - try { - const signInCallbackResponse = await callbacks.signIn(userOrProfile, account, OAuthProfile) - if (signInCallbackResponse === false) { - return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`) - } else if (typeof signInCallbackResponse === 'string') { - return res.redirect(signInCallbackResponse) - } - } catch (error) { - if (error instanceof Error) { - return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`) - } - // TODO: Remove in a future major release - logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT') - return res.redirect(error) + try { + const signInCallbackResponse = await callbacks.signIn(userOrProfile, account, OAuthProfile) + if (signInCallbackResponse === false) { + return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`) + } else if (typeof signInCallbackResponse === 'string') { + return res.redirect(signInCallbackResponse) } - - // Sign user in - const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, req.options) - - if (useJwtSession) { - const defaultJwtPayload = { - name: user.name, - email: user.email, - picture: user.image, - sub: user.id?.toString() - } - const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, OAuthProfile, isNewUser) - - // Sign and encrypt token - const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload }) - - // Set cookie expiry date - const cookieExpires = new Date() - cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000)) - - cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options }) - } else { - // Save Session Token in cookie - cookie.set(res, cookies.sessionToken.name, session.sessionToken, { expires: session.expires || null, ...cookies.sessionToken.options }) + } catch (error) { + if (error instanceof Error) { + return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`) } + // TODO: Remove in a future major release + logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT') + return res.redirect(error) + } - await dispatchEvent(events.signIn, { user, account, isNewUser }) + // Sign user in + const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, req.options) - // Handle first logins on new accounts - // e.g. option to send users to a new account landing page on initial login - // Note that the callback URL is preserved, so the journey can still be resumed - if (isNewUser && pages.newUser) { - return res.redirect(`${pages.newUser}${pages.newUser.includes('?') ? '&' : '?'}callbackUrl=${encodeURIComponent(callbackUrl)}`) + if (useJwtSession) { + const defaultJwtPayload = { + name: user.name, + email: user.email, + picture: user.image, + sub: user.id?.toString() } + const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, OAuthProfile, isNewUser) - // Callback URL is already verified at this point, so safe to use if specified - return res.redirect(callbackUrl || baseUrl) - } catch (error) { - if (error.name === 'AccountNotLinkedError') { - // If the email on the account is already linked, but not with this OAuth account - return res.redirect(`${baseUrl}${basePath}/error?error=OAuthAccountNotLinked`) - } else if (error.name === 'CreateUserError') { - return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCreateAccount`) - } - logger.error('OAUTH_CALLBACK_HANDLER_ERROR', error) - return res.redirect(`${baseUrl}${basePath}/error?error=Callback`) + // Sign and encrypt token + const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload }) + + // Set cookie expiry date + const cookieExpires = new Date() + cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000)) + + cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options }) + } else { + // Save Session Token in cookie + cookie.set(res, cookies.sessionToken.name, session.sessionToken, { expires: session.expires || null, ...cookies.sessionToken.options }) } + + await dispatchEvent(events.signIn, { user, account, isNewUser }) + + // Handle first logins on new accounts + // e.g. option to send users to a new account landing page on initial login + // Note that the callback URL is preserved, so the journey can still be resumed + if (isNewUser && pages.newUser) { + return res.redirect(`${pages.newUser}${pages.newUser.includes('?') ? '&' : '?'}callbackUrl=${encodeURIComponent(callbackUrl)}`) + } + + // Callback URL is already verified at this point, so safe to use if specified + return res.redirect(callbackUrl || baseUrl) } catch (error) { - if (error.name === 'OAuthCallbackError') { - logger.error('CALLBACK_OAUTH_ERROR', error) - return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCallback`) + switch (error.name) { + case 'OAuthCallbackHandlerError': + return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCallback`) + case 'AccountNotLinkedError': + // If the email on the account is already linked, but not with this OAuth account + return res.redirect(`${baseUrl}${basePath}/error?error=OAuthAccountNotLinked`) + case 'CreateUserError': + return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCreateAccount`) + default: + logger.error('CALLBACK_OAUTH_ERROR', error) + return res.redirect(`${baseUrl}${basePath}/error?error=Callback`) } - logger.error('OAUTH_CALLBACK_ERROR', error) - return res.redirect(`${baseUrl}${basePath}/error?error=Callback`) } } else if (provider.type === 'email') { try {