diff --git a/package-lock.json b/package-lock.json index ef720728a..f3b6e097e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -794,7 +793,6 @@ "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -803,7 +801,6 @@ "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", - "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", @@ -870,7 +867,6 @@ "version": "7.23.6", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", - "dev": true, "dependencies": { "@babel/compat-data": "^7.23.5", "@babel/helper-validator-option": "^7.23.5", @@ -886,7 +882,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, "dependencies": { "yallist": "^3.0.2" } @@ -894,8 +889,7 @@ "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.23.10", @@ -1011,7 +1005,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", - "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-module-imports": "^7.22.15", @@ -1084,7 +1077,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dev": true, "dependencies": { "@babel/types": "^7.22.5" }, @@ -1135,7 +1127,6 @@ "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -1158,7 +1149,6 @@ "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", - "dev": true, "dependencies": { "@babel/template": "^7.23.9", "@babel/traverse": "^7.23.9", @@ -4529,6 +4519,23 @@ "node": ">=12" } }, + "node_modules/@n8n/vm2": { + "version": "3.9.23", + "resolved": "https://registry.npmjs.org/@n8n/vm2/-/vm2-3.9.23.tgz", + "integrity": "sha512-yu+It+L89uljQsCJ2e9cQaXzoXJe9bU69QQIoWUOcUw0u5Zon37DuB7bdNNsjKS1ZdFD+fBWCQpq/FkqHsSjXQ==", + "peer": true, + "dependencies": { + "acorn": "^8.7.0", + "acorn-walk": "^8.2.0" + }, + "bin": { + "vm2": "bin/vm2" + }, + "engines": { + "node": ">=18.10", + "pnpm": ">=8.6.12" + } + }, "node_modules/@next/env": { "version": "12.3.3", "resolved": "https://registry.npmjs.org/@next/env/-/env-12.3.3.tgz", @@ -6309,6 +6316,17 @@ "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/acorn-import-assertions": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", @@ -6318,6 +6336,14 @@ "acorn": "^8" } }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", @@ -7401,7 +7427,6 @@ "version": "4.22.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz", "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==", - "dev": true, "funding": [ { "type": "opencollective", @@ -7522,6 +7547,11 @@ "node": ">=0.4.0" } }, + "node_modules/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": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-fill": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", @@ -8428,8 +8458,7 @@ "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "node_modules/cookie": { "version": "0.6.0", @@ -9438,6 +9467,14 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, + "node_modules/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==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -9691,8 +9728,7 @@ "node_modules/electron-to-chromium": { "version": "1.4.667", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.667.tgz", - "integrity": "sha512-66L3pLlWhTNVUhnmSA5+qDM3fwnXsM6KAqE36e2w4KN0g6pkEtlT5bs41FQtQwVwKnfhNBXiWRLPs30HSxd7Kw==", - "dev": true + "integrity": "sha512-66L3pLlWhTNVUhnmSA5+qDM3fwnXsM6KAqE36e2w4KN0g6pkEtlT5bs41FQtQwVwKnfhNBXiWRLPs30HSxd7Kw==" }, "node_modules/electron-util": { "version": "0.17.2", @@ -10816,7 +10852,6 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -13661,6 +13696,41 @@ "node": ">=12.0.0" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "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": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jsprim": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", @@ -13701,6 +13771,25 @@ "node": "*" } }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kew": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz", @@ -13955,12 +14044,47 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -14694,8 +14818,7 @@ "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, "node_modules/node-vault": { "version": "0.10.2", @@ -17947,7 +18070,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "devOptional": true, "bin": { "semver": "bin/semver.js" } @@ -19204,18 +19326,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/terser/node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -19727,7 +19837,6 @@ "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -20024,25 +20133,6 @@ "node": ">=6.0" } }, - "node_modules/vm2/node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/vm2/node_modules/acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/vscode-languageserver-types": { "version": "3.17.5", "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", @@ -20242,18 +20332,6 @@ "node": ">=10.13.0" } }, - "node_modules/webpack/node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/webpack/node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -20986,6 +21064,7 @@ "dependencies": { "@usebruno/query": "0.1.0", "ajv": "^8.12.0", + "ajv-formats": "^2.1.1", "atob": "^2.1.2", "axios": "^1.5.1", "btoa": "^1.2.1", @@ -21073,6 +21152,7 @@ "express-xml-bodyparser": "^0.3.0", "http-proxy": "^1.18.1", "js-yaml": "^4.1.0", + "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", "multer": "^1.4.5-lts.1" } diff --git a/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js b/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js index 1ba47582d..fe13e31f0 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js @@ -80,6 +80,15 @@ const AuthMode = ({ item, collection }) => { > Inherit +
{ + dropdownTippyRef?.current?.hide(); + onModeChange('oauth2'); + }} + > + OAuth 2.0 +
{ diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/StyledWrapper.js new file mode 100644 index 000000000..e4d6a7d6c --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/StyledWrapper.js @@ -0,0 +1,15 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div` + label { + font-size: 0.8125rem; + } + .single-line-editor-wrapper { + padding: 0.15rem 0.4rem; + border-radius: 3px; + border: solid 1px ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.input.bg}; + } +`; + +export default Wrapper; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js new file mode 100644 index 000000000..08ac8dab1 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js @@ -0,0 +1,65 @@ +import React from 'react'; +import get from 'lodash/get'; +import { useTheme } from 'providers/Theme'; +import { useDispatch } from 'react-redux'; +import SingleLineEditor from 'components/SingleLineEditor'; +import { updateAuth } from 'providers/ReduxStore/slices/collections'; +import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; +import StyledWrapper from './StyledWrapper'; +import { inputsConfig } from './inputsConfig'; + +const OAuth2AuthorizationCode = ({ item, collection }) => { + const dispatch = useDispatch(); + const { storedTheme } = useTheme(); + + const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {}); + + const handleRun = async () => { + dispatch(sendRequest(item, collection.uid)); + }; + + const handleSave = () => dispatch(saveRequest(item.uid, collection.uid)); + + const handleChange = (key, value) => { + dispatch( + updateAuth({ + mode: 'oauth2', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + grantType: 'authorization_code', + ...oAuth, + [key]: value + } + }) + ); + }; + + return ( + + {inputsConfig.map((input) => { + const { key, label } = input; + return ( +
+ +
+ handleChange(key, val)} + onRun={() => {}} + collection={collection} + /> +
+
+ ); + })} + +
+ ); +}; + +export default OAuth2AuthorizationCode; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/inputsConfig.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/inputsConfig.js new file mode 100644 index 000000000..a1f3f3d45 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/inputsConfig.js @@ -0,0 +1,28 @@ +const inputsConfig = [ + { + key: 'callbackUrl', + label: 'Callback URL' + }, + { + key: 'authorizationUrl', + label: 'Auth URL' + }, + { + key: 'accessTokenUrl', + label: 'Access Token URL' + }, + { + key: 'clientId', + label: 'Client ID' + }, + { + key: 'clientSecret', + label: 'Client Secret' + }, + { + key: 'scope', + label: 'Scope' + } +]; + +export { inputsConfig }; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/StyledWrapper.js new file mode 100644 index 000000000..e4d6a7d6c --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/StyledWrapper.js @@ -0,0 +1,15 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div` + label { + font-size: 0.8125rem; + } + .single-line-editor-wrapper { + padding: 0.15rem 0.4rem; + border-radius: 3px; + border: solid 1px ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.input.bg}; + } +`; + +export default Wrapper; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js new file mode 100644 index 000000000..b8c460cac --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js @@ -0,0 +1,78 @@ +import React from 'react'; +import get from 'lodash/get'; +import { useTheme } from 'providers/Theme'; +import { useDispatch } from 'react-redux'; +import SingleLineEditor from 'components/SingleLineEditor'; +import { updateAuth } from 'providers/ReduxStore/slices/collections'; +import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; +import StyledWrapper from './StyledWrapper'; + +const OAuth2ClientCredentials = ({ item, collection }) => { + const dispatch = useDispatch(); + const { storedTheme } = useTheme(); + + const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {}); + + const handleRun = () => dispatch(sendRequest(item, collection.uid)); + const handleSave = () => dispatch(saveRequest(item.uid, collection.uid)); + + const handleClientIdChange = (clientId) => { + dispatch( + updateAuth({ + mode: 'oauth2', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + grantType: 'client_credentials', + clientId: clientId, + clientSecret: oAuth.clientSecret + } + }) + ); + }; + + const handleClientSecretChange = (clientSecret) => { + dispatch( + updateAuth({ + mode: 'oauth2', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + grantType: 'client_credentials', + clientId: oAuth.clientId, + clientSecret: clientSecret + } + }) + ); + }; + + return ( + + +
+ handleClientIdChange(val)} + onRun={handleRun} + collection={collection} + /> +
+ + +
+ handleClientSecretChange(val)} + onRun={handleRun} + collection={collection} + /> +
+
+ ); +}; + +export default OAuth2ClientCredentials; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/GrantTypeSelector/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/GrantTypeSelector/StyledWrapper.js new file mode 100644 index 000000000..4423a49c1 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/GrantTypeSelector/StyledWrapper.js @@ -0,0 +1,54 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div` + font-size: 0.8125rem; + + .grant-type-mode-selector { + padding: 0.5rem 0px; + border-radius: 3px; + border: solid 1px ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.input.bg}; + + .dropdown { + width: 100%; + + div[data-tippy-root] { + width: 100%; + } + .tippy-box { + width: 100%; + max-width: none !important; + + .tippy-content: { + width: 100%; + max-width: none !important; + } + } + } + + .grant-type-label { + width: 100%; + color: ${(props) => props.theme.colors.text.yellow}; + justify-content: space-between; + padding: 0 0.5rem; + } + + .dropdown-item { + padding: 0.2rem 0.6rem !important; + } + + .label-item { + padding: 0.2rem 0.6rem !important; + } + } + + .caret { + color: rgb(140, 140, 140); + fill: rgb(140 140 140); + } + label { + font-size: 0.8125rem; + } +`; + +export default Wrapper; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/GrantTypeSelector/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/GrantTypeSelector/index.js new file mode 100644 index 000000000..06a9b5e31 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/GrantTypeSelector/index.js @@ -0,0 +1,75 @@ +import React, { useRef, forwardRef } from 'react'; +import get from 'lodash/get'; +import Dropdown from 'components/Dropdown'; +import { useDispatch } from 'react-redux'; +import StyledWrapper from './StyledWrapper'; +import { IconCaretDown } from '@tabler/icons'; +import { updateAuth } from 'providers/ReduxStore/slices/collections'; +import { humanizeGrantType } from 'utils/collections'; + +const GrantTypeSelector = ({ item, collection }) => { + const dispatch = useDispatch(); + const dropdownTippyRef = useRef(); + const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); + + const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {}); + + const Icon = forwardRef((props, ref) => { + return ( +
+ {humanizeGrantType(oAuth?.grantType)} +
+ ); + }); + + const onGrantTypeChange = (grantType) => { + dispatch( + updateAuth({ + mode: 'oauth2', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + grantType + } + }) + ); + }; + + return ( + + +
+ } placement="bottom-end"> +
{ + dropdownTippyRef.current.hide(); + onGrantTypeChange('password'); + }} + > + Resource Owner Password Credentials +
+
{ + dropdownTippyRef.current.hide(); + onGrantTypeChange('authorization_code'); + }} + > + Authorization Code +
+
{ + dropdownTippyRef.current.hide(); + onGrantTypeChange('client_credentials'); + }} + > + Client Credentials +
+
+
+
+ ); +}; +export default GrantTypeSelector; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Ropc/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Ropc/StyledWrapper.js new file mode 100644 index 000000000..e4d6a7d6c --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Ropc/StyledWrapper.js @@ -0,0 +1,15 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div` + label { + font-size: 0.8125rem; + } + .single-line-editor-wrapper { + padding: 0.15rem 0.4rem; + border-radius: 3px; + border: solid 1px ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.input.bg}; + } +`; + +export default Wrapper; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Ropc/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Ropc/index.js new file mode 100644 index 000000000..104ebdd77 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Ropc/index.js @@ -0,0 +1,78 @@ +import React from 'react'; +import get from 'lodash/get'; +import { useTheme } from 'providers/Theme'; +import { useDispatch } from 'react-redux'; +import SingleLineEditor from 'components/SingleLineEditor'; +import { updateAuth } from 'providers/ReduxStore/slices/collections'; +import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; +import StyledWrapper from './StyledWrapper'; + +const OAuth2Ropc = ({ item, collection }) => { + const dispatch = useDispatch(); + const { storedTheme } = useTheme(); + + const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {}); + + const handleRun = () => dispatch(sendRequest(item, collection.uid)); + const handleSave = () => dispatch(saveRequest(item.uid, collection.uid)); + + const handleUsernameChange = (username) => { + dispatch( + updateAuth({ + mode: 'oauth2', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + grantType: 'password', + username: username, + password: oAuth.password + } + }) + ); + }; + + const handlePasswordChange = (password) => { + dispatch( + updateAuth({ + mode: 'oauth2', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + grantType: 'password', + username: oAuth.username, + password: password + } + }) + ); + }; + + return ( + + +
+ handleUsernameChange(val)} + onRun={handleRun} + collection={collection} + /> +
+ + +
+ handlePasswordChange(val)} + onRun={handleRun} + collection={collection} + /> +
+
+ ); +}; + +export default OAuth2Ropc; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/StyledWrapper.js new file mode 100644 index 000000000..e4d6a7d6c --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/StyledWrapper.js @@ -0,0 +1,15 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div` + label { + font-size: 0.8125rem; + } + .single-line-editor-wrapper { + padding: 0.15rem 0.4rem; + border-radius: 3px; + border: solid 1px ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.input.bg}; + } +`; + +export default Wrapper; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/index.js new file mode 100644 index 000000000..1d51962a5 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/index.js @@ -0,0 +1,37 @@ +import React from 'react'; +import get from 'lodash/get'; +import StyledWrapper from './StyledWrapper'; +import GrantTypeSelector from './GrantTypeSelector/index'; +import OAuth2Ropc from './Ropc/index'; +import OAuth2AuthorizationCode from './AuthorizationCode/index'; +import OAuth2ClientCredentials from './ClientCredentials/index'; + +const grantTypeComponentMap = (grantType, item, collection) => { + switch (grantType) { + case 'password': + return ; + break; + case 'authorization_code': + return ; + break; + case 'client_credentials': + return ; + break; + default: + return
TBD
; + break; + } +}; + +const OAuth2 = ({ item, collection }) => { + const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {}); + + return ( + + + {grantTypeComponentMap(oAuth?.grantType, item, collection)} + + ); +}; + +export default OAuth2; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/index.js index b13c6b097..730facf69 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/index.js @@ -7,6 +7,7 @@ import BasicAuth from './BasicAuth'; import DigestAuth from './DigestAuth'; import StyledWrapper from './StyledWrapper'; import { humanizeRequestAuthMode } from 'utils/collections/index'; +import OAuth2 from './OAuth2/index'; const Auth = ({ item, collection }) => { const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode'); @@ -28,6 +29,9 @@ const Auth = ({ item, collection }) => { case 'digest': { return ; } + case 'oauth2': { + return ; + } case 'inherit': { return (
@@ -36,7 +40,6 @@ const Auth = ({ item, collection }) => {
); } - } }; return ( diff --git a/packages/bruno-app/src/components/RunnerResults/index.jsx b/packages/bruno-app/src/components/RunnerResults/index.jsx index e5e46b3da..aba8f785a 100644 --- a/packages/bruno-app/src/components/RunnerResults/index.jsx +++ b/packages/bruno-app/src/components/RunnerResults/index.jsx @@ -140,12 +140,11 @@ export default function RunnerResults({ collection }) { )}
-
-
+
+
Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length}
@@ -235,8 +234,8 @@ export default function RunnerResults({ collection }) {
) : null}
-
- {selectedItem ? ( + {selectedItem ? ( +
{selectedItem.relativePath} @@ -251,8 +250,8 @@ export default function RunnerResults({ collection }) { {/*
{selectedItem.relativePath}
*/}
- ) : null} -
+
+ ) : null}
); diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index a5f9100c0..41b284149 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -402,6 +402,10 @@ export const collectionsSlice = createSlice({ item.draft.request.auth.mode = 'digest'; item.draft.request.auth.digest = action.payload.content; break; + case 'oauth2': + item.draft.request.auth.mode = 'oauth2'; + item.draft.request.auth.oauth2 = action.payload.content; + break; } } } diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index 92f85a099..a77a789d4 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -509,6 +509,30 @@ export const humanizeRequestAuthMode = (mode) => { label = 'Digest Auth'; break; } + case 'oauth2': { + label = 'OAuth 2.0'; + break; + } + } + + return label; +}; + +export const humanizeGrantType = (mode) => { + let label = 'No Auth'; + switch (mode) { + case 'password': { + label = 'Resource Owner Password Credentials'; + break; + } + case 'authorization_code': { + label = 'Authorization Code'; + break; + } + case 'client_credentials': { + label = 'Client Credentials'; + break; + } } return label; diff --git a/packages/bruno-cli/package.json b/packages/bruno-cli/package.json index 5949ba1d2..d90146976 100644 --- a/packages/bruno-cli/package.json +++ b/packages/bruno-cli/package.json @@ -1,6 +1,6 @@ { "name": "@usebruno/cli", - "version": "1.9.0", + "version": "1.9.2", "license": "MIT", "main": "src/index.js", "bin": { @@ -24,9 +24,11 @@ "package.json" ], "dependencies": { + "@aws-sdk/credential-providers": "^3.425.0", "@usebruno/common": "0.1.0", "@usebruno/js": "0.10.1", "@usebruno/lang": "0.10.0", + "aws4-axios": "^3.3.0", "axios": "^1.5.1", "chai": "^4.3.7", "chalk": "^3.0.0", diff --git a/packages/bruno-cli/src/runner/awsv4auth-helper.js b/packages/bruno-cli/src/runner/awsv4auth-helper.js new file mode 100644 index 000000000..4a2ff5aa2 --- /dev/null +++ b/packages/bruno-cli/src/runner/awsv4auth-helper.js @@ -0,0 +1,56 @@ +const { fromIni } = require('@aws-sdk/credential-providers'); +const { aws4Interceptor } = require('aws4-axios'); + +function isStrPresent(str) { + return str && str !== '' && str !== 'undefined'; +} + +async function resolveAwsV4Credentials(request) { + const awsv4 = request.awsv4config; + if (isStrPresent(awsv4.profileName)) { + try { + credentialsProvider = fromIni({ + profile: awsv4.profileName + }); + credentials = await credentialsProvider(); + awsv4.accessKeyId = credentials.accessKeyId; + awsv4.secretAccessKey = credentials.secretAccessKey; + awsv4.sessionToken = credentials.sessionToken; + } catch { + console.error('Failed to fetch credentials from AWS profile.'); + } + } + return awsv4; +} + +function addAwsV4Interceptor(axiosInstance, request) { + if (!request.awsv4config) { + console.warn('No Auth Config found!'); + return; + } + + const awsv4 = request.awsv4config; + if (!isStrPresent(awsv4.accessKeyId) || !isStrPresent(awsv4.secretAccessKey)) { + console.warn('Required Auth Fields are not present'); + return; + } + + const interceptor = aws4Interceptor({ + options: { + region: awsv4.region, + service: awsv4.service + }, + credentials: { + accessKeyId: awsv4.accessKeyId, + secretAccessKey: awsv4.secretAccessKey, + sessionToken: awsv4.sessionToken + } + }); + + axiosInstance.interceptors.request.use(interceptor); +} + +module.exports = { + addAwsV4Interceptor, + resolveAwsV4Credentials +}; diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js index 3a45575e5..c2e5b13f7 100644 --- a/packages/bruno-cli/src/runner/prepare-request.js +++ b/packages/bruno-cli/src/runner/prepare-request.js @@ -57,6 +57,17 @@ const prepareRequest = (request, collectionRoot) => { }; } + if (request.auth.mode === 'awsv4') { + axiosRequest.awsv4config = { + accessKeyId: get(request, 'auth.awsv4.accessKeyId'), + secretAccessKey: get(request, 'auth.awsv4.secretAccessKey'), + sessionToken: get(request, 'auth.awsv4.sessionToken'), + service: get(request, 'auth.awsv4.service'), + region: get(request, 'auth.awsv4.region'), + profileName: get(request, 'auth.awsv4.profileName') + }; + } + if (request.auth.mode === 'bearer') { axiosRequest.headers['authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`; } diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index e30a667c2..ec4767efb 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -15,6 +15,7 @@ const https = require('https'); const { HttpProxyAgent } = require('http-proxy-agent'); const { SocksProxyAgent } = require('socks-proxy-agent'); const { makeAxiosInstance } = require('../utils/axios-instance'); +const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper'); const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util'); const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/; @@ -190,6 +191,24 @@ const runSingleRequest = async function ( // run request const axiosInstance = makeAxiosInstance(); + if (request.awsv4config) { + // todo: make this happen in prepare-request.js + // interpolate the aws v4 config + request.awsv4config.accessKeyId = interpolateString(request.awsv4config.accessKeyId, interpolationOptions); + request.awsv4config.secretAccessKey = interpolateString( + request.awsv4config.secretAccessKey, + interpolationOptions + ); + request.awsv4config.sessionToken = interpolateString(request.awsv4config.sessionToken, interpolationOptions); + request.awsv4config.service = interpolateString(request.awsv4config.service, interpolationOptions); + request.awsv4config.region = interpolateString(request.awsv4config.region, interpolationOptions); + request.awsv4config.profileName = interpolateString(request.awsv4config.profileName, interpolationOptions); + + request.awsv4config = await resolveAwsV4Credentials(request); + addAwsV4Interceptor(axiosInstance, request); + delete request.awsv4config; + } + /** @type {import('axios').AxiosResponse} */ response = await axiosInstance(request); diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js index ef0e141dc..52afec0a8 100644 --- a/packages/bruno-electron/src/index.js +++ b/packages/bruno-electron/src/index.js @@ -21,7 +21,8 @@ const contentSecurityPolicy = [ "script-src * 'unsafe-inline' 'unsafe-eval'", "connect-src * 'unsafe-inline'", "font-src 'self' https:", - "form-action 'none'", + // this has been commented out to make oauth2 work + // "form-action 'none'", "img-src 'self' blob: data: https:", "style-src 'self' 'unsafe-inline' https:" ]; diff --git a/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js b/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js new file mode 100644 index 000000000..e4439f612 --- /dev/null +++ b/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js @@ -0,0 +1,61 @@ +const { BrowserWindow } = require('electron'); + +const authorizeUserInWindow = ({ authorizeUrl, callbackUrl }) => { + return new Promise(async (resolve, reject) => { + let finalUrl = null; + + const window = new BrowserWindow({ + webPreferences: { + nodeIntegration: false + }, + show: false + }); + window.on('ready-to-show', window.show.bind(window)); + + function onWindowRedirect(url) { + // check if the url contains an authorization code + if (url.match(/(code=).*/)) { + finalUrl = url; + if (url && finalUrl.includes(callbackUrl)) { + window.close(); + } else { + reject(new Error('Invalid Callback Url')); + } + } + } + + window.on('close', () => { + if (finalUrl) { + try { + const callbackUrlWithCode = new URL(finalUrl); + const authorizationCode = callbackUrlWithCode.searchParams.get('code'); + + return resolve(authorizationCode); + } catch (error) { + return reject(error); + } + } else { + return reject(new Error('Authorization window closed')); + } + }); + + // wait for the window to navigate to the callback url + const didNavigateListener = (_, url) => { + onWindowRedirect(url); + }; + window.webContents.on('did-navigate', didNavigateListener); + const willRedirectListener = (_, authorizeUrl) => { + onWindowRedirect(authorizeUrl); + }; + window.webContents.on('will-redirect', willRedirectListener); + + try { + await window.loadURL(authorizeUrl); + } catch (error) { + reject(error); + window.close(); + } + }); +}; + +module.exports = { authorizeUserInWindow }; diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index b068193c7..3446d5256 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -9,7 +9,7 @@ const Mustache = require('mustache'); const contentDispositionParser = require('content-disposition'); const mime = require('mime-types'); const { ipcMain } = require('electron'); -const { isUndefined, isNull, each, get, compact } = require('lodash'); +const { isUndefined, isNull, each, get, compact, cloneDeep } = require('lodash'); const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js'); const prepareRequest = require('./prepare-request'); const prepareGqlIntrospectionRequest = require('./prepare-gql-introspection-request'); @@ -29,6 +29,7 @@ const { addDigestInterceptor } = require('./digestauth-helper'); const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../../utils/proxy-util'); const { chooseFileToSave, writeBinaryFile } = require('../../utils/filesystem'); const { getCookieStringForUrl, addCookieToJar, getDomainsWithCookies } = require('../../utils/cookies'); +const { resolveOAuth2AuthorizationCodecessToken } = require('./oauth2-authorization-code-helper'); // override the default escape function to prevent escaping Mustache.escape = function (value) { @@ -189,6 +190,16 @@ const configureRequest = async ( const axiosInstance = makeAxiosInstance(); + if (request.oauth2) { + if (request?.oauth2?.grantType == 'authorization_code') { + let requestCopy = cloneDeep(request); + interpolateVars(requestCopy, envVars, collectionVariables, processEnvVars); + const { data, url } = await resolveOAuth2AuthorizationCodecessToken(requestCopy); + request.data = data; + request.url = url; + } + } + if (request.awsv4config) { request.awsv4config = await resolveAwsV4Credentials(request); addAwsV4Interceptor(axiosInstance, request); @@ -484,7 +495,6 @@ const registerNetworkIpc = (mainWindow) => { setCookieHeaders = Array.isArray(response.headers['set-cookie']) ? response.headers['set-cookie'] : [response.headers['set-cookie']]; - for (let setCookieHeader of setCookieHeaders) { if (typeof setCookieHeader === 'string' && setCookieHeader.length) { addCookieToJar(setCookieHeader, request.url); @@ -495,6 +505,7 @@ const registerNetworkIpc = (mainWindow) => { // send domain cookies to renderer const domainsWithCookies = await getDomainsWithCookies(); + mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies))); await runPostResponse( diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index 417f2df71..abf06bd86 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -98,17 +98,53 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces } // todo: we have things happening in two places w.r.t basic auth - // need to refactor this in the future + // need to refactor this in the future // the request.auth (basic auth) object gets set inside the prepare-request.js file if (request.auth) { const username = _interpolate(request.auth.username) || ''; const password = _interpolate(request.auth.password) || ''; - // use auth header based approach and delete the request.auth object request.headers['authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`; delete request.auth; } + if (request?.oauth2?.grantType) { + switch (request.oauth2.grantType) { + case 'password': + let username = _interpolate(request.oauth2.username) || ''; + let password = _interpolate(request.oauth2.password) || ''; + request.oauth2.username = username; + request.oauth2.password = password; + request.data = { + grant_type: 'password', + username, + password + }; + break; + case 'authorization_code': + request.oauth2.callbackUrl = _interpolate(request.oauth2.callbackUrl) || ''; + request.oauth2.authorizationUrl = _interpolate(request.oauth2.authorizationUrl) || ''; + request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || ''; + request.oauth2.clientId = _interpolate(request.oauth2.clientId) || ''; + request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || ''; + request.oauth2.scope = _interpolate(request.oauth2.scope) || ''; + break; + case 'client_credentials': + let clientId = _interpolate(request.oauth2.clientId) || ''; + let clientSecret = _interpolate(request.oauth2.clientSecret) || ''; + request.oauth2.clientId = clientId; + request.oauth2.clientSecret = clientSecret; + request.data = { + grant_type: 'client_credentials', + client_id: clientId, + client_secret: clientSecret + }; + break; + default: + break; + } + } + // interpolate vars for aws sigv4 auth if (request.awsv4config) { request.awsv4config.accessKeyId = _interpolate(request.awsv4config.accessKeyId) || ''; diff --git a/packages/bruno-electron/src/ipc/network/oauth2-authorization-code-helper.js b/packages/bruno-electron/src/ipc/network/oauth2-authorization-code-helper.js new file mode 100644 index 000000000..303af8170 --- /dev/null +++ b/packages/bruno-electron/src/ipc/network/oauth2-authorization-code-helper.js @@ -0,0 +1,41 @@ +const { get, cloneDeep } = require('lodash'); +const { authorizeUserInWindow } = require('./authorize-user-in-window'); + +const resolveOAuth2AuthorizationCodecessToken = async (request) => { + let requestCopy = cloneDeep(request); + const authorization_code = await getOAuth2AuthorizationCode(requestCopy); + const oAuth = get(requestCopy, 'oauth2', {}); + const { clientId, clientSecret, callbackUrl, scope } = oAuth; + const data = { + grant_type: 'authorization_code', + code: authorization_code, + redirect_uri: callbackUrl, + client_id: clientId, + client_secret: clientSecret, + scope: scope + }; + const url = requestCopy?.oauth2?.accessTokenUrl; + return { + data, + url + }; +}; + +const getOAuth2AuthorizationCode = (request) => { + return new Promise(async (resolve, reject) => { + const { oauth2 } = request; + const { callbackUrl, clientId, authorizationUrl, scope } = oauth2; + const authorizationUrlWithQueryParams = `${authorizationUrl}?client_id=${clientId}&redirect_uri=${callbackUrl}&response_type=code&scope=${scope}`; + try { + const code = await authorizeUserInWindow({ authorizeUrl: authorizationUrlWithQueryParams, callbackUrl }); + resolve(code); + } catch (err) { + reject(err); + } + }); +}; + +module.exports = { + resolveOAuth2AuthorizationCodecessToken, + getOAuth2AuthorizationCode +}; diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 2fe7df4f3..c1ec520a9 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -62,6 +62,36 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { password: get(collectionAuth, 'digest.password') }; break; + case 'oauth2': + const grantType = get(collectionAuth, 'auth.oauth2.grantType'); + switch (grantType) { + case 'password': + axiosRequest.oauth2 = { + grantType: grantType, + username: get(collectionAuth, 'auth.oauth2.username'), + password: get(collectionAuth, 'auth.oauth2.password') + }; + break; + case 'authorization_code': + axiosRequest.oauth2 = { + grantType: grantType, + callbackUrl: get(collectionAuth, 'auth.oauth2.callbackUrl'), + authorizationUrl: get(collectionAuth, 'auth.oauth2.authorizationUrl'), + accessTokenUrl: get(collectionAuth, 'auth.oauth2.accessTokenUrl'), + clientId: get(collectionAuth, 'auth.oauth2.clientId'), + clientSecret: get(collectionAuth, 'auth.oauth2.clientSecret'), + scope: get(collectionAuth, 'auth.oauth2.scope') + }; + break; + case 'client_credentials': + axiosRequest.oauth2 = { + grantType: grantType, + clientId: get(collectionAuth, 'auth.oauth2.clientId'), + clientSecret: get(collectionAuth, 'auth.oauth2.clientSecret') + }; + break; + } + break; } } @@ -91,6 +121,37 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { username: get(request, 'auth.digest.username'), password: get(request, 'auth.digest.password') }; + break; + case 'oauth2': + const grantType = get(request, 'auth.oauth2.grantType'); + switch (grantType) { + case 'password': + axiosRequest.oauth2 = { + grantType: grantType, + username: get(request, 'auth.oauth2.username'), + password: get(request, 'auth.oauth2.password') + }; + break; + case 'authorization_code': + axiosRequest.oauth2 = { + grantType: grantType, + callbackUrl: get(request, 'auth.oauth2.callbackUrl'), + authorizationUrl: get(request, 'auth.oauth2.authorizationUrl'), + accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'), + clientId: get(request, 'auth.oauth2.clientId'), + clientSecret: get(request, 'auth.oauth2.clientSecret'), + scope: get(request, 'auth.oauth2.scope') + }; + break; + case 'client_credentials': + axiosRequest.oauth2 = { + grantType: grantType, + clientId: get(request, 'auth.oauth2.clientId'), + clientSecret: get(request, 'auth.oauth2.clientSecret') + }; + break; + } + break; } } diff --git a/packages/bruno-electron/src/store/env-secrets.js b/packages/bruno-electron/src/store/env-secrets.js index b3d26c723..8ded05ae9 100644 --- a/packages/bruno-electron/src/store/env-secrets.js +++ b/packages/bruno-electron/src/store/env-secrets.js @@ -28,7 +28,7 @@ class EnvironmentSecretsStore { } isValidValue(val) { - return val && typeof val === 'string' && val.length > 0; + return typeof val === 'string' && val.length >= 0; } storeEnvSecrets(collectionPathname, environment) { diff --git a/packages/bruno-electron/src/utils/encryption.js b/packages/bruno-electron/src/utils/encryption.js index 980311ff9..b73e437e6 100644 --- a/packages/bruno-electron/src/utils/encryption.js +++ b/packages/bruno-electron/src/utils/encryption.js @@ -48,7 +48,7 @@ function safeStorageDecrypt(str) { } function encryptString(str) { - if (!str || typeof str !== 'string' || str.length === 0) { + if (typeof str !== 'string') { throw new Error('Encrypt failed: invalid string'); } diff --git a/packages/bruno-js/package.json b/packages/bruno-js/package.json index b7dfa4b31..05386ce08 100644 --- a/packages/bruno-js/package.json +++ b/packages/bruno-js/package.json @@ -16,6 +16,7 @@ "dependencies": { "@usebruno/query": "0.1.0", "ajv": "^8.12.0", + "ajv-formats": "^2.1.1", "atob": "^2.1.2", "axios": "^1.5.1", "btoa": "^1.2.1", @@ -28,7 +29,7 @@ "moment": "^2.29.4", "nanoid": "3.3.4", "node-fetch": "2.*", - "uuid": "^9.0.0", - "node-vault": "^0.10.2" + "node-vault": "^0.10.2", + "uuid": "^9.0.0" } } diff --git a/packages/bruno-js/src/runtime/script-runtime.js b/packages/bruno-js/src/runtime/script-runtime.js index 8df51d793..e1b7270bf 100644 --- a/packages/bruno-js/src/runtime/script-runtime.js +++ b/packages/bruno-js/src/runtime/script-runtime.js @@ -16,6 +16,7 @@ const { cleanJson } = require('../utils'); // Inbuilt Library Support const ajv = require('ajv'); +const addFormats = require('ajv-formats'); const atob = require('atob'); const btoa = require('btoa'); const lodash = require('lodash'); @@ -102,6 +103,7 @@ class ScriptRuntime { zlib, // 3rd party libs ajv, + 'ajv-formats': addFormats, atob, btoa, lodash, @@ -194,6 +196,7 @@ class ScriptRuntime { zlib, // 3rd party libs ajv, + 'ajv-formats': addFormats, atob, btoa, lodash, diff --git a/packages/bruno-js/src/runtime/test-runtime.js b/packages/bruno-js/src/runtime/test-runtime.js index cc46fd14c..e67bb6bbc 100644 --- a/packages/bruno-js/src/runtime/test-runtime.js +++ b/packages/bruno-js/src/runtime/test-runtime.js @@ -19,6 +19,7 @@ const { cleanJson } = require('../utils'); // Inbuilt Library Support const ajv = require('ajv'); +const addFormats = require('ajv-formats'); const atob = require('atob'); const btoa = require('btoa'); const lodash = require('lodash'); @@ -120,6 +121,7 @@ class TestRuntime { zlib, // 3rd party libs ajv, + 'ajv-formats': addFormats, btoa, atob, lodash, diff --git a/packages/bruno-js/tests/runtime.spec.js b/packages/bruno-js/tests/runtime.spec.js index b8e4dfae3..502cba27b 100644 --- a/packages/bruno-js/tests/runtime.spec.js +++ b/packages/bruno-js/tests/runtime.spec.js @@ -1,5 +1,6 @@ const { describe, it, expect } = require('@jest/globals'); const TestRuntime = require('../src/runtime/test-runtime'); +const ScriptRuntime = require('../src/runtime/script-runtime'); describe('runtime', () => { describe('test-runtime', () => { @@ -49,5 +50,129 @@ describe('runtime', () => { { description: 'async test', status: 'pass' } ]); }); + + it('should have ajv and ajv-formats dependencies available', async () => { + const testFile = ` + const Ajv = require('ajv'); + const addFormats = require("ajv-formats"); + const ajv = new Ajv(); + addFormats(ajv); + + const schema = { + type: 'string', + format: 'date-time' + }; + + const validate = ajv.compile(schema) + + test('format valid', () => { + const valid = validate(new Date().toISOString()) + expect(valid).to.be.true; + }) + `; + + const runtime = new TestRuntime(); + const result = await runtime.runTests( + testFile, + { ...baseRequest }, + { ...baseResponse }, + {}, + {}, + '.', + null, + process.env + ); + expect(result.results.map((el) => ({ description: el.description, status: el.status }))).toEqual([ + { description: 'format valid', status: 'pass' } + ]); + }); + }); + + describe('script-runtime', () => { + describe('run-request-script', () => { + const baseRequest = { + method: 'GET', + url: 'http://localhost:3000/', + headers: {}, + data: undefined + }; + + it('should have ajv and ajv-formats dependencies available', async () => { + const script = ` + const Ajv = require('ajv'); + const addFormats = require("ajv-formats"); + const ajv = new Ajv(); + addFormats(ajv); + + const schema = { + type: 'string', + format: 'date-time' + }; + + const validate = ajv.compile(schema) + + bru.setVar('validation', validate(new Date().toISOString())) + `; + + const runtime = new ScriptRuntime(); + const result = await runtime.runRequestScript(script, { ...baseRequest }, {}, {}, '.', null, process.env); + expect(result.collectionVariables.validation).toBeTruthy(); + }); + }); + + describe('run-response-script', () => { + const baseRequest = { + method: 'GET', + url: 'http://localhost:3000/', + headers: {}, + data: undefined + }; + const baseResponse = { + status: 200, + statusText: 'OK', + data: [ + { + id: 1 + }, + { + id: 2 + }, + { + id: 3 + } + ] + }; + + it('should have ajv and ajv-formats dependencies available', async () => { + const script = ` + const Ajv = require('ajv'); + const addFormats = require("ajv-formats"); + const ajv = new Ajv(); + addFormats(ajv); + + const schema = { + type: 'string', + format: 'date-time' + }; + + const validate = ajv.compile(schema) + + bru.setVar('validation', validate(new Date().toISOString())) + `; + + const runtime = new ScriptRuntime(); + const result = await runtime.runResponseScript( + script, + { ...baseRequest }, + { ...baseResponse }, + {}, + {}, + '.', + null, + process.env + ); + expect(result.collectionVariables.validation).toBeTruthy(); + }); + }); }); }); diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index 3c2a6c8b0..71c3e9e6c 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -23,7 +23,7 @@ const { outdentString } = require('../../v1/src/utils'); */ const grammar = ohm.grammar(`Bru { BruFile = (meta | http | query | headers | auths | bodies | varsandassert | script | tests | docs)* - auths = authawsv4 | authbasic | authbearer | authdigest + auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2 bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body bodyforms = bodyformurlencoded | bodymultipart @@ -80,6 +80,7 @@ const grammar = ohm.grammar(`Bru { authbasic = "auth:basic" dictionary authbearer = "auth:bearer" dictionary authdigest = "auth:digest" dictionary + authOAuth2 = "auth:oauth2" dictionary body = "body" st* "{" nl* textblock tagend bodyjson = "body:json" st* "{" nl* textblock tagend @@ -380,6 +381,46 @@ const sem = grammar.createSemantics().addAttribute('ast', { } }; }, + authOAuth2(_1, dictionary) { + const auth = mapPairListToKeyValPairs(dictionary.ast, false); + const grantTypeKey = _.find(auth, { name: 'grant_type' }); + const usernameKey = _.find(auth, { name: 'username' }); + const passwordKey = _.find(auth, { name: 'password' }); + const callbackUrlKey = _.find(auth, { name: 'callback_url' }); + const authorizationUrlKey = _.find(auth, { name: 'authorization_url' }); + const accessTokenUrlKey = _.find(auth, { name: 'access_token_url' }); + const clientIdKey = _.find(auth, { name: 'client_id' }); + const clientSecretKey = _.find(auth, { name: 'client_secret' }); + const scopeKey = _.find(auth, { name: 'scope' }); + return { + auth: { + oauth2: + grantTypeKey?.value && grantTypeKey?.value == 'password' + ? { + grantType: grantTypeKey ? grantTypeKey.value : '', + username: usernameKey ? usernameKey.value : '', + password: passwordKey ? passwordKey.value : '' + } + : grantTypeKey?.value && grantTypeKey?.value == 'authorization_code' + ? { + grantType: grantTypeKey ? grantTypeKey.value : '', + callbackUrl: callbackUrlKey ? callbackUrlKey.value : '', + authorizationUrl: authorizationUrlKey ? authorizationUrlKey.value : '', + accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '', + clientId: clientIdKey ? clientIdKey.value : '', + clientSecret: clientSecretKey ? clientSecretKey.value : '', + scope: scopeKey ? scopeKey.value : '' + } + : grantTypeKey?.value && grantTypeKey?.value == 'client_credentials' + ? { + grantType: grantTypeKey ? grantTypeKey.value : '', + clientId: clientIdKey ? clientIdKey.value : '', + clientSecret: clientSecretKey ? clientSecretKey.value : '' + } + : {} + } + }; + }, bodyformurlencoded(_1, dictionary) { return { body: { diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 961a270e4..17b551449 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -126,6 +126,42 @@ ${indentString(`password: ${auth.digest.password}`)} `; } + if (auth && auth.oauth2) { + switch (auth?.oauth2?.grantType) { + case 'password': + bru += `auth:oauth2 { +${indentString(`grant_type: password`)} +${indentString(`username: ${auth.oauth2.username}`)} +${indentString(`password: ${auth.oauth2.password}`)} +} + +`; + break; + case 'authorization_code': + bru += `auth:oauth2 { +${indentString(`grant_type: authorization_code`)} +${indentString(`callback_url: ${auth.oauth2.callbackUrl}`)} +${indentString(`authorization_url: ${auth.oauth2.authorizationUrl}`)} +${indentString(`access_token_url: ${auth.oauth2.accessTokenUrl}`)} +${indentString(`client_id: ${auth.oauth2.clientId}`)} +${indentString(`client_secret: ${auth.oauth2.clientSecret}`)} +${indentString(`scope: ${auth.oauth2.scope}`)} +} + +`; + break; + case 'client_credentials': + bru += `auth:oauth2 { +${indentString(`grant_type: client_credentials`)} +${indentString(`client_id: ${auth.oauth2.clientId}`)} +${indentString(`client_secret: ${auth.oauth2.clientSecret}`)} +} + +`; + break; + } + } + if (body && body.json && body.json.length) { bru += `body:json { ${indentString(body.json)} diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru index 21b20477b..20f55e07b 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.bru +++ b/packages/bruno-lang/v2/tests/fixtures/request.bru @@ -45,6 +45,15 @@ auth:digest { password: secret } +auth:oauth2 { + grantType: authorization_code + client_id: client_id_1 + client_secret: client_secret_1 + auth_url: http://localhost:8080/api/auth/oauth2/ac/authorize + callback_url: http://localhost:8080/api/auth/oauth2/ac/callback + access_token_url: http://localhost:8080/api/auth/oauth2/ac/token +} + body:json { { "hello": "world" diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json index 778da60b2..edfb6fbb8 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.json +++ b/packages/bruno-lang/v2/tests/fixtures/request.json @@ -63,6 +63,14 @@ "digest": { "username": "john", "password": "secret" + }, + "oauth2": { + "grantType": "authorization_code", + "client_id": "client_id_1", + "client_secret": "client_secret_1", + "auth_url": "http://localhost:8080/api/auth/oauth2/ac/authorize", + "callback_url": "http://localhost:8080/api/auth/oauth2/ac/callback", + "access_token_url": "http://localhost:8080/api/auth/oauth2/ac/token" } }, "body": { diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index cabb76eaf..36ddf2811 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -119,12 +119,61 @@ const authDigestSchema = Yup.object({ .noUnknown(true) .strict(); +const oauth2Schema = Yup.object({ + grantType: Yup.string() + .oneOf(['client_credentials', 'password', 'authorization_code']) + .required('grantType is required'), + username: Yup.string().when('grantType', { + is: (val) => ['client_credentials', 'password'].includes(val), + then: Yup.string().nullable(), + otherwise: Yup.string().nullable().strip() + }), + password: Yup.string().when('grantType', { + is: (val) => ['client_credentials', 'password'].includes(val), + then: Yup.string().nullable(), + otherwise: Yup.string().nullable().strip() + }), + callbackUrl: Yup.string().when('grantType', { + is: (val) => ['authorization_code'].includes(val), + then: Yup.string().nullable(), + otherwise: Yup.string().nullable().strip() + }), + authorizationUrl: Yup.string().when('grantType', { + is: (val) => ['authorization_code'].includes(val), + then: Yup.string().nullable(), + otherwise: Yup.string().nullable().strip() + }), + accessTokenUrl: Yup.string().when('grantType', { + is: (val) => ['authorization_code'].includes(val), + then: Yup.string().nullable(), + otherwise: Yup.string().nullable().strip() + }), + clientId: Yup.string().when('grantType', { + is: (val) => ['authorization_code', 'client_credentials'].includes(val), + then: Yup.string().nullable(), + otherwise: Yup.string().nullable().strip() + }), + clientSecret: Yup.string().when('grantType', { + is: (val) => ['authorization_code', 'client_credentials'].includes(val), + then: Yup.string().nullable(), + otherwise: Yup.string().nullable().strip() + }), + scope: Yup.string().when('grantType', { + is: (val) => ['authorization_code'].includes(val), + then: Yup.string().nullable(), + otherwise: Yup.string().nullable().strip() + }) +}) + .noUnknown(true) + .strict(); + const authSchema = Yup.object({ - mode: Yup.string().oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest']).required('mode is required'), + mode: Yup.string().oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest', 'oauth2']).required('mode is required'), awsv4: authAwsV4Schema.nullable(), basic: authBasicSchema.nullable(), bearer: authBearerSchema.nullable(), - digest: authDigestSchema.nullable() + digest: authDigestSchema.nullable(), + oauth2: oauth2Schema.nullable() }) .noUnknown(true) .strict(); diff --git a/packages/bruno-tests/collection/bruno.json b/packages/bruno-tests/collection/bruno.json index 79602ccd3..b6d437bbb 100644 --- a/packages/bruno-tests/collection/bruno.json +++ b/packages/bruno-tests/collection/bruno.json @@ -15,7 +15,7 @@ "bypassProxy": "" }, "scripts": { - "moduleWhitelist": ["crypto"], + "moduleWhitelist": ["crypto", "buffer"], "filesystemAccess": { "allow": true } diff --git a/packages/bruno-tests/collection/environments/Local.bru b/packages/bruno-tests/collection/environments/Local.bru index 26d3a6575..86e79139d 100644 --- a/packages/bruno-tests/collection/environments/Local.bru +++ b/packages/bruno-tests/collection/environments/Local.bru @@ -1,8 +1,28 @@ vars { - host: http://localhost:80 + host: http://localhost:8080 bearer_auth_token: your_secret_token basic_auth_password: della - env.var1: envVar1 - env-var2: envVar2 - bark: {{process.env.PROC_ENV_VAR}} + client_id: client_id_1 + client_secret: client_secret_1 + auth_url: http://localhost:8080/api/auth/oauth2/ac/authorize + callback_url: http://localhost:8080/api/auth/oauth2/ac/callback + access_token_url: http://localhost:8080/api/auth/oauth2/ac/token + ropc_username: foo + ropc_password: bar + github_authorize_url: https://github.com/login/oauth/authorize + github_access_token_url: https://github.com/login/oauth/access_token + google_auth_url: https://accounts.google.com/o/oauth2/auth + google_access_token_url: https://accounts.google.com/o/oauth2/token + google_scope: https://www.googleapis.com/auth/userinfo.email } +vars:secret [ + github_client_secret, + github_client_id, + google_client_id, + google_client_secret, + github_authorization_code, + ropc_access_token, + cc_access_token, + ac_access_token, + github_access_token +] diff --git a/packages/bruno-tests/collection/package-lock.json b/packages/bruno-tests/collection/package-lock.json index 717181ec3..b8b4283ae 100644 --- a/packages/bruno-tests/collection/package-lock.json +++ b/packages/bruno-tests/collection/package-lock.json @@ -8,7 +8,9 @@ "name": "@usebruno/test-collection", "version": "0.0.1", "dependencies": { - "@faker-js/faker": "^8.4.0" + "@faker-js/faker": "^8.4.0", + "jsonwebtoken": "^9.0.2", + "lru-map-cache": "^0.1.0" } }, "node_modules/@faker-js/faker": { @@ -25,6 +27,153 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0", "npm": ">=6.14.13" } + }, + "node_modules/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": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/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==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "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": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-map-cache": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-map-cache/-/lru-map-cache-0.1.0.tgz", + "integrity": "sha512-r1lasvJbg3lrTS37W5h4Ugy9miaWluYqviZGbfH9A6AbjxSDJCtPNqtGr5MRl/RG/EfYrwe07DC4zQEBnY2q4w==" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } } diff --git a/packages/bruno-tests/collection/package.json b/packages/bruno-tests/collection/package.json index 23621129b..b21ab5b94 100644 --- a/packages/bruno-tests/collection/package.json +++ b/packages/bruno-tests/collection/package.json @@ -2,6 +2,8 @@ "name": "@usebruno/test-collection", "version": "0.0.1", "dependencies": { - "@faker-js/faker": "^8.4.0" + "@faker-js/faker": "^8.4.0", + "jsonwebtoken": "^9.0.2", + "lru-map-cache": "^0.1.0" } } diff --git a/packages/bruno-tests/collection_oauth2/.env b/packages/bruno-tests/collection_oauth2/.env new file mode 100644 index 000000000..0c7267404 --- /dev/null +++ b/packages/bruno-tests/collection_oauth2/.env @@ -0,0 +1 @@ +PROC_ENV_VAR=woof \ No newline at end of file diff --git a/packages/bruno-tests/collection_oauth2/.gitignore b/packages/bruno-tests/collection_oauth2/.gitignore new file mode 100644 index 000000000..1e18f275e --- /dev/null +++ b/packages/bruno-tests/collection_oauth2/.gitignore @@ -0,0 +1 @@ +!.env \ No newline at end of file diff --git a/packages/bruno-tests/collection_oauth2/.nvmrc b/packages/bruno-tests/collection_oauth2/.nvmrc new file mode 100644 index 000000000..0828ab794 --- /dev/null +++ b/packages/bruno-tests/collection_oauth2/.nvmrc @@ -0,0 +1 @@ +v18 \ No newline at end of file diff --git a/packages/bruno-tests/collection_oauth2/auth/oauth2/ac/github token with authorize.bru b/packages/bruno-tests/collection_oauth2/auth/oauth2/ac/github token with authorize.bru new file mode 100644 index 000000000..c18d2c6ed --- /dev/null +++ b/packages/bruno-tests/collection_oauth2/auth/oauth2/ac/github token with authorize.bru @@ -0,0 +1,25 @@ +meta { + name: github token with authorize + type: http + seq: 1 +} + +post { + url: github.com + body: none + auth: oauth2 +} + +auth:oauth2 { + grant_type: authorization_code + callback_url: {{callback_url}} + authorization_url: {{github_authorize_url}} + access_token_url: {{github_access_token_url}} + client_id: {{github_client_id}} + client_secret: {{github_client_secret}} + scope: repo,gist +} + +script:post-response { + bru.setEnvVar('github_access_token',res.body.split('access_token=')[1]?.split('&scope')[0]); +} diff --git a/packages/bruno-tests/collection_oauth2/auth/oauth2/ac/google token with authorize.bru b/packages/bruno-tests/collection_oauth2/auth/oauth2/ac/google token with authorize.bru new file mode 100644 index 000000000..93ea7975e --- /dev/null +++ b/packages/bruno-tests/collection_oauth2/auth/oauth2/ac/google token with authorize.bru @@ -0,0 +1,25 @@ +meta { + name: google token with authorize + type: http + seq: 4 +} + +post { + url: + body: none + auth: oauth2 +} + +auth:oauth2 { + grant_type: authorization_code + callback_url: {{callback_url}} + authorization_url: {{google_auth_url}} + access_token_url: {{google_access_token_url}} + client_id: {{google_client_id}} + client_secret: {{google_client_secret}} + scope: {{google_scope}} +} + +script:post-response { + bru.setEnvVar('ac_access_token', res.body.access_token); +} diff --git a/packages/bruno-tests/collection_oauth2/auth/oauth2/ac/resource.bru b/packages/bruno-tests/collection_oauth2/auth/oauth2/ac/resource.bru new file mode 100644 index 000000000..ead30ec6b --- /dev/null +++ b/packages/bruno-tests/collection_oauth2/auth/oauth2/ac/resource.bru @@ -0,0 +1,27 @@ +meta { + name: resource + type: http + seq: 3 +} + +post { + url: {{host}}/api/auth/oauth2/ac/resource?token={{ac_access_token}} + body: json + auth: none +} + +query { + token: {{ac_access_token}} +} + +auth:bearer { + token: +} + +body:json { + { + "code": "eb30dbf783b65bec4539ee1dcb068606", + "client_id": "{{client_id}}", + "client_secret": "{{client_secret}}" + } +} diff --git a/packages/bruno-tests/collection_oauth2/auth/oauth2/ac/token with authorize.bru b/packages/bruno-tests/collection_oauth2/auth/oauth2/ac/token with authorize.bru new file mode 100644 index 000000000..e42fd7c77 --- /dev/null +++ b/packages/bruno-tests/collection_oauth2/auth/oauth2/ac/token with authorize.bru @@ -0,0 +1,25 @@ +meta { + name: token with authorize + type: http + seq: 4 +} + +post { + url: + body: none + auth: oauth2 +} + +auth:oauth2 { + grant_type: authorization_code + callback_url: {{callback_url}} + authorization_url: {{auth_url}} + access_token_url: {{access_token_url}} + client_id: {{client_id}} + client_secret: {{client_secret}} + scope: +} + +script:post-response { + bru.setEnvVar('ac_access_token', res.body.access_token); +} diff --git a/packages/bruno-tests/collection_oauth2/auth/oauth2/cc/resource.bru b/packages/bruno-tests/collection_oauth2/auth/oauth2/cc/resource.bru new file mode 100644 index 000000000..c4a1ce399 --- /dev/null +++ b/packages/bruno-tests/collection_oauth2/auth/oauth2/cc/resource.bru @@ -0,0 +1,15 @@ +meta { + name: resource + type: http + seq: 2 +} + +get { + url: {{host}}/api/auth/oauth2/cc/resource?token={{cc_access_token}} + body: none + auth: none +} + +query { + token: {{cc_access_token}} +} diff --git a/packages/bruno-tests/collection_oauth2/auth/oauth2/cc/token.bru b/packages/bruno-tests/collection_oauth2/auth/oauth2/cc/token.bru new file mode 100644 index 000000000..13987b2eb --- /dev/null +++ b/packages/bruno-tests/collection_oauth2/auth/oauth2/cc/token.bru @@ -0,0 +1,21 @@ +meta { + name: token + type: http + seq: 1 +} + +post { + url: {{host}}/api/auth/oauth2/cc/token + body: none + auth: oauth2 +} + +auth:oauth2 { + grant_type: client_credentials + client_id: {{client_id}} + client_secret: {{client_secret}} +} + +script:post-response { + bru.setEnvVar('cc_access_token', res.body.access_token); +} diff --git a/packages/bruno-tests/collection_oauth2/auth/oauth2/ropc/resource.bru b/packages/bruno-tests/collection_oauth2/auth/oauth2/ropc/resource.bru new file mode 100644 index 000000000..1395250ee --- /dev/null +++ b/packages/bruno-tests/collection_oauth2/auth/oauth2/ropc/resource.bru @@ -0,0 +1,15 @@ +meta { + name: resource + type: http + seq: 2 +} + +post { + url: {{host}}/api/auth/oauth2/ropc/resource + body: none + auth: bearer +} + +auth:bearer { + token: {{ropc_access_token}} +} diff --git a/packages/bruno-tests/collection_oauth2/auth/oauth2/ropc/token.bru b/packages/bruno-tests/collection_oauth2/auth/oauth2/ropc/token.bru new file mode 100644 index 000000000..495655ab3 --- /dev/null +++ b/packages/bruno-tests/collection_oauth2/auth/oauth2/ropc/token.bru @@ -0,0 +1,21 @@ +meta { + name: token + type: http + seq: 1 +} + +post { + url: {{host}}/api/auth/oauth2/ropc/token + body: none + auth: oauth2 +} + +auth:oauth2 { + grant_type: password + username: {{ropc_username}} + password: {{ropc_password}} +} + +script:post-response { + bru.setEnvVar('ropc_access_token', res.body.access_token); +} diff --git a/packages/bruno-tests/collection_oauth2/bruno.json b/packages/bruno-tests/collection_oauth2/bruno.json new file mode 100644 index 000000000..79602ccd3 --- /dev/null +++ b/packages/bruno-tests/collection_oauth2/bruno.json @@ -0,0 +1,31 @@ +{ + "version": "1", + "name": "bruno-testbench", + "type": "collection", + "proxy": { + "enabled": false, + "protocol": "http", + "hostname": "{{proxyHostname}}", + "port": 4000, + "auth": { + "enabled": false, + "username": "anoop", + "password": "password" + }, + "bypassProxy": "" + }, + "scripts": { + "moduleWhitelist": ["crypto"], + "filesystemAccess": { + "allow": true + } + }, + "clientCertificates": { + "enabled": true, + "certs": [] + }, + "presets": { + "requestType": "http", + "requestUrl": "http://localhost:6000" + } +} diff --git a/packages/bruno-tests/collection_oauth2/collection.bru b/packages/bruno-tests/collection_oauth2/collection.bru new file mode 100644 index 000000000..e31b64995 --- /dev/null +++ b/packages/bruno-tests/collection_oauth2/collection.bru @@ -0,0 +1,22 @@ +headers { + check: again +} + +auth { + mode: none +} + +auth:basic { + username: bruno + password: {{basicAuthPassword}} +} + +auth:bearer { + token: {{bearerAuthToken}} +} + +docs { + # bruno-testbench 🐶 + + This is a test collection that I am using to test various functionalities around bruno +} diff --git a/packages/bruno-tests/collection_oauth2/environments/Local.bru b/packages/bruno-tests/collection_oauth2/environments/Local.bru new file mode 100644 index 000000000..99fff5991 --- /dev/null +++ b/packages/bruno-tests/collection_oauth2/environments/Local.bru @@ -0,0 +1,26 @@ +vars { + host: http://localhost:8080 + bearer_auth_token: your_secret_token + basic_auth_password: della + client_id: client_id_1 + client_secret: client_secret_1 + auth_url: http://localhost:8080/api/auth/oauth2/ac/authorize + callback_url: http://localhost:8080/api/auth/oauth2/ac/callback + access_token_url: http://localhost:8080/api/auth/oauth2/ac/token + ropc_username: foo + ropc_password: bar + github_authorize_url: https://github.com/login/oauth/authorize + github_access_token_url: https://github.com/login/oauth/access_token + google_auth_url: https://accounts.google.com/o/oauth2/auth + google_access_token_url: https://accounts.google.com/o/oauth2/token + google_scope: https://www.googleapis.com/auth/userinfo.email +} +vars:secret [ + github_client_secret, + github_client_id, + google_client_id, + google_client_secret, + github_authorization_code, + github_access_token, + ac_access_token +] diff --git a/packages/bruno-tests/collection_oauth2/environments/Prod.bru b/packages/bruno-tests/collection_oauth2/environments/Prod.bru new file mode 100644 index 000000000..e6286f3b6 --- /dev/null +++ b/packages/bruno-tests/collection_oauth2/environments/Prod.bru @@ -0,0 +1,8 @@ +vars { + host: https://testbench-sanity.usebruno.com + bearer_auth_token: your_secret_token + basic_auth_password: della + env.var1: envVar1 + env-var2: envVar2 + bark: {{process.env.PROC_ENV_VAR}} +} diff --git a/packages/bruno-tests/collection_oauth2/file.json b/packages/bruno-tests/collection_oauth2/file.json new file mode 100644 index 000000000..a967fac5b --- /dev/null +++ b/packages/bruno-tests/collection_oauth2/file.json @@ -0,0 +1,3 @@ +{ + "hello": "bruno" +} diff --git a/packages/bruno-tests/collection_oauth2/package-lock.json b/packages/bruno-tests/collection_oauth2/package-lock.json new file mode 100644 index 000000000..717181ec3 --- /dev/null +++ b/packages/bruno-tests/collection_oauth2/package-lock.json @@ -0,0 +1,30 @@ +{ + "name": "@usebruno/test-collection", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@usebruno/test-collection", + "version": "0.0.1", + "dependencies": { + "@faker-js/faker": "^8.4.0" + } + }, + "node_modules/@faker-js/faker": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.0.tgz", + "integrity": "sha512-htW87352wzUCdX1jyUQocUcmAaFqcR/w082EC8iP/gtkF0K+aKcBp0hR5Arb7dzR8tQ1TrhE9DNa5EbJELm84w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" + } + } + } +} diff --git a/packages/bruno-tests/collection_oauth2/package.json b/packages/bruno-tests/collection_oauth2/package.json new file mode 100644 index 000000000..23621129b --- /dev/null +++ b/packages/bruno-tests/collection_oauth2/package.json @@ -0,0 +1,7 @@ +{ + "name": "@usebruno/test-collection", + "version": "0.0.1", + "dependencies": { + "@faker-js/faker": "^8.4.0" + } +} diff --git a/packages/bruno-tests/collection_oauth2/readme.md b/packages/bruno-tests/collection_oauth2/readme.md new file mode 100644 index 000000000..a41582d22 --- /dev/null +++ b/packages/bruno-tests/collection_oauth2/readme.md @@ -0,0 +1,3 @@ +# bruno-tests collection + +API Collection to run sanity tests on Bruno CLI. diff --git a/packages/bruno-tests/package.json b/packages/bruno-tests/package.json index 0135eeb61..84ede3d62 100644 --- a/packages/bruno-tests/package.json +++ b/packages/bruno-tests/package.json @@ -27,6 +27,7 @@ "express-xml-bodyparser": "^0.3.0", "http-proxy": "^1.18.1", "js-yaml": "^4.1.0", + "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", "multer": "^1.4.5-lts.1" } diff --git a/packages/bruno-tests/src/auth/index.js b/packages/bruno-tests/src/auth/index.js index 84d93798e..0b5dc7f63 100644 --- a/packages/bruno-tests/src/auth/index.js +++ b/packages/bruno-tests/src/auth/index.js @@ -4,7 +4,13 @@ const router = express.Router(); const authBearer = require('./bearer'); const authBasic = require('./basic'); const authCookie = require('./cookie'); +const authOAuth2Ropc = require('./oauth2/ropc'); +const authOAuth2AuthorizationCode = require('./oauth2/ac'); +const authOAuth2Cc = require('./oauth2/cc'); +router.use('/oauth2/ropc', authOAuth2Ropc); +router.use('/oauth2/ac', authOAuth2AuthorizationCode); +router.use('/oauth2/cc', authOAuth2Cc); router.use('/bearer', authBearer); router.use('/basic', authBasic); router.use('/cookie', authCookie); diff --git a/packages/bruno-tests/src/auth/oauth2/ac.js b/packages/bruno-tests/src/auth/oauth2/ac.js new file mode 100644 index 000000000..840c2a778 --- /dev/null +++ b/packages/bruno-tests/src/auth/oauth2/ac.js @@ -0,0 +1,141 @@ +const express = require('express'); +const router = express.Router(); +const crypto = require('crypto'); +const clients = [ + { + client_id: 'client_id_1', + client_secret: 'client_secret_1', + redirect_uri: 'http://localhost:3001/callback' + } +]; + +const authCodes = []; + +const tokens = []; + +function generateUniqueString() { + return crypto.randomBytes(16).toString('hex'); +} + +router.get('/authorize', (req, res) => { + const { response_type, client_id, redirect_uri } = req.query; + if (response_type !== 'code') { + return res.status(401).json({ error: 'Invalid Response type, expected "code"' }); + } + + const client = clients.find((c) => c.client_id === client_id); + + if (!client) { + return res.status(401).json({ error: 'Invalid client' }); + } + + if (!redirect_uri) { + return res.status(401).json({ error: 'Invalid redirect URI' }); + } + + const authorization_code = generateUniqueString(); + authCodes.push({ + authCode: authorization_code, + client_id, + redirect_uri + }); + + const redirectUrl = `${redirect_uri}?code=${authorization_code}`; + + const _res = ` + + + + + + + `; + + res.send(_res); +}); + +// Handle the authorization callback +router.get('/callback', (req, res) => { + const { code } = req.query; + + // Check if the authCode is valid. + const storedAuthCode = authCodes.find((t) => t.authCode === code); + + if (!storedAuthCode) { + return res.status(401).json({ error: 'Invalid Authorization Code' }); + } + + return res.json({ message: 'Authorization successful', storedAuthCode }); +}); + +router.post('/token', (req, res) => { + let grant_type, code, redirect_uri, client_id, client_secret; + if (req?.body?.grant_type) { + grant_type = req?.body?.grant_type; + code = req?.body?.code; + redirect_uri = req?.body?.redirect_uri; + client_id = req?.body?.client_id; + client_secret = req?.body?.client_secret; + } + if (req?.headers?.grant_type) { + grant_type = req?.headers?.grant_type; + code = req?.headers?.code; + redirect_uri = req?.headers?.redirect_uri; + client_id = req?.headers?.client_id; + client_secret = req?.headers?.client_secret; + } + + if (grant_type !== 'authorization_code') { + return res.status(401).json({ error: 'Invalid Grant Type' }); + } + + // const client = clients.find((c) => c.client_id === client_id && c.client_secret === client_secret); + // if (!client) { + // return res.status(401).json({ error: 'Invalid client credentials' }); + // } + + const storedAuthCode = authCodes.find((t) => t.authCode === code); + + if (!storedAuthCode) { + return res.status(401).json({ error: 'Invalid Authorization Code' }); + } + + const accessToken = generateUniqueString(); + tokens.push({ + accessToken: accessToken, + client_id + }); + + res.json({ access_token: accessToken }); +}); + +router.post('/resource', (req, res) => { + try { + const { token } = req.query; + const storedToken = tokens.find((t) => t.accessToken === token); + if (!storedToken) { + return res.status(401).json({ error: 'Invalid Access Token' }); + } + return res.json({ resource: { name: 'foo', email: 'foo@bar.com' } }); + } catch (err) { + return res.status(401).json({ error: 'Corrupt Access Token' }); + } +}); + +module.exports = router; diff --git a/packages/bruno-tests/src/auth/oauth2/cc.js b/packages/bruno-tests/src/auth/oauth2/cc.js new file mode 100644 index 000000000..dcaee3027 --- /dev/null +++ b/packages/bruno-tests/src/auth/oauth2/cc.js @@ -0,0 +1,62 @@ +const express = require('express'); +const router = express.Router(); +const crypto = require('crypto'); +const clients = [ + { + client_id: 'client_id_1', + client_secret: 'client_secret_1' + } +]; + +const tokens = []; + +function generateUniqueString() { + return crypto.randomBytes(16).toString('hex'); +} + +router.post('/token', (req, res) => { + let grant_type, client_id, client_secret; + if (req?.body?.grant_type) { + grant_type = req?.body?.grant_type; + client_id = req?.body?.client_id; + client_secret = req?.body?.client_secret; + } else if (req?.headers?.grant_type) { + grant_type = req?.headers?.grant_type; + client_id = req?.headers?.client_id; + client_secret = req?.headers?.client_secret; + } + + if (grant_type !== 'client_credentials') { + return res.status(401).json({ error: 'Invalid Grant Type, expected "client_credentials"' }); + } + + const client = clients.find((c) => c.client_id == client_id && c.client_secret == client_secret); + + if (!client) { + return res.status(401).json({ error: 'Invalid client' }); + } + + const token = generateUniqueString(); + tokens.push({ + token, + client_id, + client_secret + }); + + return res.json({ message: 'Authenticated successfully', access_token: token }); +}); + +router.get('/resource', (req, res) => { + try { + const { token } = req.query; + const storedToken = tokens.find((t) => t.token === token); + if (!storedToken) { + return res.status(401).json({ error: 'Invalid Access Token' }); + } + return res.json({ resource: { name: 'foo', email: 'foo@bar.com' } }); + } catch (err) { + return res.status(401).json({ error: 'Corrupt Access Token' }); + } +}); + +module.exports = router; diff --git a/packages/bruno-tests/src/auth/oauth2/ropc.js b/packages/bruno-tests/src/auth/oauth2/ropc.js new file mode 100644 index 000000000..84bb979a7 --- /dev/null +++ b/packages/bruno-tests/src/auth/oauth2/ropc.js @@ -0,0 +1,59 @@ +const express = require('express'); +const router = express.Router(); +const jwt = require('jsonwebtoken'); + +const users = [ + { + username: 'foo', + password: 'bar' + } +]; + +// P +// { +// grant_type: 'password', +// username: 'foo', +// password: 'bar' +// } + +// I +// { +// grant_type: 'password', +// username: 'foo', +// password: 'bar', +// client_id: 'client_id_1', +// client_secret: 'client_secret_1' +// } +router.post('/token', (req, res) => { + const { grant_type, username, password, client_id, client_secret } = req.body; + + if (grant_type !== 'password') { + return res.status(401).json({ error: 'Invalid Grant Type' }); + } + + const user = users.find((u) => u.username == username && u.password == password); + + if (!user) { + return res.status(401).json({ error: 'Invalid user credentials' }); + } + var token = jwt.sign({ username, password }, 'bruno'); + return res.json({ message: 'Authorization successful', access_token: token }); +}); + +router.post('/resource', (req, res) => { + try { + const tokenString = req.header('Authorization'); + const token = tokenString.split(' ')[1]; + var decodedJwt = jwt.verify(token, 'bruno'); + const { username, password } = decodedJwt; + const user = users.find((u) => u.username === username && u.password === password); + if (!user) { + return res.status(401).json({ error: 'Invalid token' }); + } + return res.json({ resource: { name: 'foo', email: 'foo@bar.com' } }); + } catch (err) { + return res.status(401).json({ error: 'Corrupt token' }); + } +}); + +module.exports = router; diff --git a/packages/bruno-tests/src/index.js b/packages/bruno-tests/src/index.js index 286cfdad6..9ba6e3170 100644 --- a/packages/bruno-tests/src/index.js +++ b/packages/bruno-tests/src/index.js @@ -5,7 +5,7 @@ const cors = require('cors'); const multer = require('multer'); const app = new express(); -const port = process.env.PORT || 80; +const port = process.env.PORT || 8080; const upload = multer(); app.use(cors());