From 656bae7875e03c45497fb3dd340f04e89cab0a20 Mon Sep 17 00:00:00 2001 From: David Luecke Date: Sun, 21 Apr 2019 11:05:55 -0700 Subject: [PATCH] feat: @feathersjs/authentication-oauth (#1299) --- .gitignore | 1 + package-lock.json | 223 ++++++++++++------ .../test/integration/fixture.ts | 1 + packages/authentication-local/test/fixture.js | 1 + packages/authentication-oauth/.npmignore | 3 + packages/authentication-oauth/CHANGELOG.md | 4 + packages/authentication-oauth/LICENSE | 21 ++ packages/authentication-oauth/README.md | 13 + packages/authentication-oauth/package.json | 64 +++++ packages/authentication-oauth/src/express.ts | 107 +++++++++ packages/authentication-oauth/src/index.ts | 57 +++++ packages/authentication-oauth/src/strategy.ts | 120 ++++++++++ packages/authentication-oauth/src/utils.ts | 40 ++++ .../authentication-oauth/test/express.test.ts | 50 ++++ packages/authentication-oauth/test/fixture.ts | 55 +++++ .../authentication-oauth/test/index.test.ts | 29 +++ .../test/strategy.test.ts | 87 +++++++ .../authentication-oauth/test/utils.test.ts | 39 +++ packages/authentication-oauth/tsconfig.json | 9 + packages/authentication/src/core.ts | 6 +- packages/authentication/src/options.ts | 2 +- packages/authentication/src/service.ts | 14 +- packages/authentication/test/core.test.ts | 20 +- packages/authentication/test/service.test.ts | 16 ++ packages/express/index.d.ts | 4 +- packages/express/lib/authentication.js | 10 + tslint.json | 1 + 27 files changed, 901 insertions(+), 96 deletions(-) create mode 100644 packages/authentication-oauth/.npmignore create mode 100644 packages/authentication-oauth/CHANGELOG.md create mode 100644 packages/authentication-oauth/LICENSE create mode 100644 packages/authentication-oauth/README.md create mode 100644 packages/authentication-oauth/package.json create mode 100644 packages/authentication-oauth/src/express.ts create mode 100644 packages/authentication-oauth/src/index.ts create mode 100644 packages/authentication-oauth/src/strategy.ts create mode 100644 packages/authentication-oauth/src/utils.ts create mode 100644 packages/authentication-oauth/test/express.test.ts create mode 100644 packages/authentication-oauth/test/fixture.ts create mode 100644 packages/authentication-oauth/test/index.test.ts create mode 100644 packages/authentication-oauth/test/strategy.test.ts create mode 100644 packages/authentication-oauth/test/utils.test.ts create mode 100644 packages/authentication-oauth/tsconfig.json diff --git a/.gitignore b/.gitignore index aab8bd5821..1a04bd9748 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ dist/ packages/authentication/lib packages/authentication-local/lib packages/authentication-client/lib +packages/authentication-oauth/lib packages/configuration/lib packages/commons/lib packages/transport-commons/lib diff --git a/package-lock.json b/package-lock.json index 021f91ead1..76f008e1b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -189,16 +189,16 @@ } }, "@lerna/changed": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@lerna/changed/-/changed-3.13.1.tgz", - "integrity": "sha512-BRXitEJGOkoudbxEewW7WhjkLxFD+tTk4PrYpHLyCBk63pNTWtQLRE6dc1hqwh4emwyGncoyW6RgXfLgMZgryw==", + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/@lerna/changed/-/changed-3.13.2.tgz", + "integrity": "sha512-mcmkxUMR0J4ZyRyVUrdDJl4ZsdHDgdA1xQcbdB4LZvAE/E2lNlPcEfAfbfs08VnRiqvFOqcczbzBq10hvSFg4w==", "dev": true, "requires": { "@lerna/collect-updates": "3.13.0", "@lerna/command": "3.13.1", "@lerna/listable": "3.13.0", "@lerna/output": "3.13.0", - "@lerna/version": "3.13.1" + "@lerna/version": "3.13.2" } }, "@lerna/check-working-tree": { @@ -605,15 +605,16 @@ } }, "@lerna/npm-publish": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/npm-publish/-/npm-publish-3.13.0.tgz", - "integrity": "sha512-y4WO0XTaf9gNRkI7as6P2ItVDOxmYHwYto357fjybcnfXgMqEA94c3GJ++jU41j0A9vnmYC6/XxpTd9sVmH9tA==", + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/@lerna/npm-publish/-/npm-publish-3.13.2.tgz", + "integrity": "sha512-HMucPyEYZfom5tRJL4GsKBRi47yvSS2ynMXYxL3kO0ie+j9J7cb0Ir8NmaAMEd3uJWJVFCPuQarehyfTDZsSxg==", "dev": true, "requires": { "@lerna/run-lifecycle": "3.13.0", "figgy-pudding": "^3.5.1", "fs-extra": "^7.0.0", "libnpmpublish": "^1.1.1", + "npm-package-arg": "^6.1.0", "npmlog": "^4.1.2", "pify": "^3.0.0", "read-package-json": "^2.0.13" @@ -805,9 +806,9 @@ } }, "@lerna/publish": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@lerna/publish/-/publish-3.13.1.tgz", - "integrity": "sha512-KhCJ9UDx76HWCF03i5TD7z5lX+2yklHh5SyO8eDaLptgdLDQ0Z78lfGj3JhewHU2l46FztmqxL/ss0IkWHDL+g==", + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/@lerna/publish/-/publish-3.13.2.tgz", + "integrity": "sha512-L8iceC3Z2YJnlV3cGbfk47NSh1+iOo1tD65z+BU3IYLRpPnnSf8i6BORdKV8rECDj6kjLYvL7//2yxbHy7shhA==", "dev": true, "requires": { "@lerna/batch-packages": "3.13.0", @@ -819,7 +820,7 @@ "@lerna/log-packed": "3.13.0", "@lerna/npm-conf": "3.13.0", "@lerna/npm-dist-tag": "3.13.0", - "@lerna/npm-publish": "3.13.0", + "@lerna/npm-publish": "3.13.2", "@lerna/output": "3.13.0", "@lerna/pack-directory": "3.13.1", "@lerna/prompt": "3.13.0", @@ -827,7 +828,7 @@ "@lerna/run-lifecycle": "3.13.0", "@lerna/run-parallel-batches": "3.13.0", "@lerna/validation-error": "3.13.0", - "@lerna/version": "3.13.1", + "@lerna/version": "3.13.2", "figgy-pudding": "^3.5.1", "fs-extra": "^7.0.0", "libnpmaccess": "^3.0.1", @@ -964,9 +965,9 @@ } }, "@lerna/version": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@lerna/version/-/version-3.13.1.tgz", - "integrity": "sha512-WpfKc5jZBBOJ6bFS4atPJEbHSiywQ/Gcd+vrwaEGyQHWHQZnPTvhqLuq3q9fIb9sbuhH5pSY6eehhuBrKqTnjg==", + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/@lerna/version/-/version-3.13.2.tgz", + "integrity": "sha512-85AEn6Cx5p1VOejEd5fpTyeDCx6yejSJCgbILkx+gXhLhFg2XpFzLswMd+u71X7RAttWHvhzeKJAw4tWTXDvpQ==", "dev": true, "requires": { "@lerna/batch-packages": "3.13.0", @@ -1020,9 +1021,9 @@ "dev": true }, "@octokit/endpoint": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-3.2.3.tgz", - "integrity": "sha512-yUPCt4vMIOclox13CUxzuKiPJIFo46b/6GhUnUTw5QySczN1L0DtSxgmIZrZV4SAb9EyAqrceoyrWoYVnfF2AA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-4.0.0.tgz", + "integrity": "sha512-b8sptNUekjREtCTJFpOfSIL4SKh65WaakcyxWzRcSPOk5RxkZJ/S8884NGZFxZ+jCB2rDURU66pSHn14cVgWVg==", "dev": true, "requires": { "deepmerge": "3.2.0", @@ -1038,12 +1039,12 @@ "dev": true }, "@octokit/request": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-2.4.2.tgz", - "integrity": "sha512-lxVlYYvwGbKSHXfbPk5vxEA8w4zHOH1wobado4a9EfsyD3Cbhuhus1w0Ye9Ro0eMubGO8kNy5d+xNFisM3Tvaw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-3.0.0.tgz", + "integrity": "sha512-DZqmbm66tq+a9FtcKrn0sjrUpi0UaZ9QPUCxxyk/4CJ2rseTMpAWRf6gCwOSUCzZcx/4XVIsDk+kz5BVdaeenA==", "dev": true, "requires": { - "@octokit/endpoint": "^3.2.0", + "@octokit/endpoint": "^4.0.0", "deprecation": "^1.0.1", "is-plain-object": "^2.0.4", "node-fetch": "^2.3.0", @@ -1052,12 +1053,12 @@ } }, "@octokit/rest": { - "version": "16.23.2", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.23.2.tgz", - "integrity": "sha512-ZxiZMaCuqBG/IsbgNRVfGwYsvBb5DjHuMGjJgOrinT+/b+1j1U7PiGyRkHDJdjTGA6N/PsMC2lP2ZybX9579iA==", + "version": "16.24.1", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.24.1.tgz", + "integrity": "sha512-V2GVL+cfuwNTcZ9qtBMOR9pIftWo1AiZIiGvWNmTcIQG5mkj83ZXC+g3w5g0cVXt7Hi+mSOrD2bZ7HJOuouUNg==", "dev": true, "requires": { - "@octokit/request": "2.4.2", + "@octokit/request": "3.0.0", "atob-lite": "^2.0.0", "before-after-hook": "^1.4.0", "btoa-lite": "^1.0.0", @@ -1861,9 +1862,9 @@ } }, "commander": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", - "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", "dev": true }, "commondir": { @@ -1894,9 +1895,9 @@ } }, "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", "dev": true }, "concat-map": { @@ -2060,9 +2061,9 @@ } }, "conventional-changelog-preset-loader": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.0.2.tgz", - "integrity": "sha512-pBY+qnUoJPXAXXqVGwQaVmcye05xi6z231QM98wHWamGAmu/ghkBprQAwmF5bdmyobdVxiLhPY3PrCfSeUNzRQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.1.1.tgz", + "integrity": "sha512-K4avzGMLm5Xw0Ek/6eE3vdOXkqnpf9ydb68XYmCc16cJ99XMMbc2oaNMuPwAsxVK6CC1yA4/I90EhmWNj0Q6HA==", "dev": true }, "conventional-changelog-writer": { @@ -2401,15 +2402,15 @@ } }, "conventional-recommended-bump": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-4.0.4.tgz", - "integrity": "sha512-9mY5Yoblq+ZMqJpBzgS+RpSq+SUfP2miOR3H/NR9drGf08WCrY9B6HAGJZEm6+ThsVP917VHAahSOjM6k1vhPg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-4.1.1.tgz", + "integrity": "sha512-JT2vKfSP9kR18RXXf55BRY1O3AHG8FPg5btP3l7LYfcWJsiXI6MCf30DepQ98E8Qhowvgv7a8iev0J1bEDkTFA==", "dev": true, "requires": { - "concat-stream": "^1.6.0", - "conventional-changelog-preset-loader": "^2.0.2", - "conventional-commits-filter": "^2.0.1", - "conventional-commits-parser": "^3.0.1", + "concat-stream": "^2.0.0", + "conventional-changelog-preset-loader": "^2.1.1", + "conventional-commits-filter": "^2.0.2", + "conventional-commits-parser": "^3.0.2", "git-raw-commits": "2.0.0", "git-semver-tags": "^2.0.2", "meow": "^4.0.0", @@ -2433,6 +2434,43 @@ "quick-lru": "^1.0.0" } }, + "concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "conventional-commits-filter": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.2.tgz", + "integrity": "sha512-WpGKsMeXfs21m1zIw4s9H5sys2+9JccTzpN6toXtxhpw2VNF2JUXwIakthKBy+LN4DvJm+TzWhxOMWOs1OFCFQ==", + "dev": true, + "requires": { + "lodash.ismatch": "^4.4.0", + "modify-values": "^1.0.0" + } + }, + "conventional-commits-parser": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.0.2.tgz", + "integrity": "sha512-y5eqgaKR0F6xsBNVSQ/5cI5qIF3MojddSUi1vKIggRkqUTbkqFKH9P5YX/AT1BVZp9DtSzBTIkvjyVLotLsVog==", + "dev": true, + "requires": { + "JSONStream": "^1.0.4", + "is-text-path": "^1.0.0", + "lodash": "^4.2.1", + "meow": "^4.0.0", + "split2": "^2.0.0", + "through2": "^3.0.0", + "trim-off-newlines": "^1.0.0" + } + }, "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", @@ -2535,6 +2573,17 @@ "read-pkg": "^3.0.0" } }, + "readable-stream": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.3.0.tgz", + "integrity": "sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, "redent": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", @@ -2557,6 +2606,15 @@ "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", "dev": true }, + "through2": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", + "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", + "dev": true, + "requires": { + "readable-stream": "2 || 3" + } + }, "trim-newlines": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", @@ -2711,15 +2769,6 @@ "meow": "^3.3.0" } }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, "debuglog": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", @@ -3057,6 +3106,15 @@ "to-regex": "^3.0.1" }, "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", @@ -4244,9 +4302,9 @@ } }, "handlebars": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.1.tgz", - "integrity": "sha512-3Zhi6C0euYZL5sM0Zcy7lInLXKQ+YLcF/olbN010mzGQ4XVm50JeyBnMqofHh696GrciGruC7kCcApPDJvVgwA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", + "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -4564,9 +4622,9 @@ } }, "inquirer": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.2.tgz", - "integrity": "sha512-Z2rREiXA6cHRR9KBOarR3WuLlFzlIfAEIiB45ll5SSadMg7WqOh1MKEjjndfuH5ewXdixWCxqnVfGOQzPeiztA==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.3.1.tgz", + "integrity": "sha512-MmL624rfkFt4TG9y/Jvmt8vdmOo836U7Y0Hxr2aFk3RelZEGX4Igk0KabWrcaaZaTv9uzglOqWh1Vly+FAWAXA==", "dev": true, "requires": { "ansi-escapes": "^3.2.0", @@ -4580,7 +4638,7 @@ "run-async": "^2.2.0", "rxjs": "^6.4.0", "string-width": "^2.1.0", - "strip-ansi": "^5.0.0", + "strip-ansi": "^5.1.0", "through": "^2.3.6" }, "dependencies": { @@ -5158,14 +5216,14 @@ } }, "lerna": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/lerna/-/lerna-3.13.1.tgz", - "integrity": "sha512-7kSz8LLozVsoUNTJzJzy+b8TnV9YdviR2Ee2PwGZSlVw3T1Rn7kOAPZjEi+3IWnOPC96zMPHVmjCmzQ4uubalw==", + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/lerna/-/lerna-3.13.2.tgz", + "integrity": "sha512-2iliiFVAMNqaKsVSJ90p49dur93d5RlktotAJNp+uuHsCuIIAvwceqmSgDQCmWu4GkgAom+5uy//KV6F9t8fLA==", "dev": true, "requires": { "@lerna/add": "3.13.1", "@lerna/bootstrap": "3.13.1", - "@lerna/changed": "3.13.1", + "@lerna/changed": "3.13.2", "@lerna/clean": "3.13.1", "@lerna/cli": "3.13.0", "@lerna/create": "3.13.1", @@ -5175,9 +5233,9 @@ "@lerna/init": "3.13.1", "@lerna/link": "3.13.1", "@lerna/list": "3.13.1", - "@lerna/publish": "3.13.1", + "@lerna/publish": "3.13.2", "@lerna/run": "3.13.1", - "@lerna/version": "3.13.1", + "@lerna/version": "3.13.2", "import-local": "^1.0.0", "npmlog": "^4.1.2" } @@ -5318,6 +5376,12 @@ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", "dev": true }, + "lodash.ismatch": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", + "integrity": "sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=", + "dev": true + }, "lodash.set": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", @@ -6750,12 +6814,6 @@ "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=", "dev": true }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - }, "quick-lru": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", @@ -6955,6 +7013,14 @@ "tough-cookie": "~2.4.3", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" + }, + "dependencies": { + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + } } }, "requestretry": { @@ -7261,6 +7327,15 @@ "use": "^3.1.0" }, "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", @@ -8005,13 +8080,13 @@ "dev": true }, "uglify-js": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.3.tgz", - "integrity": "sha512-rIQPT2UMDnk4jRX+w4WO84/pebU2jiLsjgIyrCktYgSvx28enOE3iYQMr+BD1rHiitWnDmpu0cY/LfIEpKcjcw==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.4.tgz", + "integrity": "sha512-GpKo28q/7Bm5BcX9vOu4S46FwisbPbAmkkqPnGIpKvKTM96I85N6XHQV+k4I6FA2wxgLhcsSyHoNhzucwCflvA==", "dev": true, "optional": true, "requires": { - "commander": "~2.19.0", + "commander": "~2.20.0", "source-map": "~0.6.1" }, "dependencies": { diff --git a/packages/authentication-client/test/integration/fixture.ts b/packages/authentication-client/test/integration/fixture.ts index acb2ca1371..5ac106f26b 100644 --- a/packages/authentication-client/test/integration/fixture.ts +++ b/packages/authentication-client/test/integration/fixture.ts @@ -14,6 +14,7 @@ export default (app: Application) => { entity: 'user', service: 'users', secret: 'supersecret', + httpStrategies: [ 'jwt' ], jwtStrategies: [ 'local', 'jwt' ], local: { usernameField: 'email', diff --git a/packages/authentication-local/test/fixture.js b/packages/authentication-local/test/fixture.js index 24e9e2c606..bff7576347 100644 --- a/packages/authentication-local/test/fixture.js +++ b/packages/authentication-local/test/fixture.js @@ -13,6 +13,7 @@ module.exports = (app = feathers()) => { service: 'users', secret: 'supersecret', jwtStrategies: [ 'local', 'jwt' ], + httpStrategies: [ 'jwt' ], local: { usernameField: 'email', passwordField: 'password' diff --git a/packages/authentication-oauth/.npmignore b/packages/authentication-oauth/.npmignore new file mode 100644 index 0000000000..d7819044b6 --- /dev/null +++ b/packages/authentication-oauth/.npmignore @@ -0,0 +1,3 @@ +test/ +src/ +tsconfig.json diff --git a/packages/authentication-oauth/CHANGELOG.md b/packages/authentication-oauth/CHANGELOG.md new file mode 100644 index 0000000000..e9fb6ecf59 --- /dev/null +++ b/packages/authentication-oauth/CHANGELOG.md @@ -0,0 +1,4 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. \ No newline at end of file diff --git a/packages/authentication-oauth/LICENSE b/packages/authentication-oauth/LICENSE new file mode 100644 index 0000000000..25f2251eae --- /dev/null +++ b/packages/authentication-oauth/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Feathers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/authentication-oauth/README.md b/packages/authentication-oauth/README.md new file mode 100644 index 0000000000..991e9d22f1 --- /dev/null +++ b/packages/authentication-oauth/README.md @@ -0,0 +1,13 @@ +# @feathersjs/authentication-oauth + +[![Build Status](https://travis-ci.org/feathersjs/feathers.png?branch=master)](https://travis-ci.org/feathersjs/feathers) +[![Dependency Status](https://img.shields.io/david/feathersjs/feathers.svg?style=flat-square&path=packages/authentication-oauth)](https://david-dm.org/feathersjs/feathers?path=packages/authentication-oauth) +[![Download Status](https://img.shields.io/npm/dm/@feathersjs/authentication-oauth.svg?style=flat-square)](https://www.npmjs.com/package/@feathersjs/authentication-oauth) + +> OAuth 1 and 2 authentication for Feathers. Powered by Grant. + +## Installation + +``` +npm install @feathersjs/authentication-oauth --save +``` diff --git a/packages/authentication-oauth/package.json b/packages/authentication-oauth/package.json new file mode 100644 index 0000000000..b230087422 --- /dev/null +++ b/packages/authentication-oauth/package.json @@ -0,0 +1,64 @@ +{ + "name": "@feathersjs/authentication-oauth", + "description": "oAuth 1 and 2 authentication for Feathers. Powered by Grant.", + "version": "0.0.1", + "homepage": "https://feathersjs.com", + "main": "lib/", + "keywords": [ + "feathers", + "feathers-plugin" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git://github.com/feathersjs/feathers.git" + }, + "author": { + "name": "Feathers contributors", + "email": "hello@feathersjs.com", + "url": "https://feathersjs.com" + }, + "contributors": [], + "bugs": { + "url": "https://github.com/feathersjs/feathers/issues" + }, + "engines": { + "node": ">= 6" + }, + "scripts": { + "start": "ts-node test/app", + "prepublish": "npm run compile", + "compile": "shx rm -rf lib/ && tsc", + "test": "mocha --opts ../../mocha.ts.opts --recursive test/**.test.ts test/**/*.test.ts" + }, + "directories": { + "lib": "lib" + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@feathersjs/authentication": "^2.1.16", + "@feathersjs/errors": "^3.3.6", + "@feathersjs/express": "^1.3.1", + "debug": "^4.1.1", + "express-session": "^1.15.6", + "grant": "^4.5.0", + "grant-profile": "^0.0.3", + "lodash": "^4.17.11" + }, + "devDependencies": { + "@feathersjs/feathers": "^3.3.1", + "@types/debug": "^4.1.3", + "@types/express": "^4.16.1", + "@types/express-session": "^1.15.12", + "@types/lodash": "^4.14.123", + "@types/mocha": "^5.2.6", + "@types/node": "^11.13.0", + "axios": "^0.18.0", + "mocha": "^6.0.2", + "shx": "^0.3.2", + "ts-node": "^8.0.3", + "typescript": "^3.4.2" + } +} diff --git a/packages/authentication-oauth/src/express.ts b/packages/authentication-oauth/src/express.ts new file mode 100644 index 0000000000..a59bd69ef7 --- /dev/null +++ b/packages/authentication-oauth/src/express.ts @@ -0,0 +1,107 @@ +// @ts-ignore +import { express as grantExpress } from 'grant'; +import Debug from 'debug'; +import session from 'express-session'; +import querystring from 'querystring'; +import { Application } from '@feathersjs/feathers'; +import { AuthenticationService, AuthenticationResult } from '@feathersjs/authentication'; +import { + Application as ExpressApplication, + original as express +} from '@feathersjs/express'; +import { OauthSetupSettings } from './utils'; + +const grant = grantExpress(); +const debug = Debug('@feathersjs/authentication-oauth/express'); + +export default (options: OauthSetupSettings) => { + return (feathersApp: Application) => { + const { path, authService, linkStrategy } = options; + const app = feathersApp as ExpressApplication; + const config = app.get('grant'); + const secret = Math.random().toString(36).substring(7); + + if (!config) { + debug('No grant configuration found, skipping Express oAuth setup'); + return; + } + + const grantApp = grant(config); + const authApp = express(); + + authApp.use(session({ + secret, + resave: true, + saveUninitialized: true + })); + + authApp.get('/:name', (req, res) => { + const { name } = req.params; + const { feathers_token, ...query } = req.query; + const qs = querystring.stringify(query); + + if (feathers_token) { + debug(`Got feathers_token query parameter to link accounts`, feathers_token); + req.session.accessToken = feathers_token; + } + + const redirect = `${path}/connect/${name}${qs.length ? '?' + qs : ''}`; + + debug(`Starting ${name} oAuth flow, redirecting to ${redirect}`); + + res.redirect(redirect); + }); + + authApp.get('/:name/authenticate', async (req, res, next) => { + const { name } = req.params; + const { accessToken, grant } = req.session; + const service: AuthenticationService = app.service(authService); + const sendResponse = async (data: AuthenticationResult|Error) => { + const redirect = await options.getRedirect(service, data); + + if (redirect !== null) { + res.redirect(redirect); + } else if (data instanceof Error) { + next(data); + } else { + res.json(data); + } + }; + + try { + const payload = config.defaults.transport === 'session' ? + grant.response : req.query; + + const params = { + provider: 'rest', + jwtStrategies: [ name ], + authentication: accessToken ? { + strategy: linkStrategy, + accessToken + } : null + }; + + const authentication = { + strategy: name, + ...payload + }; + + debug(`Calling ${authService}.create authentication with strategy ${name}`); + + const authResult = await service.create(authentication, params); + + debug('Successful oAuth authentication, sending response'); + + await sendResponse(authResult); + } catch (error) { + debug('Received oAuth authentication error', error); + sendResponse(error); + } + }); + + authApp.use(grantApp); + + app.set('grant', grantApp.config); + app.use(path, authApp); + }; +}; diff --git a/packages/authentication-oauth/src/index.ts b/packages/authentication-oauth/src/index.ts new file mode 100644 index 0000000000..0e928c4ab7 --- /dev/null +++ b/packages/authentication-oauth/src/index.ts @@ -0,0 +1,57 @@ +import Debug from 'debug'; +import { merge, each, omit } from 'lodash'; +import { Application } from '@feathersjs/feathers'; +import { AuthenticationService } from '@feathersjs/authentication'; +import { OAuthStrategy, OAuthProfile } from './strategy'; +import { default as setupExpress } from './express'; +import { OauthSetupSettings, getDefaultSettings } from './utils'; + +const debug = Debug('@feathersjs/authentication-oauth'); + +export { OauthSetupSettings, OAuthStrategy, OAuthProfile }; + +export const setup = (options: OauthSetupSettings) => (app: Application) => { + const path = options.authService; + const service: AuthenticationService = app.service(path); + + if (!service) { + throw new Error(`'${path}' authentication service must exist before registering @feathersjs/authentication-oauth`); + } + + const { oauth } = service.configuration; + + if (!oauth) { + debug(`No oauth configuration found at '${path}'. Skipping oAuth setup.`); + return; + } + + const { strategyNames } = service; + const grant = merge({ + defaults: { + host: `${app.get('host')}:${app.get('port')}`, + path: '/auth', + protocol: app.get('env') === 'production' ? 'https' : 'http', + transport: 'session' + } + }, omit(oauth, 'redirect')); + + each(grant, (value, key) => { + if (key !== 'defaults') { + value.callback = value.callback || `/auth/${key}/authenticate`; + + if (!strategyNames.includes(key)) { + debug(`Registering oAuth default strategy for '${key}'`); + service.register(key, new OAuthStrategy()); + } + } + }); + + app.set('grant', grant); +}; + +export const express = (settings: OauthSetupSettings = {}) => (app: Application) => { + const options = getDefaultSettings(app, settings); + + app.configure(setup(options)); + app.configure(setupExpress(options)); +}; diff --git a/packages/authentication-oauth/src/strategy.ts b/packages/authentication-oauth/src/strategy.ts new file mode 100644 index 0000000000..681d5c7926 --- /dev/null +++ b/packages/authentication-oauth/src/strategy.ts @@ -0,0 +1,120 @@ +// @ts-ignore +import getProfile from 'grant-profile/lib/client'; +import Debug from 'debug'; +import { + AuthenticationRequest, AuthenticationBaseStrategy +} from '@feathersjs/authentication'; +import { Params } from '@feathersjs/feathers'; +import { NotAuthenticated } from '@feathersjs/errors'; + +const debug = Debug('@feathersjs/authentication-oauth/strategy'); + +export interface OAuthProfile { + id?: string|number; + [key: string]: any; +} + +export class OAuthStrategy extends AuthenticationBaseStrategy { + get configuration () { + const { entity, service, entityId } = this.authentication.configuration; + + return { + entity, + service, + entityId, + ...super.configuration + }; + } + + get entityId (): string { + return this.configuration.entityId || this.entityService.id; + } + + /* istanbul ignore next */ + async getProfile (data: AuthenticationRequest, _params: Params) { + const config = this.app.get('grant'); + const provider = config[data.strategy]; + + debug('getProfile of oAuth profile from grant-profile with', data); + + return getProfile(provider, data); + } + + async getCurrentEntity (params: Params) { + const { authentication } = params; + const { entity } = this.configuration; + + if (authentication && authentication.strategy) { + debug('getCurrentEntity with authentication', authentication); + + const { strategy } = authentication; + const authResult = await this.authentication + .authenticate(authentication, params, strategy); + + return authResult[entity] || null; + } + + return null; + } + + async findEntity (profile: OAuthProfile, params: Params) { + const query = { + [`${this.name}Id`]: profile.id + }; + + debug('findEntity with query', query); + + const result = await this.entityService.find({ + ...params, + query + }); + const [ entity = null ] = result.data ? result.data : result; + + debug('findEntity returning', entity); + + return entity; + } + + async createEntity (profile: OAuthProfile, params: Params) { + const data = { + [`${this.name}Id`]: profile.id + }; + + debug('createEntity with data', data); + + return this.entityService.create(data, params); + } + + async updateEntity (entity: any, profile: OAuthProfile, params: Params) { + const id = entity[this.entityId]; + const data = { + [`${this.name}Id`]: profile.id + }; + + debug(`updateEntity with id ${id} and data`, data); + + return this.entityService.patch(id, data, params); + } + + async authenticate (authentication: AuthenticationRequest, params: Params) { + if (authentication.strategy !== this.name) { + throw new NotAuthenticated('Not authenticated'); + } + + const entity: string = this.configuration.entity; + const profile = await this.getProfile(authentication, params); + const existingEntity = await this.findEntity(profile, params) + || await this.getCurrentEntity(params); + + debug(`authenticate with (existing) entity`, existingEntity); + + const authEntity = existingEntity === null + ? await this.createEntity(profile, params) + : await this.updateEntity(existingEntity, profile, params); + + return { + authentication: { strategy: this.name }, + [entity]: authEntity + }; + } +} diff --git a/packages/authentication-oauth/src/utils.ts b/packages/authentication-oauth/src/utils.ts new file mode 100644 index 0000000000..04bda995d0 --- /dev/null +++ b/packages/authentication-oauth/src/utils.ts @@ -0,0 +1,40 @@ +import querystring from 'querystring'; +import { Application } from '@feathersjs/feathers'; +import { AuthenticationService, AuthenticationResult } from '@feathersjs/authentication'; + +export interface OauthSetupSettings { + path?: string; + authService?: string; + linkStrategy?: string; + getRedirect? (service: AuthenticationService, data: AuthenticationResult|Error): Promise; +} + +export const getRedirect = async (service: AuthenticationService, data: AuthenticationResult|Error) => { + const { redirect } = service.configuration.oauth; + + if (!redirect) { + return null; + } + + const separator = redirect.endsWith('?') ? '' : '#'; + const authResult: AuthenticationResult = data; + const query = authResult.accessToken ? { + access_token: authResult.accessToken + } : { + error: data.message || 'OAuth Authentication not successful' + }; + + return redirect + separator + querystring.stringify(query); +}; + +export const getDefaultSettings = (app: Application, other?: OauthSetupSettings) => { + const defaults: OauthSetupSettings = { + path: '/auth', + authService: app.get('defaultAuthentication'), + linkStrategy: 'jwt', + getRedirect, + ...other + }; + + return defaults; +}; diff --git a/packages/authentication-oauth/test/express.test.ts b/packages/authentication-oauth/test/express.test.ts new file mode 100644 index 0000000000..afb03682ab --- /dev/null +++ b/packages/authentication-oauth/test/express.test.ts @@ -0,0 +1,50 @@ +import { strict as assert } from 'assert'; +import { Server } from 'http'; +import axios from 'axios'; +import { app } from './fixture'; + +describe('@feathersjs/authentication-oauth/express', () => { + let server: Server; + + before(async () => { + server = app.listen(9876); + + await new Promise(resolve => server.once('listening', () => resolve())); + }); + + after(() => server.close()); + + it('auth/test', async () => { + axios.get('http://localhost:9876/auth/test?feathers_token=testing'); + }); + + it('auth/test with query', async () => { + axios.get('http://localhost:9876/auth/test?other=test'); + }); + + it('auth/test/authenticate', async () => { + const { data } = await axios.get('http://localhost:9876/auth/test/authenticate?id=expressTest'); + + assert.ok(data.accessToken); + assert.equal(data.user.testId, 'expressTest'); + }); + + it('auth/test/authenticate with redirect', async () => { + app.get('authentication').oauth.redirect = '/'; + + try { + await axios.get('http://localhost:9876/auth/test/authenticate'); + } catch (error) { + assert.ok(/Cannot GET/.test(error.response.data)); + delete app.get('authentication').oauth.redirect; + } + }); + + it('auth/test/authenticate with error', async () => { + try { + await axios.get('http://localhost:9876/auth/test/authenticate'); + } catch (error) { + assert.equal(error.response.data.message, 'Data needs an id'); + } + }); +}); diff --git a/packages/authentication-oauth/test/fixture.ts b/packages/authentication-oauth/test/fixture.ts new file mode 100644 index 0000000000..bffc920c85 --- /dev/null +++ b/packages/authentication-oauth/test/fixture.ts @@ -0,0 +1,55 @@ +import feathers, { Params } from '@feathersjs/feathers'; +import express, { rest, errorHandler } from '@feathersjs/express'; +import { AuthenticationService, JWTStrategy, AuthenticationRequest } from '@feathersjs/authentication'; +import { express as oauth, OAuthStrategy } from '../src'; + +// @ts-ignore +import memory from 'feathers-memory'; + +export class TestOAuthStrategy extends OAuthStrategy { + async getProfile (data: AuthenticationRequest, _params: Params) { + if (!data.id) { + throw new Error('Data needs an id'); + } + + return data; + } +} + +const port = 3000; +const app = express(feathers()); +const auth = new AuthenticationService(app); + +auth.register('jwt', new JWTStrategy()); +auth.register('test', new TestOAuthStrategy()); + +app.configure(rest()); +app.set('host', '127.0.0.1'); +app.set('port', port); +app.set('authentication', { + secret: 'supersecret', + entity: 'user', + service: 'users', + httpStrategies: [ 'jwt' ], + oauth: { + defaults: { + transport: 'query' + }, + test: { + key: 'some-key', + secret: 'a secret secret' + }, + twitter: { + key: 'twitter key', + secret: 'some secret' + } + } +}); + +app.use('/authentication', auth); +app.use('/users', memory()); + +app.configure(oauth()); +app.use(errorHandler({ logger: null })); + +export { app }; diff --git a/packages/authentication-oauth/test/index.test.ts b/packages/authentication-oauth/test/index.test.ts new file mode 100644 index 0000000000..e03b284d43 --- /dev/null +++ b/packages/authentication-oauth/test/index.test.ts @@ -0,0 +1,29 @@ +import { strict as assert } from 'assert'; +import feathers from '@feathersjs/feathers'; +import { setup, express } from '../src'; +import { AuthenticationService } from '@feathersjs/authentication/lib'; + +describe('@feathersjs/authentication-oauth', () => { + describe('setup', () => { + it('errors when service does not exist', () => { + const app = feathers(); + + try { + app.configure(setup({ authService: 'something' })); + assert.fail('Should never get here'); + } catch (error) { + assert.equal(error.message, + `'something' authentication service must exist before registering @feathersjs/authentication-oauth` + ); + } + }); + + it('errors when service does not exist', () => { + const app = feathers(); + + app.use('/authentication', new AuthenticationService(app)); + + app.configure(express()); + }); + }); +}); diff --git a/packages/authentication-oauth/test/strategy.test.ts b/packages/authentication-oauth/test/strategy.test.ts new file mode 100644 index 0000000000..23e0e1a59d --- /dev/null +++ b/packages/authentication-oauth/test/strategy.test.ts @@ -0,0 +1,87 @@ +import { strict as assert } from 'assert'; +import { app, TestOAuthStrategy } from './fixture'; +import { AuthenticationService } from '@feathersjs/authentication/lib'; + +describe('@feathersjs/authentication-oauth/strategy', () => { + const authService: AuthenticationService = app.service('authentication'); + const [strategy] = authService.getStrategies('test') as TestOAuthStrategy[]; + + it('initializes, has .entityId and configuration', () => { + assert.ok(strategy); + assert.strictEqual(strategy.entityId, 'id'); + assert.ok(strategy.configuration.entity); + }); + + it('getProfile', async () => { + const data = { id: 'getProfileTest' }; + const profile = await strategy.getProfile(data, {}); + + assert.deepEqual(profile, data); + }); + + describe('authenticate', () => { + it('errors when strategy is not set', async () => { + try { + await strategy.authenticate({ + id: 'newEntity' + }, {}); + assert.fail('Should never get here'); + } catch (error) { + assert.equal(error.name, 'NotAuthenticated'); + assert.equal(error.message, 'Not authenticated'); + } + }); + + it('with new user', async () => { + const authResult = await strategy.authenticate({ + strategy: 'test', + id: 'newEntity' + }, {}); + + assert.deepEqual(authResult, { + authentication: { strategy: 'test' }, + user: { testId: 'newEntity', id: authResult.user.id } + }); + }); + + it('with existing user and already linked strategy', async () => { + const existingUser = await app.service('users').create({ + testId: 'existingEntity', + name: 'David' + }); + const authResult = await strategy.authenticate({ + strategy: 'test', + id: 'existingEntity' + }, {}); + + assert.deepEqual(authResult, { + authentication: { strategy: 'test' }, + user: existingUser + }); + }); + + it('links user with existing authentication', async () => { + const user = await app.service('users').create({ + name: 'David' + }); + const jwt = await authService.createJWT({}, { + subject: `${user.id}` + }); + + const authResult = await strategy.authenticate({ + strategy: 'test', + id: 'linkedEntity' + }, { + authentication: { + strategy: 'jwt', + accessToken: jwt + } + }); + + assert.deepEqual(authResult, { + authentication: { strategy: 'test' }, + user: { id: user.id, name: user.name, testId: 'linkedEntity' } + }); + }); + }); +}); diff --git a/packages/authentication-oauth/test/utils.test.ts b/packages/authentication-oauth/test/utils.test.ts new file mode 100644 index 0000000000..edd03a982d --- /dev/null +++ b/packages/authentication-oauth/test/utils.test.ts @@ -0,0 +1,39 @@ +import { strict as assert } from 'assert'; +import { getRedirect, getDefaultSettings } from '../src/utils'; +import { app } from './fixture'; +import { AuthenticationService } from '@feathersjs/authentication/lib'; + +describe('@feathersjs/authentication-oauth/utils', () => { + it('getRedirect', async () => { + const service: AuthenticationService = app.service('authentication'); + + app.get('authentication').oauth.redirect = '/home'; + + let redirect = await getRedirect(service, { accessToken: 'testing' }); + assert.equal(redirect, '/home#access_token=testing'); + + redirect = await getRedirect(service, { message: 'something went wrong' }); + assert.equal(redirect, '/home#error=something%20went%20wrong'); + + redirect = await getRedirect(service, {}); + assert.equal(redirect, '/home#error=OAuth%20Authentication%20not%20successful'); + + app.get('authentication').oauth.redirect = '/home?'; + + redirect = await getRedirect(service, { accessToken: 'testing' }); + assert.equal(redirect, '/home?access_token=testing'); + + delete app.get('authentication').oauth.redirect; + + redirect = await getRedirect(service, { accessToken: 'testing' }); + assert.equal(redirect, null); + }); + + it('getDefaultSettings', () => { + const settings = getDefaultSettings(app); + + assert.equal(settings.path, '/auth'); + assert.equal(settings.authService, 'authentication'); + assert.equal(settings.linkStrategy, 'jwt'); + }); +}); diff --git a/packages/authentication-oauth/tsconfig.json b/packages/authentication-oauth/tsconfig.json new file mode 100644 index 0000000000..316fd41336 --- /dev/null +++ b/packages/authentication-oauth/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig", + "include": [ + "src/**/*.ts" + ], + "compilerOptions": { + "outDir": "lib" + } +} diff --git a/packages/authentication/src/core.ts b/packages/authentication/src/core.ts index 04189a5c7c..8ee6efc617 100644 --- a/packages/authentication/src/core.ts +++ b/packages/authentication/src/core.ts @@ -2,7 +2,7 @@ import { promisify } from 'util'; import { merge } from 'lodash'; import jsonwebtoken, { SignOptions, Secret, VerifyOptions } from 'jsonwebtoken'; import uuidv4 from 'uuid/v4'; -import { NotAuthenticated, BadRequest } from '@feathersjs/errors'; +import { NotAuthenticated } from '@feathersjs/errors'; import Debug from 'debug'; import { Application, Params } from '@feathersjs/feathers'; import { IncomingMessage, ServerResponse } from 'http'; @@ -244,10 +244,6 @@ export class AuthenticationBase { const strategies = this.getStrategies(...names) .filter(current => current && typeof current.parse === 'function'); - if (strategies.length === 0) { - throw new BadRequest('Authentication HTTP parser needs at least one allowed strategy'); - } - debug('Strategies parsing HTTP header for authentication information', names); for (const authStrategy of strategies) { diff --git a/packages/authentication/src/options.ts b/packages/authentication/src/options.ts index 6abf50e8a3..492565916d 100644 --- a/packages/authentication/src/options.ts +++ b/packages/authentication/src/options.ts @@ -1,5 +1,5 @@ export default { - httpStrategies: [ 'jwt' ], + httpStrategies: [], jwtStrategies: [], jwtOptions: { header: { typ: 'access' }, // by default is an access token but can be any type diff --git a/packages/authentication/src/service.ts b/packages/authentication/src/service.ts index d36c27289b..4fc779c5bb 100644 --- a/packages/authentication/src/service.ts +++ b/packages/authentication/src/service.ts @@ -14,11 +14,11 @@ export class AuthenticationService extends AuthenticationBase implements Service * @param _authResult The current authentication result * @param params The service call parameters */ - getPayload (_authResult: AuthenticationResult, params: Params) { + async getPayload (_authResult: AuthenticationResult, params: Params) { // Uses `params.payload` or returns an empty payload const { payload = {} } = params; - return Promise.resolve(payload); + return payload; } /** @@ -29,7 +29,7 @@ export class AuthenticationService extends AuthenticationBase implements Service */ async getJwtOptions (authResult: AuthenticationResult, params: Params) { const { service, entity, entityId } = this.configuration; - const jwtOptions = merge({}, params.jwt); + const jwtOptions = merge({}, params.jwtOptions, params.jwt); const hasEntity = service && entity && authResult[entity]; // Set the subject to the entity id if it is available @@ -53,8 +53,8 @@ export class AuthenticationService extends AuthenticationBase implements Service * @param data The authentication request (should include `strategy` key) * @param params Service call parameters */ - async create (data: AuthenticationRequest, params?: Params) { - const { jwtStrategies } = this.configuration; + async create (data: AuthenticationRequest, params: Params) { + const jwtStrategies = params.jwtStrategies || this.configuration.jwtStrategies; if (!jwtStrategies.length) { throw new NotAuthenticated('No authentication strategies allowed for creating a JWT (`jwtStrategies`)'); @@ -86,7 +86,7 @@ export class AuthenticationService extends AuthenticationBase implements Service * @param id The JWT to remove or null * @param params Service call parameters */ - async remove (id: null|string, params?: Params) { + async remove (id: null|string, params: Params) { const { authentication } = params; const { jwtStrategies } = this.configuration; @@ -114,7 +114,7 @@ export class AuthenticationService extends AuthenticationBase implements Service if (entity !== null) { if (service === undefined) { - throw new Error(`The 'service' options is not set in the authentication configuration`); + throw new Error(`The 'service' option is not set in the authentication configuration`); } if (this.app.service(service) === undefined) { diff --git a/packages/authentication/test/core.test.ts b/packages/authentication/test/core.test.ts index e083afe177..f73aacc1d0 100644 --- a/packages/authentication/test/core.test.ts +++ b/packages/authentication/test/core.test.ts @@ -104,6 +104,17 @@ describe('authentication/core', () => { assert.deepStrictEqual(first.configuration, { hello: 'test' }); }); + + it('strategy configuration getter', () => { + const [ first ] = auth.getStrategies('first') as [ Strategy1 ]; + const oldService = auth.configuration.service; + + delete auth.configuration.service; + + assert.strictEqual(first.entityService, null); + + auth.configuration.service = oldService; + }); }); describe('authenticate', () => { @@ -227,15 +238,10 @@ describe('authentication/core', () => { describe('parse', () => { const res = {} as ServerResponse; - it('errors when no names are given', async () => { + it('returns null when no names are given', async () => { const req = {} as MockRequest; - try { - await auth.parse(req, res); - assert.fail('Should never get here'); - } catch (error) { - assert.strictEqual(error.message, 'Authentication HTTP parser needs at least one allowed strategy'); - } + assert.strictEqual(await auth.parse(req, res), null); }); it('successfully parses a request (first)', async () => { diff --git a/packages/authentication/test/service.test.ts b/packages/authentication/test/service.test.ts index 71af23b32e..682022a834 100644 --- a/packages/authentication/test/service.test.ts +++ b/packages/authentication/test/service.test.ts @@ -228,6 +228,22 @@ describe('authentication/service', () => { } }); + it('throws an error if service name is not set', () => { + const otherApp = feathers(); + + otherApp.use('/authentication', new AuthenticationService(otherApp, 'authentication', { + secret: 'supersecret', + jwtStrategies: [ 'first' ] + })); + + try { + otherApp.setup(); + assert.fail('Should never get here'); + } catch (error) { + assert.strictEqual(error.message, `The 'service' option is not set in the authentication configuration`); + } + }); + it('throws an error if entity service does not exist', () => { const otherApp = feathers(); diff --git a/packages/express/index.d.ts b/packages/express/index.d.ts index 906bb452c4..5bc8b9f680 100644 --- a/packages/express/index.d.ts +++ b/packages/express/index.d.ts @@ -14,7 +14,7 @@ export type Application = express.Application & FeathersApplication; export function errorHandler (options?: { public?: string, - logger?: { error?: (msg: string) => void }, + logger?: { error?: (msg: string) => void }|null, html?: any, json?: any }): express.ErrorRequestHandler; @@ -55,4 +55,4 @@ export { export function parseAuthentication (...strategies: string[]): express.RequestHandler; export function authenticate (...strategies: string[]): express.RequestHandler; -export const original: typeof express; +export const original: () => express.Application; diff --git a/packages/express/lib/authentication.js b/packages/express/lib/authentication.js index 93cdfe6b2b..ec99d7b667 100644 --- a/packages/express/lib/authentication.js +++ b/packages/express/lib/authentication.js @@ -1,4 +1,5 @@ const { flatten, merge } = require('lodash'); +const debug = require('debug')('@feathersjs/express/authentication'); const normalizeStrategy = (_settings = [], ..._strategies) => typeof _settings === 'string' @@ -25,8 +26,14 @@ exports.parseAuthentication = (settings = {}) => { const { httpStrategies = [] } = service.configuration; + if (httpStrategies.length === 0) { + debug('No `httpStrategies` found in authentication configuration'); + return next(); + } + service.parse(req, res, ...httpStrategies) .then(authentication => { + debug('Parsed authentication from HTTP header', authentication); merge(req, { authentication, feathers: { authentication } @@ -48,8 +55,11 @@ exports.authenticate = (...strategies) => { const { app, authentication } = req; const service = getService(settings, app); + debug('Authenticating with Express middleware and strategies', settings.strategies); + service.authenticate(authentication, req.feathers, ...settings.strategies) .then(authResult => { + debug('Merging request with', authResult); merge(req, authResult); next(); diff --git a/tslint.json b/tslint.json index bda814338b..ed8892ff1b 100644 --- a/tslint.json +++ b/tslint.json @@ -8,6 +8,7 @@ "packages/authentication/lib/**", "packages/authentication-local/lib/**", "packages/authentication-client/lib/**", + "packages/authentication-oauth/lib/**", "packages/configuration/lib/**", "packages/commons/lib/**", "packages/transport-commons/lib/**",