diff --git a/README.md b/README.md index 6ba9bbcf..0c5e40fb 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ REST Client allows you to send HTTP request and view the response in Visual Stud + `{{$processEnv [%]envVarName}}` + `{{$dotenv [%]variableName}}` + `{{$aadToken [new] [public|cn|de|us|ppe] [] [aud:]}}` + + `{{$oidcAccessToken [new] [] [] [authorizeEndpoint:`: Optional. Identifier of the application registration to use to obtain the token. Default uses an application registration created specifically for this plugin. +* `{{$oidcAccessToken [new] [] [] [authorizeEndpoint:`: Optional. Identifier of the application registration to use to obtain the token. + + `callbackPort:`: Optional. Port to use for the local callback server. Default: 7777 (random port). + + `authorizeEndpoint:`: The authorization endpoint to use. + + `tokenEndpoint:`: The token endpoint to use. + + `scopes:`: Optional. Comma delimited list of scopes that must have consent to allow the call to be successful. + + `audience:`: Optional. + * `{{$guid}}`: Add a RFC 4122 v4 UUID * `{{$processEnv [%]envVarName}}`: Allows the resolution of a local machine environment variable to a string value. A typical use case is for secret keys that you don't want to commit to source control. For example: Define a shell environment variable in `.bashrc` or similar on windows diff --git a/package-lock.json b/package-lock.json index 4e294aac..931feda1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,11 @@ "jsonc-parser": "^2.0.2", "jsonpath-plus": "^0.20.1", "mime-types": "^2.1.14", + "node-fetch": "^2.6.7", + "node-jws": "^0.1.4", + "open": "^10.1.0", "pretty-data": "^0.40.0", + "sanitize-html": "^2.13.0", "tough-cookie": "^4.1.3", "tough-cookie-file-store": "^2.0.3", "uuid": "^3.3.2", @@ -40,8 +44,10 @@ "devDependencies": { "@types/aws4": "^1.5.1", "@types/fs-extra": "^5.0.4", + "@types/jws": "^3.2.10", "@types/mocha": "^5.2.6", "@types/node": "^18.0.0", + "@types/node-fetch": "^2.6.11", "@types/vscode": "^1.81.0", "mocha": "^10.4.0", "ts-loader": "^7.0.5", @@ -284,6 +290,16 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/jws": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/@types/jws/-/jws-3.2.10.tgz", + "integrity": "sha512-cOevhttJmssERB88/+XvZXvsq5m9JLKZNUiGfgjUb5lcPRdV2ZQciU6dU76D/qXXFYpSqkP3PrSg4hMTiafTZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", @@ -306,6 +322,17 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", @@ -819,6 +846,21 @@ "node": ">=0.10.0" } }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", @@ -1060,6 +1102,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -1068,6 +1147,18 @@ "node": ">=10" } }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1101,6 +1192,61 @@ "node": ">=0.3.1" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "8.6.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", @@ -1175,6 +1321,18 @@ "node": ">=6.9.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/envinfo": { "version": "7.13.0", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz", @@ -1394,6 +1552,21 @@ } } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/from": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", @@ -1639,6 +1812,25 @@ "node": "*" } }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -1803,6 +1995,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1824,6 +2031,24 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -1882,6 +2107,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2663,12 +2903,56 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-jws": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/node-jws/-/node-jws-0.1.4.tgz", + "integrity": "sha512-oJk6X0kad7VOms/uUVagHskJ8ENP2tqPAReIBT1R7ulf0BkBmNJgyMK7kFhwG9w55KLZj17bWzNbZpnkbZjvMQ==", + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -2703,6 +2987,24 @@ "wrappy": "1" } }, + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-cancelable": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", @@ -2747,6 +3049,12 @@ "node": ">=6" } }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2789,10 +3097,10 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", @@ -2837,6 +3145,34 @@ "node": ">=8" } }, + "node_modules/postcss": { + "version": "8.4.40", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz", + "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/pretty-data": { "version": "0.40.0", "resolved": "https://registry.npmjs.org/pretty-data/-/pretty-data-0.40.0.tgz", @@ -3023,6 +3359,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -3047,6 +3395,41 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/sanitize-html": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.13.0.tgz", + "integrity": "sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, + "node_modules/sanitize-html/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sanitize-html/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -3120,6 +3503,15 @@ "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -3401,6 +3793,12 @@ "node": ">= 4.0.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/ts-loader": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-7.0.5.tgz", @@ -3681,6 +4079,12 @@ "node": ">=10.13.0" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, "node_modules/webpack": { "version": "5.91.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz", @@ -3827,6 +4231,16 @@ "node": ">=6" } }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 35250175..74206b3f 100644 --- a/package.json +++ b/package.json @@ -475,6 +475,54 @@ "scope": "resource", "description": "Preview response in untitled document if set to true, otherwise displayed in html view" }, + "rest-client.oidcScopes": { + "type": "array", + "default": [], + "scope": "resource", + "description": "Scopes to be used by the OIDC client, default: [openid profile email]" + }, + "rest-client.oidcCertificates": { + "type": "object", + "default": {}, + "scope": "resource", + "description": "Certificate paths for Oidc callback server", + "pattern": "^(?!http(s?)://)", + "additionalProperties": { + "anyOf": [ + { + "type": "object", + "default": {}, + "description": "Certifcate paths for Oidc callback server", + "properties": { + "cert": { + "type": "string", + "description": "Absolute or relative path of Public x509 certificate" + }, + "key": { + "type": "string", + "description": "Absolute or relative path of Private key" + }, + "pfx": { + "type": "string", + "description": "Absolute or relative path of PKCS #12 certificate" + }, + "passphrase": { + "type": "string", + "description": "[Optional] A string of passphrase for the private key or pfx" + } + }, + "dependencies": { + "cert": [ + "key" + ], + "key": [ + "cert" + ] + } + } + ] + } + }, "rest-client.certificates": { "type": "object", "default": {}, @@ -660,8 +708,10 @@ "devDependencies": { "@types/aws4": "^1.5.1", "@types/fs-extra": "^5.0.4", + "@types/jws": "^3.2.10", "@types/mocha": "^5.2.6", "@types/node": "^18.0.0", + "@types/node-fetch": "^2.6.11", "@types/vscode": "^1.81.0", "mocha": "^10.4.0", "ts-loader": "^7.0.5", @@ -691,7 +741,11 @@ "jsonc-parser": "^2.0.2", "jsonpath-plus": "^0.20.1", "mime-types": "^2.1.14", + "node-fetch": "^2.6.7", + "node-jws": "^0.1.4", + "open": "^10.1.0", "pretty-data": "^0.40.0", + "sanitize-html": "^2.13.0", "tough-cookie": "^4.1.3", "tough-cookie-file-store": "^2.0.3", "uuid": "^3.3.2", diff --git a/src/common/constants.ts b/src/common/constants.ts index c6d74c0e..65da8bbd 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -24,6 +24,11 @@ export const AzureActiveDirectoryDescription = "Prompts to sign in to Azure AD a export const AzureActiveDirectoryV2TokenVariableName = "$aadV2Token"; export const AzureActiveDirectoryV2TokenDescription = "Prompts to sign in to Azure AD V2 and adds the token to the request"; +export const OidcVariableName = "$oidcAccessToken"; +export const OidcDescription = "Prompts to sign in to an Oidc provider and adds the token to the request"; +export const OIdcForceNewOption = "new"; + + /** * NOTE: The client id represents an AAD app people sign in to. The client id is sent to AAD to indicate what app * is requesting a token for the user. When the user signs in, AAD shows the name of the app to confirm the user is diff --git a/src/models/configurationSettings.ts b/src/models/configurationSettings.ts index 97253567..20da7373 100644 --- a/src/models/configurationSettings.ts +++ b/src/models/configurationSettings.ts @@ -34,6 +34,8 @@ export interface IRestClientSettings { readonly mimeAndFileExtensionMapping: { [key: string]: string }; readonly previewResponseInUntitledDocument: boolean; readonly hostCertificates: HostCertificates; + readonly oidcCertificates: HostCertificates; + readonly oidcScopes: string[]; readonly suppressResponseBodyContentTypeValidationWarning: boolean; readonly previewOption: PreviewOption; readonly disableHighlightResponseBodyForLargeResponse: boolean; @@ -68,6 +70,8 @@ export class SystemSettings implements IRestClientSettings { private _mimeAndFileExtensionMapping: { [key: string]: string }; private _previewResponseInUntitledDocument: boolean; private _hostCertificates: HostCertificates; + private _oidcCertificates: HostCertificates; + private _oidcScopes: string[]; private _suppressResponseBodyContentTypeValidationWarning: boolean; private _previewOption: PreviewOption; private _disableHighlightResponseBodyForLargeResponse: boolean; @@ -151,6 +155,14 @@ export class SystemSettings implements IRestClientSettings { return this._hostCertificates; } + public get oidcCertificates() { + return this._oidcCertificates; + } + + public get oidcScopes() { + return this._oidcScopes; + } + public get suppressResponseBodyContentTypeValidationWarning() { return this._suppressResponseBodyContentTypeValidationWarning; } @@ -268,6 +280,8 @@ export class SystemSettings implements IRestClientSettings { this._previewColumn = this.parseColumn(restClientSettings.get("previewColumn", "two")); this._previewResponsePanelTakeFocus = restClientSettings.get("previewResponsePanelTakeFocus", true); this._hostCertificates = restClientSettings.get("certificates", {}); + this._oidcCertificates = Object.assign({}, restClientSettings.get("oidcCertificates", {})); + this._oidcScopes = restClientSettings.get("oidcScopes", ['openid', 'profile', 'email']); this._disableHighlightResponseBodyForLargeResponse = restClientSettings.get("disableHighlightResponseBodyForLargeResponse", true); this._disableAddingHrefLinkForLargeResponse = restClientSettings.get("disableAddingHrefLinkForLargeResponse", true); this._largeResponseBodySizeLimitInMB = restClientSettings.get("largeResponseBodySizeLimitInMB", 5); @@ -389,6 +403,14 @@ export class RestClientSettings implements IRestClientSettings { return this.systemSettings.previewResponseInUntitledDocument; } + public get oidcCertificates() { + return this.systemSettings.oidcCertificates; + } + + public get oidcScopes() { + return this.systemSettings.oidcScopes; + } + public get hostCertificates() { return this.systemSettings.hostCertificates; } diff --git a/src/utils/auth/oidcClient.ts b/src/utils/auth/oidcClient.ts new file mode 100644 index 00000000..7ca824b6 --- /dev/null +++ b/src/utils/auth/oidcClient.ts @@ -0,0 +1,554 @@ +import * as crypto from 'crypto'; +import fs from 'fs'; +import * as http from "http"; +import * as https from "https"; +import * as jws from 'jws'; +import fetch from 'node-fetch'; +import path from 'path'; +import { SecureContextOptions } from 'tls'; +import { v4 as uuid } from 'uuid'; +import { env, Uri, window } from "vscode"; +import { IRestClientSettings, SystemSettings } from '../../models/configurationSettings'; +import { MemoryCache } from '../memoryCache'; +import { getCurrentHttpFileName, getWorkspaceRootPath } from '../workspaceUtility'; +import sanitizeHtml from 'sanitize-html'; + +type ServerAuthorizationCodeResponse = { + // Success case + code?: string; + client_info?: string; + state?: string; + cloud_instance_name?: string; + cloud_instance_host_name?: string; + cloud_graph_host_name?: string; + msgraph_host?: string; + // Error case + error?: string; + error_uri?: string; + error_description?: string; + suberror?: string; + timestamp?: string; + trace_id?: string; + correlation_id?: string; + claims?: string; + // Native Account ID + accountId?: string; +}; + +export class CodeLoopbackClient { + port: number = 0; // default port, which will be set to a random available port + private server!: http.Server | https.Server; + + private constructor(private callbackDomain: string, port: number = 0) { + this.port = port; + } + + /** + * Initializes a loopback server with an available port + * @param preferredPort + * @param logger + * @returns + */ + static async initialize(callbackDomain: string, preferredPort: number | undefined): Promise { + const loopbackClient = new CodeLoopbackClient(callbackDomain); + + if (preferredPort === 0 || preferredPort === undefined) { + return loopbackClient; + } + const isPortAvailable = await loopbackClient.isPortAvailable(preferredPort); + + if (isPortAvailable) { + loopbackClient.port = preferredPort; + } + + return loopbackClient; + } + + /** + * Spins up a loopback server which returns the server response when the localhost redirectUri is hit + * @param successTemplate + * @param errorTemplate + * @returns + */ + async listenForAuthCode(successTemplate?: string, errorTemplate?: string): Promise { + if (!!this.server) { + throw new Error('Loopback server already exists. Cannot create another.'); + } + + const authCodeListener = new Promise((resolve, reject) => { + const handler = async (req: http.IncomingMessage, res: http.ServerResponse) => { + const url = req.url; + if (!url) { + res.end(errorTemplate || "Error occurred loading redirectUrl"); + reject(new Error('Loopback server callback was invoked without a url. This is unexpected.')); + return; + } else if (url === "/") { + res.end(successTemplate || "Auth code was successfully acquired. You can close this window now."); + return; + } + + const authCodeResponse = CodeLoopbackClient.getDeserializedQueryString(url); + if (authCodeResponse.code) { + const redirectUri = await this.getRedirectUri(); + res.writeHead(302, { location: redirectUri }); // Prevent auth code from being saved in the browser history + res.end(); + } else { + res.end(`Authorization Server Error:${sanitizeHtml(JSON.stringify(authCodeResponse))}`); + reject(new Error(`Authorization Server Error:${JSON.stringify(authCodeResponse)}`)); + } + resolve({ url, ...authCodeResponse }); + }; + + const settings = SystemSettings.Instance as IRestClientSettings; + + try { + const certificates = this.getSslCertificate(settings); + if (certificates && (certificates.cert && certificates.key || certificates.pfx)) { + const options: SecureContextOptions = { + cert: certificates?.cert, + key: certificates?.key, + pfx: certificates?.pfx, + passphrase: certificates?.passphrase + }; + this.server = https.createServer(options, handler); + } else { + this.server = http.createServer(handler); + } + this.server.listen(this.port); + } catch (ex) { + reportError('Failed to start server', ex); + } + }); + + // Wait for server to be listening + await new Promise((resolve) => { + let ticks = 0; + const id = setInterval(() => { + if ((5000 / 100) < ticks) { + throw new Error('Timed out waiting for auth code listener to be registered.'); + } + + if (this.server.listening) { + clearInterval(id); + resolve(); + } + ticks++; + }, 100); + }); + + return authCodeListener; + } + + private getSslCertificate(settings: IRestClientSettings): SecureContextOptions | null { + const { cert: certPath, key: keyPath, pfx: pfxPath, passphrase } = settings.oidcCertificates[this.callbackDomain] ?? {}; + if (!certPath && !keyPath && !pfxPath && this.callbackDomain) { + reportError(`No certificates found for ${this.callbackDomain} in settings.`); + return null; + } + try { + const cert = this.resolveCertificate(certPath); + const key = this.resolveCertificate(keyPath); + const pfx = this.resolveCertificate(pfxPath); + return { cert, key, pfx, passphrase }; + } catch (ex) { + reportError(`Failed to load certificates from: {certPath:${certPath}} {keyPath:${keyPath}} {pfxPath:${pfxPath}}`, ex); + return null; + } + } + + private resolveCertificate(absoluteOrRelativePath: string | undefined): Buffer | undefined { + if (absoluteOrRelativePath === undefined) { + return undefined; + } + + if (path.isAbsolute(absoluteOrRelativePath)) { + if (!fs.existsSync(absoluteOrRelativePath)) { + reportError(`Certificate path ${absoluteOrRelativePath} doesn't exist, please make sure it exists.`); + return undefined; + } else { + return fs.readFileSync(absoluteOrRelativePath); + } + } + + // the path should be relative path + const rootPath = getWorkspaceRootPath(); + let absolutePath = ''; + if (rootPath) { + absolutePath = path.join(Uri.parse(rootPath).fsPath, absoluteOrRelativePath); + if (fs.existsSync(absolutePath)) { + return fs.readFileSync(absolutePath); + } else { + window.showWarningMessage(`Certificate path ${absoluteOrRelativePath} doesn't exist, please make sure it exists.`); + return undefined; + } + } + + const currentFilePath = getCurrentHttpFileName(); + if (!currentFilePath) { + return undefined; + } + + absolutePath = path.join(path.dirname(currentFilePath), absoluteOrRelativePath); + if (fs.existsSync(absolutePath)) { + return fs.readFileSync(absolutePath); + } else { + window.showWarningMessage(`Certificate path ${absoluteOrRelativePath} doesn't exist, please make sure it exists.`); + return undefined; + } + } + + /** + * Get the redirect uri for the loopback server + * @returns + */ + getRedirectUri(): string { + if (!this.server) { + throw new Error('No loopback server exists yet.'); + } + + const address = this.server.address(); + if (!address || typeof address === "string" || !address.port) { + this.closeServer(); + throw new Error('Loopback server address is not type string. This is unexpected.'); + } + + const port = address && address.port; + + return `${this.callbackDomain ? 'https' : 'http'}://${this.callbackDomain ?? 'localhost'}:${port}`; + } + + /** + * Close the loopback server + */ + closeServer(): void { + if (!!this.server) { + this.server.close(); + } + } + + /** + * Attempts to create a server and listen on a given port + * @param port + * @returns + */ + isPortAvailable(port: number): Promise { + return new Promise(resolve => { + const server = http.createServer() + .listen(port, () => { + server.close(); + resolve(true); + }) + .on("error", () => { + resolve(false); + }); + }); + } + + /** + * Returns URL query string as server auth code response object. + */ + static getDeserializedQueryString( + query: string + ): ServerAuthorizationCodeResponse { + // Check if given query is empty + if (!query) { + return {}; + } + // Strip the ? symbol if present + const parsedQueryString = this.parseQueryString(query); + // If ? symbol was not present, above will return empty string, so give original query value + const deserializedQueryString: ServerAuthorizationCodeResponse = + this.queryStringToObject( + parsedQueryString || query + ); + // Check if deserialization didn't work + if (!deserializedQueryString) { + throw "Unable to deserialize query string"; + } + return deserializedQueryString; + } + + /** + * Parses query string from given string. Returns empty string if no query symbol is found. + * @param queryString + */ + static parseQueryString(queryString: string): string { + const queryIndex1 = queryString.indexOf("?"); + const queryIndex2 = queryString.indexOf("/?"); + if (queryIndex2 > -1) { + return queryString.substring(queryIndex2 + 2); + } else if (queryIndex1 > -1) { + return queryString.substring(queryIndex1 + 1); + } + return ""; + } + /** + * Parses string into an object. + * + * @param query + */ + static queryStringToObject(query: string): ServerAuthorizationCodeResponse { + const obj: { [key: string]: string } = {}; + const params = query.split("&"); + const decode = (s: string) => decodeURIComponent(s.replace(/\+/g, " ")); + params.forEach((pair) => { + if (pair.trim()) { + const [key, value] = pair.split(/=(.+)/g, 2); // Split on the first occurence of the '=' character + if (key && value) { + obj[decode(key)] = decode(value); + } + } + }); + return obj as ServerAuthorizationCodeResponse; + } +} + +export const CALLBACK_PORT = 7777; + +export const remoteOutput = window.createOutputChannel('REST-OIDC'); + +const reportError = (msg: string, ex: Error | null = null ) => { + window.showWarningMessage(`Message: ${msg} Exception: ${ex?.message}`); + remoteOutput.appendLine(`Error: ${msg} Exception: ${ex?.message} Stack:${ex?.stack}`); +}; + +interface TokenInformation { + access_token: string; + refresh_token: string; +} + +export class OidcClient { + private _tokenInformation: TokenInformation | undefined; + private _pendingStates: string[] = []; + private _codeVerfifiers = new Map(); + private _scopes = new Map(); + + constructor(private clientId: string, + private callbackDomain: string, + private callbackPort: number, + private authorizeEndpoint: string, + private tokenEndpoint: string, + private scopes: string, + private audience: string, + ) { + } + + public static async getAccessToken(forceNew: boolean, clientId: string, callbackDomain: string, callbackPort: number, + authorizeEndpoint: string, + tokenEndpoint: string, + scopes: string, + audience: string): Promise { + const key = `${clientId}--${callbackDomain}-${callbackPort}-${authorizeEndpoint}-${tokenEndpoint}-${scopes}-${audience}`; + const cache = MemoryCache.createOrGet('oidc'); + + const client = cache.get(key) ?? new OidcClient(clientId, callbackDomain, callbackPort, authorizeEndpoint, tokenEndpoint, scopes, audience); + cache.set(key, client); + if (forceNew) { + client.cleanupTokenCache(); + } + + return client.getAccessToken(); + } + + public cleanupTokenCache() { + this._tokenInformation = undefined; + } + + get redirectUri() { + return `${this.callbackDomain ? 'https' : 'http'}://${this.callbackDomain ?? 'localhost'}:${this.callbackPort}`; + } + + public async getAccessToken(): Promise { + const tryDecode = (token: string): any => { + try { + const { payload } = jws.decode(token) ?? {}; + return JSON.parse(payload); + } catch (ex) { + reportError('Faild to decode access token', ex); + return null; + } + }; + if (this._tokenInformation?.access_token) { + const payloadJson = tryDecode(this._tokenInformation.access_token); + if (payloadJson === null || payloadJson.exp && payloadJson.exp > Date.now() / 1000) { + return this._tokenInformation.access_token; + } else { + return this.getAccessTokenByRefreshToken(this._tokenInformation.refresh_token, this.clientId).then((resp) => { + this._tokenInformation = resp; + return resp.access_token; + }); + } + } + + const nonceId = uuid(); + + // Retrieve all required scopes + const scopes = this.getScopes((this.scopes ?? "").split(',')); + + const codeVerifier = toBase64UrlEncoding(crypto.randomBytes(32)); + const codeChallenge = toBase64UrlEncoding(sha256(codeVerifier)); + + let callbackUri = await env.asExternalUri(Uri.parse(this.redirectUri)); + + remoteOutput.appendLine(`Callback URI: ${callbackUri.toString(true)}`); + + const callbackQuery = new URLSearchParams(callbackUri.query); + const stateId = callbackQuery.get('state') || nonceId; + + remoteOutput.appendLine(`State ID: ${stateId}`); + remoteOutput.appendLine(`Nonce ID: ${nonceId}`); + + callbackQuery.set('state', encodeURIComponent(stateId)); + callbackQuery.set('nonce', encodeURIComponent(nonceId)); + callbackUri = callbackUri.with({ + query: callbackQuery.toString() + }); + + this._pendingStates.push(stateId); + this._codeVerfifiers.set(stateId, codeVerifier); + this._scopes.set(stateId, scopes); + + const params = [ + ['response_type', "code"], + ['client_id', this.clientId], + ['redirect_uri', this.redirectUri], + ['state', stateId], + ['scope', scopes.join(' ')], + ['prompt', "login"], + ['code_challenge_method', 'S256'], + ['code_challenge', codeChallenge], + ]; + + if (this.audience) { + params.push(['resource', this.audience]); + } + + const searchParams = new URLSearchParams(params as [string, string][]); + + const uri = Uri.parse(`${this.authorizeEndpoint}?${searchParams.toString()}`); + + remoteOutput.appendLine(`Login URI: ${uri.toString(true)}`); + + const loopbackClient = await CodeLoopbackClient.initialize(this.callbackDomain, this.callbackPort); + + try { + await env.openExternal(uri); + const callBackResp = await loopbackClient.listenForAuthCode(); + const codeExchangePromise = this._handleCallback(Uri.parse(callBackResp.url)); + + const resp = await Promise.race([ + codeExchangePromise, + new Promise((_, reject) => setTimeout(() => reject('Cancelled'), 60000)) + ]); + this._tokenInformation = resp as TokenInformation; + return resp?.access_token; + } finally { + loopbackClient.closeServer(); + this._pendingStates = this._pendingStates.filter(n => n !== stateId); + this._codeVerfifiers.delete(stateId); + this._scopes.delete(stateId); + } + } + + private async getAccessTokenByRefreshToken(refreshToken: string, clientId: string): Promise { + const postData = new URLSearchParams({ + grant_type: 'refresh_token', + client_id: clientId, + refresh_token: refreshToken + }).toString(); + + const response = await fetch(this.tokenEndpoint, { + method: 'POST', + headers: { + "Content-Type": "application/x-www-form-urlencoded", + 'Content-Length': postData.length.toString() + }, + body: postData + }); + + if (response.status !== 200) { + const error = await response.json(); + throw new Error(`Failed to retrieve access token: ${response.status} ${JSON.stringify(error)}`); + } + + const { access_token, refresh_token } = await response.json(); + + + return { access_token, refresh_token }; + } + + private async _handleCallback(uri: Uri): Promise { + const query = new URLSearchParams(uri.query); + const code = query.get('code'); + const stateId = query.get('state'); + + if (!code) { + throw new Error('No code'); + + } + if (!stateId) { + throw new Error('No state'); + + } + + const codeVerifier = this._codeVerfifiers.get(stateId); + if (!codeVerifier) { + throw new Error('No code verifier'); + } + + // Check if it is a valid auth request started by the extension + if (!this._pendingStates.some(n => n === stateId)) { + throw new Error('State not found'); + } + + const postData = new URLSearchParams({ + grant_type: 'authorization_code', + client_id: this.clientId, + code, + code_verifier: codeVerifier, + redirect_uri: this.redirectUri, + }).toString(); + try { + const response = await fetch(`${this.tokenEndpoint}`, { + method: 'POST', + headers: { + "Content-Type": "application/x-www-form-urlencoded", + 'Content-Length': postData.length.toString() + }, + body: postData + }); + const json = await response.json(); + const { access_token, refresh_token } = json; + if (!access_token) { + reportError(`Failed to retrieve access token: ${response.status} ${JSON.stringify(json)}`); + } + + return { access_token, refresh_token }; + } catch (ex) { + reportError('Failed to retrieve access token', ex); + return undefined; + } + } + + /** + * Get all required scopes + * @param scopes + */ + private getScopes(scopes: string[] = []): string[] { + const settings = SystemSettings.Instance as IRestClientSettings; + + const modifiedScopes = [...(settings.oidcScopes ?? []), ...scopes]; + return Array.from(new Set(modifiedScopes.sort())); + } +} + +export function toBase64UrlEncoding(buffer: Buffer) { + return buffer.toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); +} + +export function sha256(buffer: string | Uint8Array): Buffer { + return crypto.createHash('sha256').update(buffer).digest(); +} diff --git a/src/utils/httpElementFactory.ts b/src/utils/httpElementFactory.ts index f7aefccf..56690fec 100644 --- a/src/utils/httpElementFactory.ts +++ b/src/utils/httpElementFactory.ts @@ -138,6 +138,12 @@ export class HttpElementFactory { null, Constants.AzureActiveDirectoryDescription, new SnippetString(`{{$\${name:${Constants.AzureActiveDirectoryVariableName.slice(1)}}}}`))); + originalElements.push(new HttpElement( + Constants.OidcVariableName, + ElementType.SystemVariable, + null, + Constants.OidcDescription, + new SnippetString(`{{$\${name:${Constants.OidcVariableName.slice(1)}}}}`))); originalElements.push(new HttpElement( Constants.AzureActiveDirectoryV2TokenVariableName, ElementType.SystemVariable, diff --git a/src/utils/httpVariableProviders/systemVariableProvider.ts b/src/utils/httpVariableProviders/systemVariableProvider.ts index 0cc11733..bfe380c9 100644 --- a/src/utils/httpVariableProviders/systemVariableProvider.ts +++ b/src/utils/httpVariableProviders/systemVariableProvider.ts @@ -12,6 +12,7 @@ import { ResolveErrorMessage, ResolveWarningMessage } from '../../models/httpVar import { VariableType } from '../../models/variableType'; import { AadTokenCache } from '../aadTokenCache'; import { AadV2TokenProvider } from '../aadV2TokenProvider'; +import { CALLBACK_PORT, OidcClient } from '../auth/oidcClient'; import { HttpClient } from '../httpClient'; import { EnvironmentVariableProvider } from './environmentVariableProvider'; import { HttpVariable, HttpVariableContext, HttpVariableProvider } from './httpVariableProvider'; @@ -38,6 +39,7 @@ export class SystemVariableProvider implements HttpVariableProvider { private readonly requestUrlRegex: RegExp = /^(?:[^\s]+\s+)([^:]*:\/\/\/?[^/\s]*\/?)/; private readonly aadRegex: RegExp = new RegExp(`\\s*\\${Constants.AzureActiveDirectoryVariableName}(\\s+(${Constants.AzureActiveDirectoryForceNewOption}))?(\\s+(ppe|public|cn|de|us))?(\\s+([^\\.]+\\.[^\\}\\s]+|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}))?(\\s+aud:([^\\.]+\\.[^\\}\\s]+|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}))?\\s*`); + private readonly oidcRegex: RegExp = new RegExp(`\\s*(\\${Constants.OidcVariableName})(?:\\s+(${Constants.OIdcForceNewOption}))?(?:\\s*clientId:([\\w|.|:|/|_|-]+))?(?:\\s*issuer:([\\w|.|:|/]+))?(?:\\s*callbackDomain:([\\w|.|:|/|_|-]+))?(?:\\s*callbackPort:([\\w|_]+))?(?:\\s*authorizeEndpoint:([\\w|.|:|/|_|-]+))?(?:\\s*tokenEndpoint:([\\w|.|:|/|_|-]+))?(?:\\s*scopes:([\\w|.|:|/|_|-]+))?(?:\\s*audience:([\\w|.|:|/|_|-]+))?`); private readonly innerSettingsEnvironmentVariableProvider: EnvironmentVariableProvider = EnvironmentVariableProvider.Instance; private static _instance: SystemVariableProvider; @@ -60,6 +62,7 @@ export class SystemVariableProvider implements HttpVariableProvider { this.registerProcessEnvVariable(); this.registerDotenvVariable(); this.registerAadTokenVariable(); + this.registerOidcTokenVariable(); this.registerAadV2TokenVariable(); } @@ -292,6 +295,17 @@ export class SystemVariableProvider implements HttpVariableProvider { }); } + private registerOidcTokenVariable() { + this.resolveFuncs.set(Constants.OidcVariableName, async (name, document, context) => { + const matchVar = this.oidcRegex.exec(name) ?? []; + const [_, _1, forceNew, clientId, _3, callbackDomain, callbackPort, authorizeEndpoint, tokenEndpoint, scopes, audience] = matchVar; + + const access_token = await OidcClient.getAccessToken(forceNew ? true : false, clientId, callbackDomain, parseInt(callbackPort ?? CALLBACK_PORT), authorizeEndpoint, tokenEndpoint, scopes, audience); + await this.clipboard.writeText(access_token ?? ""); + return { value: access_token ?? "" }; + }); + } + private registerAadV2TokenVariable() { this.resolveFuncs.set(Constants.AzureActiveDirectoryV2TokenVariableName, async (name) => { diff --git a/src/utils/memoryCache.ts b/src/utils/memoryCache.ts new file mode 100644 index 00000000..2efab8ad --- /dev/null +++ b/src/utils/memoryCache.ts @@ -0,0 +1,30 @@ +export class OidcPayload { + public access_token: string; + public refresh_token: string; +} + +export class MemoryCache { + private cache = new Map(); + private static caches = new Map>(); + + public static createOrGet(name: string): MemoryCache { + if (!this.caches.has(name)) { + const cache = new MemoryCache(); + this.caches.set(name, cache); + return cache; + } + return this.caches.get(name) as MemoryCache; + } + + public get(key: string): T | undefined { + return this.cache.get(key); + } + + public set(key: string, value: T) { + this.cache.set(key, value); + } + + public clear(): void { + this.cache.clear(); + } +} \ No newline at end of file