feat(#1003): oauth2 support - resourceOwnerPasswordCredentials, authorization code, client credentials (#1654)

* feat(#1003): oauth2 support
Co-authored-by: lohit-1 <lohit@usebruno.com>
This commit is contained in:
lohit 2024-02-26 16:44:38 +05:30 committed by GitHub
parent a4b13d5c2a
commit 9f81e6dc73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 1622 additions and 77 deletions

207
package-lock.json generated
View File

@ -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",
@ -21074,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"
}

View File

@ -71,6 +71,15 @@ const AuthMode = ({ item, collection }) => {
>
Digest Auth
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('oauth2');
}}
>
OAuth 2.0
</div>
<div
className="dropdown-item"
onClick={() => {

View File

@ -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;

View File

@ -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 (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
{inputsConfig.map((input) => {
const { key, label } = input;
return (
<div className="flex flex-col w-full gap-1">
<label className="block font-medium">{label}</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={oAuth[key] || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange(key, val)}
onRun={() => {}}
collection={collection}
/>
</div>
</div>
);
})}
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
Get Authorization Code
</button>
</StyledWrapper>
);
};
export default OAuth2AuthorizationCode;

View File

@ -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 };

View File

@ -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;

View File

@ -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 (
<StyledWrapper className="mt-2 w-full">
<label className="block font-medium mb-2">Client Id</label>
<div className="single-line-editor-wrapper mb-2">
<SingleLineEditor
value={oAuth.clientId || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleClientIdChange(val)}
onRun={handleRun}
collection={collection}
/>
</div>
<label className="block font-medium mb-2">Client Secret</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={oAuth.clientSecret || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleClientSecretChange(val)}
onRun={handleRun}
collection={collection}
/>
</div>
</StyledWrapper>
);
};
export default OAuth2ClientCredentials;

View File

@ -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;

View File

@ -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 (
<div ref={ref} className="flex items-center justify-end grant-type-label select-none">
{humanizeGrantType(oAuth?.grantType)} <IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
</div>
);
});
const onGrantTypeChange = (grantType) => {
dispatch(
updateAuth({
mode: 'oauth2',
collectionUid: collection.uid,
itemUid: item.uid,
content: {
grantType
}
})
);
};
return (
<StyledWrapper>
<label className="block font-medium mb-2">Grant Type</label>
<div className="inline-flex items-center cursor-pointer grant-type-mode-selector w-full">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onGrantTypeChange('password');
}}
>
Resource Owner Password Credentials
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onGrantTypeChange('authorization_code');
}}
>
Authorization Code
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onGrantTypeChange('client_credentials');
}}
>
Client Credentials
</div>
</Dropdown>
</div>
</StyledWrapper>
);
};
export default GrantTypeSelector;

View File

@ -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;

View File

@ -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 (
<StyledWrapper className="mt-2 w-full">
<label className="block font-medium mb-2">Username</label>
<div className="single-line-editor-wrapper mb-2">
<SingleLineEditor
value={oAuth.username || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleUsernameChange(val)}
onRun={handleRun}
collection={collection}
/>
</div>
<label className="block font-medium mb-2">Password</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={oAuth.password || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handlePasswordChange(val)}
onRun={handleRun}
collection={collection}
/>
</div>
</StyledWrapper>
);
};
export default OAuth2Ropc;

View File

@ -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;

View File

@ -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 <OAuth2Ropc item={item} collection={collection} />;
break;
case 'authorization_code':
return <OAuth2AuthorizationCode item={item} collection={collection} />;
break;
case 'client_credentials':
return <OAuth2ClientCredentials item={item} collection={collection} />;
break;
default:
return <div>TBD</div>;
break;
}
};
const OAuth2 = ({ item, collection }) => {
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
return (
<StyledWrapper className="mt-2 w-full">
<GrantTypeSelector item={item} collection={collection} />
{grantTypeComponentMap(oAuth?.grantType, item, collection)}
</StyledWrapper>
);
};
export default OAuth2;

View File

@ -6,6 +6,7 @@ import BearerAuth from './BearerAuth';
import BasicAuth from './BasicAuth';
import DigestAuth from './DigestAuth';
import StyledWrapper from './StyledWrapper';
import OAuth2 from './OAuth2/index';
const Auth = ({ item, collection }) => {
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
@ -24,6 +25,9 @@ const Auth = ({ item, collection }) => {
case 'digest': {
return <DigestAuth collection={collection} item={item} />;
}
case 'oauth2': {
return <OAuth2 collection={collection} item={item} />;
}
}
};

View File

@ -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;
}
}
}

View File

@ -505,6 +505,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;

View File

@ -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:"
];

View File

@ -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 };

View File

@ -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(

View File

@ -103,12 +103,48 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
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) || '';

View File

@ -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
};

View File

@ -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;
}
}

View File

@ -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: {

View File

@ -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)}

View File

@ -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"

View File

@ -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": {

View File

@ -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(['none', 'awsv4', 'basic', 'bearer', 'digest']).required('mode is required'),
mode: Yup.string().oneOf(['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();

View File

@ -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
]

View File

@ -0,0 +1 @@
PROC_ENV_VAR=woof

View File

@ -0,0 +1 @@
!.env

View File

@ -0,0 +1 @@
v18

View File

@ -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]);
}

View File

@ -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);
}

View File

@ -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}}"
}
}

View File

@ -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);
}

View File

@ -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}}
}

View File

@ -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);
}

View File

@ -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}}
}

View File

@ -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);
}

View File

@ -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"
}
}

View File

@ -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
}

View File

@ -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
]

View File

@ -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}}
}

View File

@ -0,0 +1,3 @@
{
"hello": "bruno"
}

View File

@ -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"
}
}
}
}

View File

@ -0,0 +1,7 @@
{
"name": "@usebruno/test-collection",
"version": "0.0.1",
"dependencies": {
"@faker-js/faker": "^8.4.0"
}
}

View File

@ -0,0 +1,3 @@
# bruno-tests collection
API Collection to run sanity tests on Bruno CLI.

View File

@ -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"
}

View File

@ -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);

View File

@ -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 = `
<html>
<script>
document.addEventListener("DOMContentLoaded", (event) => {
const buttonElement = document.getElementById('authorize');
buttonElement.addEventListener('click', e => {
e.preventDefault();
buttonElement.innerText = 'redirecting...';
try {
const url = new URL("${redirectUrl}");
window.location.href = url;
}
catch(err) {
buttonElement.innerText = 'Invalid Redirect URL';
console.log('Invalid Redirect URL')
}
});
});
</script>
<body>
<button id='authorize'>Authorize</button>
</body>
</html>
`;
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;

View File

@ -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;

View File

@ -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;

View File

@ -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());