diff --git a/extensions/authentication-passport/package-lock.json b/extensions/authentication-passport/package-lock.json index 17117a4e3f79..cb3b02984e5f 100644 --- a/extensions/authentication-passport/package-lock.json +++ b/extensions/authentication-passport/package-lock.json @@ -56,6 +56,15 @@ "integrity": "sha512-gpNnRnZP3VWzzj5k3qrpRC6Rk3H/uclhAVo1aIvwzK5p5cOrs9yEyQ8H/HBsBY0u5rrWxXEiVPQ0dEB6pkjE8Q==", "dev": true }, + "@types/oauth": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.1.tgz", + "integrity": "sha512-a1iY62/a3yhZ7qH7cNUsxoI3U/0Fe9+RnuFrpTKr+0WVOzbKlSLojShCKe20aOD1Sppv+i8Zlq0pLDuTJnwS4A==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/passport": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.3.tgz", @@ -75,6 +84,23 @@ "@types/passport": "*" } }, + "@types/passport-oauth2": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.8.tgz", + "integrity": "sha512-tlX16wyFE5YJR2pHpZ308dgB1MV9/Ra2wfQh71eWk+/umPoD1Rca2D4N5M27W7nZm1wqUNGTk1I864nHvEgiFA==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/oauth": "*", + "@types/passport": "*" + } + }, + "@types/qs": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.1.tgz", + "integrity": "sha512-lhbQXx9HKZAPgBkISrBcmAcMpZsmpe/Cd/hY7LGZS5OfkySUBItnPZHgQPssWYUET8elF+yCFBbP1Q0RZPTdaw==", + "dev": true + }, "@types/range-parser": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", @@ -91,6 +117,536 @@ "@types/mime": "*" } }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dev": true, + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "dev": true, + "requires": { + "follow-redirects": "1.5.10" + } + }, + "base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "dev": true + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "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" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + } + } + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=", + "dev": true + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "component-emitter": { + "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 + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dev": true, + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "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" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.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" + } + } + } + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "dev": true, + "requires": { + "debug": "=3.1.0" + } + }, + "form-data": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", + "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", + "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==", + "dev": true + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "dev": true + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "dev": true, + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dev": true, + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dev": true, + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=", + "dev": true + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=", + "dev": true + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=", + "dev": true + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=", + "dev": true + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "dev": true + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=", + "dev": true + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=", + "dev": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", + "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==", + "dev": true + }, + "mime-types": { + "version": "2.1.26", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", + "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", + "dev": true, + "requires": { + "mime-db": "1.43.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "dev": true + }, + "oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE=", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, "passport": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz", @@ -109,25 +665,283 @@ "passport-strategy": "1.x.x" } }, + "passport-oauth2": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.5.0.tgz", + "integrity": "sha512-kqBt6vR/5VlCK8iCx1/KpY42kQ+NEHZwsSyt4Y6STiNjU+wWICG1i8ucc1FapXDGO15C5O5VZz7+7vRzrDPXXQ==", + "dev": true, + "requires": { + "base64url": "3.x.x", + "oauth": "0.9.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + } + }, "passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, "pause": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "dev": true, + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, + "qs": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz", + "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==", + "dev": true + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.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" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "dev": true, + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.2.0", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.3.5" + }, + "dependencies": { + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + } + } + }, + "supertest": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-4.0.2.tgz", + "integrity": "sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ==", + "dev": true, + "requires": { + "methods": "^1.1.2", + "superagent": "^3.8.3" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true + }, "tslib": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==" }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "uid2": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", + "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, "util-promisifyall": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/util-promisifyall/-/util-promisifyall-1.0.6.tgz", "integrity": "sha512-l+o62sbaqStC1xt7oEhlafC4jWBgkOjBXvlPwxkvOYmNqpY8dNXuKdOa+VHjkYz2Fw98e0HvJtNKUg0+6hfP2w==" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true } } } diff --git a/extensions/authentication-passport/package.json b/extensions/authentication-passport/package.json index 391b5435b085..d1a99d177837 100644 --- a/extensions/authentication-passport/package.json +++ b/extensions/authentication-passport/package.json @@ -59,6 +59,17 @@ "@types/node": "^10.17.17", "@types/passport": "^1.0.3", "@types/passport-http": "^0.3.8", - "passport-http": "^0.3.0" + "@types/passport-oauth2": "^1.4.8", + "@types/qs": "^6.9.1", + "axios": "^0.19.2", + "body-parser": "^1.19.0", + "express": "^4.17.1", + "form-data": "^3.0.0", + "jsonwebtoken": "^8.5.1", + "lodash": "^4.17.15", + "passport-http": "^0.3.0", + "passport-oauth2": "^1.5.0", + "qs": "^6.9.3", + "supertest": "^4.0.2" } } diff --git a/extensions/authentication-passport/src/__tests__/acceptance/authentication-with-passport-strategy-oauth2-adapter.acceptance.ts b/extensions/authentication-passport/src/__tests__/acceptance/authentication-with-passport-strategy-oauth2-adapter.acceptance.ts new file mode 100644 index 000000000000..b83d8b92a090 --- /dev/null +++ b/extensions/authentication-passport/src/__tests__/acceptance/authentication-with-passport-strategy-oauth2-adapter.acceptance.ts @@ -0,0 +1,331 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/authentication-passport +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {UserProfileFactory, authenticate} from '@loopback/authentication'; +import { + Strategy as Oauth2Strategy, + StrategyOptions, + VerifyFunction, + VerifyCallback, +} from 'passport-oauth2'; +import {MyUser, userRepository} from './fixtures/user-repository'; +import { + simpleRestApplication, + configureApplication, +} from './fixtures/simple-rest-app'; +import {securityId, UserProfile, SecurityBindings} from '@loopback/security'; +import {StrategyAdapter} from '../../strategy-adapter'; +import {get} from '@loopback/openapi-v3'; +import { + Client, + createClientForHandler, + expect, + supertest, +} from '@loopback/testlab'; +import {RestApplication, RestBindings, Response} from '@loopback/rest'; +import { + startApp as startMockOauth2Server, + stopApp as stopMockOauth2Server, +} from './fixtures/mock-oauth2-social-app'; +import * as url from 'url'; +import {inject} from '@loopback/core'; +import axios from 'axios'; +import qs from 'qs'; + +/** + * This test consists of three main components -> the supertest client, the LoopBack app (simple-rest-app.ts) + * and the Mock Authorization Server (mock-oauth2-social-app.ts) + * + * Mock Authorization Server (mock-oauth2-social-app.ts) : + * mocks the authorization flow login with a social app like facebook, google, etc + * LoopBack app (simple-rest-app.ts) : + * has a simple app with no controller, Oauth2Controller is added in this test + * + * Test steps: + * + * A. an Oauth2Controller is added to LB App : defines two endpoints `/auth/thirdparty` and `/auth/thirdparty/callback` + * + * B. start LB app and Mock Authorization Server + * + * C. Test stage 1 : Authorization code grant - Get access code + * from below diagram check flows - [1, 2, 3, 4, 5] + * [1] - test end point `/auth/thirdparty` + * [2, 3] - test redirection to mock auth server login page + * [4, 5] - test login with mock server redirects to callback url + * + * D. Test stage 2 : Authentication - Exchange access code for access token + * from below diagram check flows - [6, 7, 8] + * [5, 6] - test callback endpoint `/auth/thirdparty/callback` + * [7, 8] - check if valid access token was fetched + */ + +// +------------+ +--------------+ +// | | | LoopBack App | +// | web client | -------------------[1]---------------------> | (simple-rest | +// | (supertest)| auth request to LB App on behalf of a user, | -app.ts) | +// | | payload: {'client_id': , 'secret': } | **** | +// | | | ^ | +// | | +---------------+ | | | +// | | | | <---------[2]------------- | | | +// | | | Mock | LB App redirects browser | | | +// | | | Authorization | to auth server,payload: | | | +// | | | Server | {'client_id':, | | | +// | | | (mock-oauth2- | 'callback_url': app url } | Stage 1 | +// | | | social-app.ts)| | | | +// | | | |----+ auth server redirects | | | +// | | | | | browser to login page, | | | +// | | | | [3] client_id and | | | +// | | | | | callback_url are | | | +// | | | |<---+ passed as hidden params | | | +// | | | | | | | +// | | -----[4]---> | | | v | +// | | login with | | -------[5]-------------> | **** | +// | | user name | | login success, auth server | ^ | +// | | /password | | redirects browser to callback | | | +// | | | | url with access_code | | | +// | | | | | | | +// | | | | <-------------[6]--------- | | | +// | | | | LB app requests access token | Stage 2 | +// | | | | with access_code | | | +// | | | | | | | +// | | | | ---------------[7]---------> | | | +// | | +---------------+ returns access token | | | +// | | | v | +// | | <------------------------[8]----------------------- | **** | +// +------------+ LB App returns to browser the access token +--------------+ + +/** + * options to pass to the Passport Strategy + */ +const oauth2Options: StrategyOptions = { + clientID: '1111', + clientSecret: '1917e2b73a87fd0c9a92afab64f6c8d4', + // `redirect_uri`: + // 'passport-oauth2' takes care of sending the configured `callBackURL` setting as `redirect_uri` + // to the authorization server. This behaviour is inherited by all other oauth2 modules like facebook, google, etc + callbackURL: 'http://localhost:8080/auth/thirdparty/callback', + // 'authorizationURL' is used by 'passport-oauth2' to begin the authorization grant flow + authorizationURL: 'http://localhost:9000/oauth/dialog', + // `tokenURL` is used by 'passport-oauth2' to exchange the access code for an access token + // this exchange is taken care internally by 'passport-oauth2' + // when the `callbackURL` is invoked by the authorization server + tokenURL: 'http://localhost:9000/oauth/token', +}; + +/** + * verify function for the oauth2 strategy + * This function mocks a lookup against a user profile datastore + * + * @param accessToken + * @param refreshToken + * @param profile + * @param done + */ +const verify: VerifyFunction = function ( + accessToken: string, + refreshToken: string, + userProfile: MyUser, + done: VerifyCallback, +) { + userProfile.token = accessToken; + if (!userRepository.findById(userProfile.id)) { + userRepository.createExternalUser(userProfile); + } + return done(null, userProfile); +}; + +/** + * convert user info to user profile + * @param user + */ +const myUserProfileFactory: UserProfileFactory = function ( + user: MyUser, +): UserProfile { + const userProfile = {[securityId]: user.id, ...user}; + return userProfile; +}; + +/** + * Login controller for third party oauth provider + * + * This creates an authentication endpoint for the third party oauth provider + * + * Two methods are expected + * + * 1. loginToThirdParty + * i. an endpoint for api clients to login via a third party app + * ii. the passport strategy identifies this call as a redirection to third party + * iii. this endpoint redirects to the third party authorization url + * + * 2. thirdPartyCallBack + * i. this is the callback for the thirdparty app + * ii. on successful user login the third party calls this endpoint with an access code + * iii. the passport oauth2 strategy exchanges the code for an access token + * iv. the passport oauth2 strategy then calls the provided `verify()` function with the access token + */ +export class Oauth2Controller { + constructor() {} + + // this configures the oauth2 strategy + @authenticate('oauth2') + // we have modeled this as a GET endpoint + @get('/auth/thirdparty') + // loginToThirdParty() is the handler for '/auth/thirdparty' + // this method is injected with redirect url and status + // the value for 'authentication.redirect.url' is set by the authentication action provider + loginToThirdParty( + @inject('authentication.redirect.url') + redirectUrl: string, + @inject('authentication.redirect.status') + status: number, + @inject(RestBindings.Http.RESPONSE) + response: Response, + ) { + // controller handles redirect + // and returns response object to indicate response is already handled + response.statusCode = status || 302; + response.setHeader('Location', redirectUrl); + response.end(); + return response; + } + + // we configure the callback url also with the same oauth2 strategy + @authenticate('oauth2') + // this SHOULD be a GET call so that the third party can ask the browser to redirect + @get('/auth/thirdparty/callback') + // thirdPartyCallBack() is the handler for '/auth/thirdparty/callback' + // the oauth2 strategy identifies this as a callback with the request.query.code sent by the third party app + // the oauth2 strategy exchanges the access code for a access token and then calls the provided verify() function + // the verify function creates a user profile after verifying the access token + thirdPartyCallBack(@inject(SecurityBindings.USER) user: UserProfile) { + // eslint-disable-next-line @typescript-eslint/camelcase + return {access_token: user.token}; + } +} + +describe('Oauth2 authorization flow', () => { + let app: RestApplication; + let oauth2Strategy: StrategyAdapter; + let client: Client; + + before(startMockOauth2Server); + after(stopMockOauth2Server); + + before(givenLoopBackApp); + before(givenOauth2Strategy); + before(setupAuthentication); + before(givenControllerInApp); + before(givenClient); + + let oauthProviderUrl: string; + let providerLoginUrl: string; + let callbackToLbApp: string; + let loginPageParams: string; + + context('Stage 1 - Authorization code grant', () => { + describe('when client invokes oauth flow', () => { + it('call is redirected to third party authorization url', async () => { + // HTTP status code 303 means see other, + // on seeing which the browser would redirect to the other location + const response = await client.get('/auth/thirdparty').expect(303); + oauthProviderUrl = response.get('Location'); + expect(url.parse(response.get('Location')).pathname).to.equal( + url.parse(oauth2Options.authorizationURL).pathname, + ); + }); + + it('call to authorization url is redirected to oauth providers login page', async () => { + // HTTP status code 302 means redirect to new uri, + // on seeing which the browser would redirect to the new uri + const response = await supertest('').get(oauthProviderUrl).expect(302); + providerLoginUrl = response.get('Location'); + loginPageParams = url.parse(providerLoginUrl).query ?? ''; + expect(url.parse(response.get('Location')).pathname).to.equal('/login'); + }); + + it('login page redirects to authorization app callback endpoint', async () => { + const loginPageHiddenParams = qs.parse(loginPageParams); + const params = { + username: 'user1', + password: 'abc', + // eslint-disable-next-line @typescript-eslint/camelcase + client_id: loginPageHiddenParams.client_id, + // eslint-disable-next-line @typescript-eslint/camelcase + redirect_uri: loginPageHiddenParams.redirect_uri, + scope: loginPageHiddenParams.scope, + }; + // On successful login, the authorizing app redirects to the callback url + // HTTP status code 302 is returned to the browser + const response = await supertest('') + .post('http://localhost:9000/login_submit') + .send(qs.stringify(params)) + .expect(302); + callbackToLbApp = response.get('Location'); + expect(url.parse(callbackToLbApp).pathname).to.equal( + '/auth/thirdparty/callback', + ); + }); + + it('callback url contains access code', async () => { + expect(url.parse(callbackToLbApp).query).to.containEql('code'); + }); + }); + }); + + context('Stage 2: Authentication', () => { + describe('Invoking call back url returns access token', () => { + it('access code can be exchanged for token', async () => { + expect(url.parse(callbackToLbApp).path).to.containEql( + '/auth/thirdparty/callback', + ); + const path: string = url.parse(callbackToLbApp).path ?? ''; + const response = await client.get(path).expect(200); + expect(response.body).property('access_token'); + }); + }); + }); + + function givenLoopBackApp() { + app = simpleRestApplication(); + } + + function givenOauth2Strategy() { + const passport = new Oauth2Strategy(oauth2Options, verify); + + // passport-oauth2 base class leaves user profile creation to subclass implementations + passport.userProfile = (accessToken, done) => { + // call the profile url in the mock authorization app with the accessToken + axios + .get('http://localhost:9000/verify?access_token=' + accessToken, { + headers: {Authorization: accessToken}, + }) + .then(response => { + done(null, response.data); + }) + .catch(err => { + done(err); + }); + }; + + // use strategy adapter to fit passport strategy to LoopBack app + oauth2Strategy = new StrategyAdapter( + passport, + 'oauth2', + myUserProfileFactory, + ); + } + + async function setupAuthentication() { + await configureApplication(oauth2Strategy, 'oauth2'); + } + + function givenControllerInApp() { + return app.controller(Oauth2Controller); + } + + function givenClient() { + client = createClientForHandler(app.requestHandler); + } +}); diff --git a/extensions/authentication-passport/src/__tests__/acceptance/fixtures/mock-oauth2-social-app.ts b/extensions/authentication-passport/src/__tests__/acceptance/fixtures/mock-oauth2-social-app.ts new file mode 100644 index 000000000000..c79c1f4cdef2 --- /dev/null +++ b/extensions/authentication-passport/src/__tests__/acceptance/fixtures/mock-oauth2-social-app.ts @@ -0,0 +1,334 @@ +// Copyright IBM Corp. 2018,2019. All Rights Reserved. +// Node module: @loopback/authentication-passport +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +/** + * Mock Authorization Server: + * mocks the authorization flow with a social app login like facebook, google, etc + * + * Endpoints : + * `/oauth/dialog` - opens the oauth2 flow, redirects to login page + * `/login` - loads the login page + * `/login_submit` - submit username , password + * `/oauth/token` - returns a token in exchange for a valid authorization code + * `/verify` - verifies token + */ + +'use strict'; + +import express from 'express'; +import {Server} from 'http'; +const jwt = require('jsonwebtoken'); +const bodyParser = require('body-parser'); +const _ = require('lodash'); +import {MyUser} from './user-repository'; + +const app = express(); +let server: Server; + +// to support json payload in body +app.use('parse', bodyParser.json()); +// to support html form bodies +app.use(bodyParser.text({type: 'text/html'})); +// create application/x-www-form-urlencoded parser +const urlencodedParser = bodyParser.urlencoded({extended: false}); + +interface JWT { + payload: { + jti: string; + client_id: string; + }; +} + +/** + * datastructure for an app registration, also holds issued tokens for an app + */ +interface App { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; + client_secret: string; + tokens: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; + }; +} + +/** + * list of registered apps for this oauth2 provider identified by their client ids + */ +interface AppRegistry { + [clientId: string]: App; +} + +/** + * apps registered with this provider + * format: + * { clientId: {client_secret, list_of_tokens} } + */ +const registeredApps: AppRegistry = { + // eslint-disable-next-line @typescript-eslint/camelcase + '1111': {client_secret: 'app1_secret', tokens: {}}, + // eslint-disable-next-line @typescript-eslint/camelcase + '2222': {client_secret: 'app2_secret', tokens: {}}, +}; + +/** + * user registry + */ +const users = [ + { + id: 1001, + username: 'user1', + password: 'abc', + email: 'usr1@lb.com', + signingKey: 'AZeb==', + }, + { + id: 1002, + username: 'user2', + password: 'xyz', + email: 'usr2@lb2.com', + signingKey: 'BuIx=+', + }, +]; + +/** + * find a user by a name and password + * @param {*} username + * @param {*} password + */ +function findUser(username: string, password: string) { + const usr = _.filter( + users, + (user: MyUser) => user.username === username && user.password === password, + ); + if (usr.length > 0) return usr[0]; + return null; +} + +/** + * create a jwt token + * @param {*} user + * @param {*} scopes + * @param {*} signingKey + */ +async function createJwt( + user: MyUser, + scopes: string, + signingKey: string, + clientId: string, +) { + const jti = Math.floor(Math.random() * Math.floor(1000)); + const token = await jwt.sign( + { + jti: jti, + sub: user.id, + name: user.username, + email: user.email, + iss: 'sample oauth provider', + exp: Math.floor(Date.now() / 1000) + 5 * 1000, + iat: Math.floor(Date.now() / 1000), + // eslint-disable-next-line @typescript-eslint/camelcase + grant_type: 'auth code', + scopes: scopes, + // eslint-disable-next-line @typescript-eslint/camelcase + client_id: clientId, + }, + signingKey, + ); + return {token: token, id: jti}; +} + +/** + * verify token + * + * check with given client id and token if token is valid + * + * @param {*} req + * @param {*} token + */ +async function verifyToken(token: string) { + const unwrappedJwt: JWT = jwt.decode(token, {json: true, complete: true}); + const tokenId: string = unwrappedJwt.payload.jti; + const registeredApp: App = registeredApps[unwrappedJwt.payload.client_id]; + if (registeredApp) { + const result = await jwt.verify(token, registeredApp[tokenId].signingKey); + if (result) { + return result; + } else { + throw new Error('invalid token'); + } + } else { + throw new Error('invalid app'); + } +} + +/** + * Endpoint: GET /oauth/dialog + * Begins the authorization code flow + * + * @params: redirect_uri, client_id + * passport-oauth2 takes care of sending the configured `callBackURL` setting as `redirect_uri` + * + * 1. validates if client_id is registered + * 2. redirects to login page if the client_id is registered + * 3. returns error if client_id is not registered + */ +app.get('/oauth/dialog', function (req, res) { + if (!req.query.redirect_uri) { + res.setHeader('Content-Type', 'application/json'); + res + .status(500) + .send(JSON.stringify({error: 'redirect_uri not sent in query'})); + return; + } + if (registeredApps[req.query.client_id]) { + let params = + '?client_id=' + + req.query.client_id + + '&&redirect_uri=' + + req.query.redirect_uri; + params = params + '&&scope=' + req.query.scope; + res.redirect('/login' + params); + } else { + res.send('invalid app'); + } +}); + +/** + * login page + * + * handles login part of the authorization call + */ +app.get('/login', function (req, response) { + response.setHeader('Content-Type', 'text/html'); + response.write(''); + response.write("
"); + // client_id and redirect_uri are stored as hidden variables + // for the provider to redirect on successful login + response.write( + '', + ); + response.write( + '', + ); + response.write( + '', + ); + response.write(''); + response.write(''); + response.write(''); + response.write(''); + response.end(); +}); + +/** + * login form submit + * handles callback part of the authorization call + * + * 1. creates access code + * 2. generates token + * 3. stores token + * 4. redirects to callback url with access code + */ +app.post('/login_submit', urlencodedParser, async function (req, res) { + const user = findUser(req.body.username, req.body.password); + if (user) { + // get registered app + const registeredApp = registeredApps[req.body.client_id]; + // generate access code + const authCode = Math.floor(Math.random() * Math.floor(1000)); + // create a token for the access code + const result = await createJwt( + user, + req.body.scope, + user.signingKey, + req.body.client_id, + ); + // store generated token + registeredApp.tokens[authCode] = {token: result.token}; + registeredApp[result.id] = {signingKey: user.signingKey, code: authCode}; + // redirect to call back url with the access code + let params = '?client_id=' + req.body.client_id; + params = params + '&&code=' + authCode; + res.redirect(req.body.redirect_uri + params); + } else { + res.sendStatus(401); + } +}); + +/** + * Endpoint: POST '/oauth/token' + * Returns: token + * + * returns token in exchange for access code + */ +app.post('/oauth/token', urlencodedParser, function (req, res) { + if (registeredApps[req.body.client_id]) { + //&& apps[req.query.client_id].client_secret === req.query.client_secret + const oauthstates = registeredApps[req.body.client_id].tokens; + if (oauthstates[req.body.code]) { + res.setHeader('Content-Type', 'application/json'); + // eslint-disable-next-line @typescript-eslint/camelcase + res.send({access_token: oauthstates[req.body.code].token}); + } else { + res.sendStatus(401); + } + } else { + res.sendStatus(401); + } +}); + +/** + * Endpoint: GET '/oauth/token' + * Returns: token + * + * returns token in exchange for access code + */ +app.get('/oauth/token', function (req, res) { + if (registeredApps[req.query.client_id]) { + //&& apps[req.query.client_id].client_secret === req.query.client_secret + const oauthstates = registeredApps[req.query.client_id].tokens; + if (oauthstates[req.query.code]) { + res.setHeader('Content-Type', 'application/json'); + // eslint-disable-next-line @typescript-eslint/camelcase + res.send({access_token: oauthstates[req.query.code].token}); + } else { + res.sendStatus(401); + } + } else { + res.sendStatus(401); + } +}); + +/** + * Endpoint: '/verify' + * Returns: user profile + * + * Verifies token and returns user profile + */ +app.get('/verify', async function (req, res) { + try { + const token = req.query.access_token || req.header('Authorization'); + const result = await verifyToken(token); + const expirationTime = result.exp; + res.setHeader('Content-Type', 'application/json'); + res.send({...result, expirationTime: expirationTime}); + } catch (err) { + res.setHeader('Content-Type', 'application/json'); + res.status(401).send(JSON.stringify({error: err})); + } +}); + +export function startApp() { + server = app.listen(9000); +} + +export function stopApp() { + server.close(); +} diff --git a/extensions/authentication-passport/src/__tests__/acceptance/fixtures/simple-rest-app.ts b/extensions/authentication-passport/src/__tests__/acceptance/fixtures/simple-rest-app.ts new file mode 100644 index 000000000000..d441d6126baf --- /dev/null +++ b/extensions/authentication-passport/src/__tests__/acceptance/fixtures/simple-rest-app.ts @@ -0,0 +1,84 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/authentication-passport +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + AuthenticateFn, + AuthenticationBindings, + AuthenticationComponent, +} from '@loopback/authentication'; +import {inject} from '@loopback/context'; +import { + FindRoute, + InvokeMethod, + ParseParams, + Reject, + RequestContext, + RestApplication, + RestBindings, + RestServer, + Send, + SequenceHandler, +} from '@loopback/rest'; +import {MyUser} from './user-repository'; +import {StrategyAdapter} from '../../../strategy-adapter'; +import {extensionFor} from '@loopback/core'; + +const SequenceActions = RestBindings.SequenceActions; + +let app: RestApplication; +let server: RestServer; + +export class MySequence implements SequenceHandler { + constructor( + @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute, + @inject(SequenceActions.PARSE_PARAMS) + protected parseParams: ParseParams, + @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, + @inject(SequenceActions.SEND) protected send: Send, + @inject(SequenceActions.REJECT) protected reject: Reject, + @inject(AuthenticationBindings.AUTH_ACTION) + protected authenticateRequest: AuthenticateFn, + ) {} + + async handle(context: RequestContext) { + try { + const {request, response} = context; + const route = this.findRoute(request); + + //call authentication action + await this.authenticateRequest(request); + + // Authentication successful, proceed to invoke controller + const args = await this.parseParams(request, route); + const result = await this.invoke(route, args); + this.send(response, result); + } catch (error) { + this.reject(context, error); + return; + } + } +} + +export function simpleRestApplication(): RestApplication { + app = new RestApplication(); + app.component(AuthenticationComponent); + return app; +} + +export async function configureApplication( + authStrategy: StrategyAdapter, + authKey: string, +) { + server = await app.getServer(RestServer); + app + .bind(authKey) + .to(authStrategy) + .apply( + extensionFor( + AuthenticationBindings.AUTHENTICATION_STRATEGY_EXTENSION_POINT_NAME, + ), + ); + server.sequence(MySequence); +} diff --git a/extensions/authentication-passport/src/__tests__/acceptance/fixtures/user-repository.ts b/extensions/authentication-passport/src/__tests__/acceptance/fixtures/user-repository.ts new file mode 100644 index 000000000000..89f078e24a03 --- /dev/null +++ b/extensions/authentication-passport/src/__tests__/acceptance/fixtures/user-repository.ts @@ -0,0 +1,73 @@ +// Copyright IBM Corp. 2018,2019. All Rights Reserved. +// Node module: @loopback/cli +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +const _ = require('lodash'); + +/** + * A simple User model + */ +export interface MyUser { + id: string; + username?: string; + firstName?: string; + lastName?: string; + password?: string; + email?: string; + token?: string; +} + +/** + * Repository to store and access user objects + */ +export class UserRepository { + constructor(readonly list: {[key: string]: MyUser}) {} + + /** + * find by username + * @param username + */ + find(username: string): MyUser { + return _.filter(this.list, (user: MyUser) => user.username === username); + } + + /** + * find by id + * @param id + */ + findById(id: string): MyUser | undefined { + const usr = _.filter(this.list, (user: MyUser) => user.id === id); + if (usr.length > 0) return usr[0]; + } + + /** + * create profile for external user + * @param user + */ + createExternalUser(user: MyUser) { + this.list[user.id] = user; + } +} + +/** + * Sample data to mock existing registered users + * new users can be registered with the repository functions + */ +const userRepository = new UserRepository({ + '999': { + id: '999', + username: 'joesmith71', + firstName: 'Joseph', + lastName: 'Smith', + }, + '1000': { + id: '1000', + username: 'simonsmith71', + firstName: 'Simon', + lastName: 'Smith', + }, +}); + +export {userRepository}; diff --git a/extensions/authentication-passport/src/strategy-adapter.ts b/extensions/authentication-passport/src/strategy-adapter.ts index a83825a4f76c..53d0e03b85dc 100644 --- a/extensions/authentication-passport/src/strategy-adapter.ts +++ b/extensions/authentication-passport/src/strategy-adapter.ts @@ -7,7 +7,7 @@ import { AuthenticationStrategy, UserProfileFactory, } from '@loopback/authentication'; -import {HttpErrors, Request} from '@loopback/rest'; +import {HttpErrors, Request, RedirectRoute} from '@loopback/rest'; import {UserProfile} from '@loopback/security'; import {Strategy} from 'passport'; @@ -42,9 +42,9 @@ export class StrategyAdapter implements AuthenticationStrategy { * 3. authenticate using the strategy * @param request The incoming request. */ - authenticate(request: Request): Promise { + authenticate(request: Request): Promise { const userProfileFactory = this.userProfileFactory; - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { // mix-in passport additions like req.logIn and req.logOut for (const key in passportRequestMixin) { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -73,6 +73,14 @@ export class StrategyAdapter implements AuthenticationStrategy { reject(new HttpErrors.InternalServerError(error)); }; + // handle redirection for oauth2 authorization flows + strategy.redirect = function (url: string, status: number) { + // resolve with redirect options + // the controller configured with the oauth2 strategy will have to handle actual redirection + const redirectOptions = new RedirectRoute('', url, status); + resolve(redirectOptions); + }; + // authenticate strategy.authenticate(request); }); diff --git a/packages/authentication/src/__tests__/unit/fixtures/mock-strategy.ts b/packages/authentication/src/__tests__/unit/fixtures/mock-strategy.ts index 8d1cb9c3c2ec..1dae558fd4eb 100644 --- a/packages/authentication/src/__tests__/unit/fixtures/mock-strategy.ts +++ b/packages/authentication/src/__tests__/unit/fixtures/mock-strategy.ts @@ -27,7 +27,7 @@ export class MockStrategy implements AuthenticationStrategy { return this.mockUser; } - async authenticate(req: Request): Promise { + async authenticate(req: Request): Promise { return this.verify(req); } /** @@ -47,6 +47,12 @@ export class MockStrategy implements AuthenticationStrategy { const err = new AuthenticationError('authorization failed'); err.statusCode = 401; throw err; + } else if ( + request.headers && + request.headers.testState && + request.headers.testState === 'empty' + ) { + return; } else if ( request.headers && request.headers.testState && diff --git a/packages/authentication/src/__tests__/unit/providers/authentication.provider.unit.ts b/packages/authentication/src/__tests__/unit/providers/authentication.provider.unit.ts index 16a290448fd2..0889ad4feb03 100644 --- a/packages/authentication/src/__tests__/unit/providers/authentication.provider.unit.ts +++ b/packages/authentication/src/__tests__/unit/providers/authentication.provider.unit.ts @@ -109,6 +109,26 @@ describe('AuthenticateActionProvider', () => { } expect(error).to.have.property('statusCode', 401); }); + + it('throws USER_PROFILE_NOT_FOUND error when userprofile not returned', async () => { + const context: Context = new Context(); + context.bind(AuthenticationBindings.STRATEGY).to(strategy); + context + .bind(AuthenticationBindings.AUTH_ACTION) + .toProvider(AuthenticateActionProvider); + const authenticate = await context.get( + AuthenticationBindings.AUTH_ACTION, + ); + const request = {}; + request.headers = {testState: 'empty'}; + let error; + try { + await authenticate(request); + } catch (err) { + error = err; + } + expect(error).to.have.property('code', 'USER_PROFILE_NOT_FOUND'); + }); }); function givenAuthenticateActionProvider() { @@ -117,6 +137,8 @@ describe('AuthenticateActionProvider', () => { provider = new AuthenticateActionProvider( () => Promise.resolve(strategy), u => (currentUser = u), + url => url, + status => status, ); currentUser = undefined; } diff --git a/packages/authentication/src/keys.ts b/packages/authentication/src/keys.ts index 6abcbafdc5ed..a7ae252591ae 100644 --- a/packages/authentication/src/keys.ts +++ b/packages/authentication/src/keys.ts @@ -118,6 +118,16 @@ export namespace AuthenticationBindings { // Make `CURRENT_USER` the alias of SecurityBindings.USER for backward compatibility export const CURRENT_USER = SecurityBindings.USER; + + // Redirect url for authenticating current user + export const AUTHENTICATION_REDIRECT_URL = BindingKey.create( + 'authentication.redirect.url', + ); + + // Authentication redirect status, usually 302 or 303, indicates a web client will redirect + export const AUTHENTICATION_REDIRECT_STATUS = BindingKey.create( + 'authentication.redirect.status', + ); } /** diff --git a/packages/authentication/src/providers/auth-action.provider.ts b/packages/authentication/src/providers/auth-action.provider.ts index aaa10d3c963e..7217e3385cf6 100644 --- a/packages/authentication/src/providers/auth-action.provider.ts +++ b/packages/authentication/src/providers/auth-action.provider.ts @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import {Getter, inject, Provider, Setter} from '@loopback/context'; -import {Request} from '@loopback/rest'; +import {Request, RedirectRoute} from '@loopback/rest'; import {SecurityBindings, UserProfile} from '@loopback/security'; import {AuthenticationBindings} from '../keys'; import { @@ -29,6 +29,10 @@ export class AuthenticateActionProvider implements Provider { readonly getStrategy: Getter, @inject.setter(SecurityBindings.USER) readonly setCurrentUser: Setter, + @inject.setter(AuthenticationBindings.AUTHENTICATION_REDIRECT_URL) + readonly setRedirectUrl: Setter, + @inject.setter(AuthenticationBindings.AUTHENTICATION_REDIRECT_STATUS) + readonly setRedirectStatus: Setter, ) {} /** @@ -49,8 +53,22 @@ export class AuthenticateActionProvider implements Provider { return undefined; } - const userProfile = await strategy.authenticate(request); - if (!userProfile) { + const authResponse = await strategy.authenticate(request); + let userProfile: UserProfile; + + // response from `strategy.authenticate()` could return an object of type UserProfile or RedirectRoute + if (RedirectRoute.isRedirectRoute(authResponse)) { + const redirectOptions = authResponse; + // bind redirection url and status to the context + // controller should handle actual redirection + this.setRedirectUrl(redirectOptions.targetLocation); + this.setRedirectStatus(redirectOptions.statusCode); + } else if (authResponse) { + // if `strategy.authenticate()` returns an object of type UserProfile, set it as current user + userProfile = authResponse as UserProfile; + this.setCurrentUser(userProfile); + return userProfile; + } else if (!authResponse) { // important to throw a non-protocol-specific error here const error = new Error( `User profile not returned from strategy's authenticate function`, @@ -60,8 +78,5 @@ export class AuthenticateActionProvider implements Provider { }); throw error; } - - this.setCurrentUser(userProfile); - return userProfile; } } diff --git a/packages/authentication/src/types.ts b/packages/authentication/src/types.ts index c810b0644908..b0721088fd23 100644 --- a/packages/authentication/src/types.ts +++ b/packages/authentication/src/types.ts @@ -10,7 +10,7 @@ import { Context, extensionFor, } from '@loopback/core'; -import {Request} from '@loopback/rest'; +import {Request, RedirectRoute} from '@loopback/rest'; import {UserProfile} from '@loopback/security'; import {AuthenticationBindings} from './keys'; @@ -87,7 +87,9 @@ export interface AuthenticationStrategy { * * @param request - Express request object */ - authenticate(request: Request): Promise; + authenticate( + request: Request, + ): Promise; } export const AUTHENTICATION_STRATEGY_NOT_FOUND = diff --git a/packages/rest/src/router/redirect-route.ts b/packages/rest/src/router/redirect-route.ts index 7e8f2ba494ad..f7bd8eef44c7 100644 --- a/packages/rest/src/router/redirect-route.ts +++ b/packages/rest/src/router/redirect-route.ts @@ -23,9 +23,9 @@ export class RedirectRoute implements RouteEntry, ResolvedRoute { }; constructor( - private readonly sourcePath: string, - private readonly targetLocation: string, - private readonly statusCode: number = 303, + public readonly sourcePath: string, + public readonly targetLocation: string, + public readonly statusCode: number = 303, ) { this.path = sourcePath; } @@ -44,4 +44,21 @@ export class RedirectRoute implements RouteEntry, ResolvedRoute { describe(): string { return `RedirectRoute from "${this.sourcePath}" to "${this.targetLocation}"`; } + + /** + * type guard type checker for this class + * @param obj + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static isRedirectRoute(obj: any): obj is RedirectRoute { + const redirectOptions = obj as RedirectRoute; + if ( + redirectOptions?.targetLocation && + redirectOptions.spec && + redirectOptions.spec.description === 'LoopBack Redirect route' + ) { + return true; + } + return false; + } }