From 3c854c8df91cc8546fb1c6bc95de8e9edc6269b5 Mon Sep 17 00:00:00 2001 From: Mateusz Pietryga Date: Fri, 24 May 2024 22:35:10 +0200 Subject: [PATCH 1/3] chore: remove duplication and Auth mode naming discrepancies --- .../CollectionSettings/Auth/AuthMode/index.js | 98 +++------------- .../RequestPane/Auth/AuthMode/index.js | 107 +++--------------- .../bruno-app/src/utils/collections/index.js | 2 +- 3 files changed, 35 insertions(+), 172 deletions(-) diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js index 2c541cc289..08b59aae30 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js @@ -13,6 +13,8 @@ const AuthMode = ({ collection }) => { const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); const authMode = get(collection, 'root.request.auth.mode'); + const authModes = ['awsv4', 'basic', 'bearer', 'digest', 'ntlm', 'oauth2', 'wsse', 'apikey', 'none']; + const Icon = forwardRef((props, ref) => { return (
@@ -34,87 +36,21 @@ const AuthMode = ({ collection }) => {
} placement="bottom-end"> -
{ - dropdownTippyRef.current.hide(); - onModeChange('awsv4'); - }} - > - AWS Sig v4 -
-
{ - dropdownTippyRef.current.hide(); - onModeChange('basic'); - }} - > - Basic Auth -
-
{ - dropdownTippyRef.current.hide(); - onModeChange('wsse'); - }} - > - WSSE Auth -
-
{ - dropdownTippyRef.current.hide(); - onModeChange('bearer'); - }} - > - Bearer Token -
-
{ - dropdownTippyRef.current.hide(); - onModeChange('digest'); - }} - > - Digest Auth -
-
{ - dropdownTippyRef.current.hide(); - onModeChange('ntlm'); - }} - > - NTLM Auth -
-
{ - dropdownTippyRef.current.hide(); - onModeChange('oauth2'); - }} - > - Oauth2 -
-
{ - dropdownTippyRef.current.hide(); - onModeChange('apikey'); - }} - > - API Key -
-
{ - dropdownTippyRef.current.hide(); - onModeChange('none'); - }} - > - No Auth -
+ {authModes.map((mode) => { + return ( + <> +
{ + dropdownTippyRef.current.hide(); + onModeChange(mode); + }} + > + {humanizeRequestAuthMode(mode)} +
+ + ); + })}
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 1e3bedc2f3..c5b9694415 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js @@ -13,6 +13,8 @@ const AuthMode = ({ item, collection }) => { const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode'); + const authModes = ['awsv4', 'basic', 'bearer', 'digest', 'ntlm', 'oauth2', 'wsse', 'apikey', 'inherit', 'none']; + const Icon = forwardRef((props, ref) => { return (
@@ -34,96 +36,21 @@ const AuthMode = ({ item, collection }) => {
} placement="bottom-end"> -
{ - dropdownTippyRef?.current?.hide(); - onModeChange('awsv4'); - }} - > - AWS Sig v4 -
-
{ - dropdownTippyRef?.current?.hide(); - onModeChange('basic'); - }} - > - Basic Auth -
-
{ - dropdownTippyRef?.current?.hide(); - onModeChange('bearer'); - }} - > - Bearer Token -
-
{ - dropdownTippyRef?.current?.hide(); - onModeChange('digest'); - }} - > - Digest Auth -
-
{ - dropdownTippyRef?.current?.hide(); - onModeChange('ntlm'); - }} - > - NTLM Auth -
-
{ - dropdownTippyRef?.current?.hide(); - onModeChange('oauth2'); - }} - > - OAuth 2.0 -
-
{ - dropdownTippyRef?.current?.hide(); - onModeChange('wsse'); - }} - > - WSSE Auth -
-
{ - dropdownTippyRef?.current?.hide(); - onModeChange('apikey'); - }} - > - API Key -
-
{ - dropdownTippyRef?.current?.hide(); - onModeChange('inherit'); - }} - > - Inherit -
-
{ - dropdownTippyRef?.current?.hide(); - onModeChange('none'); - }} - > - No Auth -
+ {authModes.map((mode) => { + return ( + <> +
{ + dropdownTippyRef?.current?.hide(); + onModeChange(mode); + }} + > + {humanizeRequestAuthMode(mode)} +
+ + ); + })}
diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index bc6c731f4d..3af1e8a074 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -688,7 +688,7 @@ export const humanizeRequestAuthMode = (mode) => { break; } case 'ntlm': { - label = 'NTLM'; + label = 'NTLM Auth'; break; } case 'oauth2': { From 7f1e656524ff91051c0937603e96d1f37d05cd69 Mon Sep 17 00:00:00 2001 From: Mateusz Pietryga Date: Sat, 25 May 2024 00:09:05 +0200 Subject: [PATCH 2/3] feat: Implement OAuth 1.0 --- package-lock.json | 7 + packages/bruno-app/package.json | 1 + .../RequestPane/Auth/AuthMode/index.js | 2 +- .../RequestPane/Auth/OAuth1/StyledWrapper.js | 20 ++ .../RequestPane/Auth/OAuth1/index.js | 131 +++++++++ .../RequestPane/Auth/OAuth1/inputsConfig.js | 79 ++++++ .../src/components/RequestPane/Auth/index.js | 12 +- .../bruno-app/src/i18n/translation/en.json | 21 ++ .../ReduxStore/slices/collections/index.js | 8 +- .../bruno-app/src/utils/collections/index.js | 24 +- .../bruno-electron/src/ipc/network/index.js | 8 +- .../src/ipc/network/interpolate-vars.js | 4 + .../src/ipc/network/oauth1-helper.js | 266 ++++++++++++++++++ .../src/ipc/network/prepare-request.js | 20 +- packages/bruno-lang/v2/src/bruToJson.js | 38 ++- packages/bruno-lang/v2/src/jsonToBru.js | 22 +- .../bruno-schema/src/collections/index.js | 24 +- 17 files changed, 666 insertions(+), 21 deletions(-) create mode 100644 packages/bruno-app/src/components/RequestPane/Auth/OAuth1/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/RequestPane/Auth/OAuth1/index.js create mode 100644 packages/bruno-app/src/components/RequestPane/Auth/OAuth1/inputsConfig.js create mode 100644 packages/bruno-electron/src/ipc/network/oauth1-helper.js diff --git a/package-lock.json b/package-lock.json index d7ee4064d3..e349ca559d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17392,6 +17392,12 @@ "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", "license": "MIT" }, + "node_modules/oauth-1.0a": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/oauth-1.0a/-/oauth-1.0a-2.2.6.tgz", + "integrity": "sha512-6bkxv3N4Gu5lty4viIcIAnq5GbxECviMBeKR3WX/q87SPQ8E8aursPZUtsXDnxCs787af09WPRBLqYrf/lwoYQ==", + "dev": true + }, "node_modules/oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -24102,6 +24108,7 @@ "html-loader": "^3.0.1", "html-webpack-plugin": "^5.5.0", "mini-css-extract-plugin": "^2.4.5", + "oauth-1.0a": "^2.2.6", "postcss": "8.4.47", "style-loader": "^3.3.1", "tailwindcss": "^3.4.1", diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json index d4fb47bcf4..3d5c71b7b3 100644 --- a/packages/bruno-app/package.json +++ b/packages/bruno-app/package.json @@ -92,6 +92,7 @@ "html-loader": "^3.0.1", "html-webpack-plugin": "^5.5.0", "mini-css-extract-plugin": "^2.4.5", + "oauth-1.0a": "^2.2.6", "postcss": "8.4.47", "style-loader": "^3.3.1", "tailwindcss": "^3.4.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 c5b9694415..53f30ecc16 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js @@ -13,7 +13,7 @@ const AuthMode = ({ item, collection }) => { const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode'); - const authModes = ['awsv4', 'basic', 'bearer', 'digest', 'ntlm', 'oauth2', 'wsse', 'apikey', 'inherit', 'none']; + const authModes = ['awsv4', 'basic', 'bearer', 'digest', 'ntlm', 'oauth1', 'oauth2', 'wsse', 'apikey', 'inherit', 'none']; const Icon = forwardRef((props, ref) => { return ( diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth1/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth1/StyledWrapper.js new file mode 100644 index 0000000000..356d4ecf1e --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth1/StyledWrapper.js @@ -0,0 +1,20 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div` + label { + font-size: 0.8125rem; + } + .single-line-editor-wrapper { + max-width: 400px; + padding: 0.15rem 0.4rem; + border-radius: 3px; + border: solid 1px ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.input.bg}; + } + .file-picker-wrapper { + max-width: 400px; + border-radius: 3px; + } +`; + +export default Wrapper; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth1/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth1/index.js new file mode 100644 index 0000000000..4894d2a4f5 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth1/index.js @@ -0,0 +1,131 @@ +import React, { forwardRef, useCallback, useRef } 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'; +import { inputsConfig } from './inputsConfig'; +import { useTranslation } from 'react-i18next'; +import Dropdown from 'components/Dropdown'; +import { IconCaretDown } from '@tabler/icons'; +import FilePickerEditor from 'components/FilePickerEditor'; +import path from 'path'; +import { isWindowsOS } from 'utils/common/platform'; +import slash from 'utils/common/slash'; + +const OAuth1 = ({ item, collection }) => { + const dispatch = useDispatch(); + const { t } = useTranslation(); + const { storedTheme } = useTheme(); + + const oAuth1 = item.draft ? get(item, 'draft.request.auth.oauth1', {}) : get(item, 'request.auth.oauth1', {}); + + const handleRun = () => dispatch(sendRequest(item, collection.uid)); + const handleSave = () => dispatch(saveRequest(item.uid, collection.uid)); + + const refs = useRef(new Map()); + const setRef = useCallback((ref, key) => { + if (ref) { + refs.current.set(key, ref); + } else { + refs.current.delete(key); + } + }, []); + + const hideDropdown = (key) => { + const dropdown = refs.current.get(key); + if (dropdown?.hide) { + dropdown.hide(); + } + }; + + const handleChange = (key, val) => { + console.log(key, val); + dispatch( + updateAuth({ + mode: 'oauth1', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + ...oAuth1, [key]: val + } + }) + ); + }; + + const relativeFile = (file) => { + if (file) { + if (isWindowsOS()) { + return slash((path.win32.relative(collection.pathname, file))); + } else { + return path.posix.relative(collection.pathname, file); + } + } else { + return ''; + } + }; + + const optionDisplayName = (options, key) => { + const option = (options.find(option => option.key === key)); + return option?.label ? t(option.label) : key; + }; + + return ( + + {inputsConfig.map((input) => { + const { key, label, type, options } = input; + return ( +
+ + {type === 'Dropdown' ? +
+ {optionDisplayName(options, oAuth1[key])} +
)} + onCreate={(ref) => setRef(ref, key)} + placement="bottom-end" + children={options.map((option) => ( +
{ + hideDropdown(key); + handleChange(key, option.key ? option.key : option); + }}> + {option.label ? t(option.label) : option} +
+ ))} + /> +
+ : ''} + {type === 'SingleLineEditor' ? +
+ handleChange(key, val)} + onRun={handleRun} + collection={collection} + item={item} + /> +
+ : ''} + {type === 'FilePickerEditor' ? +
+ handleChange(key, relativeFile(val[0]))} + collection={collection} + /> +
+ : ''} +
+ ); + })} + + ); +}; + +export default OAuth1; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth1/inputsConfig.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth1/inputsConfig.js new file mode 100644 index 0000000000..eefdcd66ec --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth1/inputsConfig.js @@ -0,0 +1,79 @@ +const inputsConfig = [ + { + key: 'consumerKey', + label: 'AUTHORIZATION.OAUTH1.CONSUMER_KEY_FIELD', + type: 'SingleLineEditor' + }, + { + key: 'consumerSecret', + label: 'AUTHORIZATION.OAUTH1.CONSUMER_SECRET_FIELD', + type: 'SingleLineEditor' + }, + { + key: 'requestTokenUrl', + label: 'AUTHORIZATION.OAUTH1.REQUEST_TOKEN_URL_FIELD', + type: 'SingleLineEditor' + }, + { + key: 'accessTokenUrl', + label: 'AUTHORIZATION.OAUTH1.ACCESS_TOKEN_URL_FIELD', + type: 'SingleLineEditor' + }, + { + key: 'authorizeUrl', + label: 'AUTHORIZATION.OAUTH1.AUTHORIZE_URL_FIELD', + type: 'SingleLineEditor' + }, + { + key: 'callbackUrl', + label: 'AUTHORIZATION.OAUTH1.CALLBACK_TOKEN_URL_FIELD', + type: 'SingleLineEditor' + }, + { + key: 'verifier', + label: 'AUTHORIZATION.OAUTH1.OAUTH_VERIFIER_FIELD', + type: 'SingleLineEditor' + }, + { + key: 'accessToken', + label: 'AUTHORIZATION.OAUTH1.ACCESS_TOKEN_FIELD', + type: 'SingleLineEditor' + }, + { + key: 'accessTokenSecret', + label: 'AUTHORIZATION.OAUTH1.ACCESS_TOKEN_SECRET_FIELD', + type: 'SingleLineEditor' + }, + { + key: 'rsaPrivateKey', + label: 'AUTHORIZATION.OAUTH1.RSA_PRIVATE_KEY_FIELD', + type: 'FilePickerEditor' + }, + { + key: 'signatureMethod', + label: 'AUTHORIZATION.OAUTH1.SIGNATURE_METHOD_FIELD', + type: 'Dropdown', + options: ['HMAC-SHA1', 'HMAC-SHA256', 'HMAC-SHA512', 'RSA-SHA1', 'RSA-SHA256', 'RSA-SHA512', 'PLAINTEXT'] + }, + { + key: 'parameterTransmissionMethod', + label: 'AUTHORIZATION.OAUTH1.PARAM_TRANSMISSION_METHOD_FIELD', + type: 'Dropdown', + options: [ + { + key: 'authorization_header', + label: 'AUTHORIZATION.OAUTH1.PARAM_TRANSMISSION_METHOD.AUTHORIZATION_HEADER' + }, + { + key: 'request_body', + label: 'AUTHORIZATION.OAUTH1.PARAM_TRANSMISSION_METHOD.REQUEST_BODY' + }, + { + key: 'query_param', + label: 'AUTHORIZATION.OAUTH1.PARAM_TRANSMISSION_METHOD.QUERY_PARAM' + } + ] + } +]; + +export { inputsConfig }; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/index.js index 743d23267e..868a663d60 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/index.js @@ -1,17 +1,18 @@ import React from 'react'; import get from 'lodash/get'; +import StyledWrapper from './StyledWrapper'; +import { humanizeRequestAuthMode } from 'utils/collections'; import AuthMode from './AuthMode'; import AwsV4Auth from './AwsV4Auth'; import BearerAuth from './BearerAuth'; import BasicAuth from './BasicAuth'; import DigestAuth from './DigestAuth'; +import OAuth1 from './OAuth1'; +import OAuth2 from './OAuth2'; import WsseAuth from './WsseAuth'; import NTLMAuth from './NTLMAuth'; import ApiKeyAuth from './ApiKeyAuth'; -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'); @@ -35,7 +36,10 @@ const Auth = ({ item, collection }) => { } case 'ntlm': { return ; - } + } + case 'oauth1': { + return ; + } case 'oauth2': { return ; } diff --git a/packages/bruno-app/src/i18n/translation/en.json b/packages/bruno-app/src/i18n/translation/en.json index 7dda41e426..ce340ec31f 100644 --- a/packages/bruno-app/src/i18n/translation/en.json +++ b/packages/bruno-app/src/i18n/translation/en.json @@ -16,5 +16,26 @@ "COLLECTION_IMPORT_SUCCESS": "Collection imported successfully", "COLLECTION_IMPORT_ERROR": "An error occurred while importing the collection. Check the logs for more information.", "COLLECTION_OPEN_ERROR": "An error occurred while opening the collection" + }, + "AUTHORIZATION": { + "OAUTH1": { + "CONSUMER_KEY_FIELD": "Consumer Key", + "CONSUMER_SECRET_FIELD": "Consumer Secret", + "REQUEST_TOKEN_URL_FIELD": "Request Token URL", + "ACCESS_TOKEN_URL_FIELD": "Access Token URL", + "AUTHORIZE_URL_FIELD": "Authorization URL", + "CALLBACK_TOKEN_URL_FIELD": "Callback URL", + "OAUTH_VERIFIER_FIELD": "Verifier code", + "ACCESS_TOKEN_FIELD": "Access Token", + "ACCESS_TOKEN_SECRET_FIELD": "Access Token Secret", + "RSA_PRIVATE_KEY_FIELD": "RSA Private Key", + "SIGNATURE_METHOD_FIELD": "Signature Method", + "PARAM_TRANSMISSION_METHOD_FIELD": "Parameter Transmission Method", + "PARAM_TRANSMISSION_METHOD": { + "AUTHORIZATION_HEADER": "Authorization Header", + "REQUEST_BODY": "Request Body", + "QUERY_PARAM": "Query Parameters" + } + } } } 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 d4ad489218..8fc79b3475 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -476,7 +476,11 @@ export const collectionsSlice = createSlice({ case 'ntlm': item.draft.request.auth.mode = 'ntlm'; item.draft.request.auth.ntlm = action.payload.content; - break; + break; + case 'oauth1': + item.draft.request.auth.mode = 'oauth1'; + item.draft.request.auth.oauth1 = action.payload.content; + break; case 'oauth2': item.draft.request.auth.mode = 'oauth2'; item.draft.request.auth.oauth2 = action.payload.content; @@ -1150,7 +1154,7 @@ export const collectionsSlice = createSlice({ break; case 'ntlm': set(collection, 'root.request.auth.ntlm', action.payload.content); - break; + break; case 'oauth2': set(collection, 'root.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 3af1e8a074..d31c8eb6ed 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -346,7 +346,23 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} password: get(si.request, 'auth.ntlm.password', ''), domain: get(si.request, 'auth.ntlm.domain', '') }; - break; + break; + case 'oauth1': + di.request.auth.oauth1 = { + consumerKey: get(si.request, 'auth.oauth1.consumerKey', ''), + consumerSecret: get(si.request, 'auth.oauth1.consumerSecret', ''), + requestTokenUrl: get(si.request, 'auth.oauth1.requestTokenUrl', ''), + accessTokenUrl: get(si.request, 'auth.oauth1.accessTokenUrl', ''), + authorizeUrl: get(si.request, 'auth.oauth1.authorizeUrl', ''), + callbackUrl: get(si.request, 'auth.oauth1.callbackUrl', ''), + verifier: get(si.request, 'auth.oauth1.verifier', ''), + accessToken: get(si.request, 'auth.oauth1.accessToken', ''), + accessTokenSecret: get(si.request, 'auth.oauth1.accessTokenSecret', ''), + rsaPrivateKey: get(si.request, 'auth.oauth1.rsaPrivateKey', ''), + parameterTransmissionMethod: get(si.request, 'auth.oauth1.parameterTransmissionMethod', ''), + signatureMethod: get(si.request, 'auth.oauth1.signatureMethod', '') + }; + break; case 'oauth2': let grantType = get(si.request, 'auth.oauth2.grantType', ''); switch (grantType) { @@ -690,7 +706,11 @@ export const humanizeRequestAuthMode = (mode) => { case 'ntlm': { label = 'NTLM Auth'; break; - } + } + case 'oauth1': { + label = 'OAuth 1.0'; + break; + } case 'oauth2': { label = 'OAuth 2.0'; break; diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 1865426e0e..a1c3a7f674 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -30,6 +30,7 @@ const { addDigestInterceptor } = require('./digestauth-helper'); const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../../utils/proxy-util'); const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem'); const { getCookieStringForUrl, addCookieToJar, getDomainsWithCookies } = require('../../utils/cookies'); +const { addOAuth1Authorization } = require('./oauth1-helper'); const { resolveOAuth2AuthorizationCodeAccessToken, transformClientCredentialsRequest, @@ -274,14 +275,17 @@ const configureRequest = async ( }); } - let axiosInstance = makeAxiosInstance(); - + if (request.ntlmConfig) { axiosInstance=NtlmClient(request.ntlmConfig,axiosInstance) delete request.ntlmConfig; } + if (request.oauth1) { + interpolateVars(request, envVars, runtimeVariables, processEnvVars); + await addOAuth1Authorization(request, collectionPath); + } if (request.oauth2) { let requestCopy = cloneDeep(request); diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index 2b46327afb..3bceefdfbd 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -155,6 +155,10 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc delete request.auth; } + if (request?.oauth1) { + Object.keys(request.oauth1).forEach(key => request.oauth1[key] = _interpolate(request.oauth1[key])); + } + if (request?.oauth2?.grantType) { let username, password, scope, clientId, clientSecret; switch (request.oauth2.grantType) { diff --git a/packages/bruno-electron/src/ipc/network/oauth1-helper.js b/packages/bruno-electron/src/ipc/network/oauth1-helper.js new file mode 100644 index 0000000000..b99669c39c --- /dev/null +++ b/packages/bruno-electron/src/ipc/network/oauth1-helper.js @@ -0,0 +1,266 @@ +const OAuth = require('oauth-1.0a'); +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); +const axios = require('axios'); +const { BrowserWindow } = require('electron'); +const { preferencesUtil } = require('../../store/preferences'); + +const addOAuth1Authorization = async (request, collectionPath) => { + + const absoluteFilePath = (file) => { + return path.isAbsolute(file) ? file : path.join(collectionPath, file) + } + + const hash_function = (signatureMethod) => { + + // https://oauth.net/core/1.0a/#anchor15 + // 9.2. HMAC-SHA1 + if (signatureMethod.startsWith('HMAC')) { + return (base_string, key) => { + return crypto + .createHmac(signatureMethod.slice('HMAC-'.length).toLowerCase(), key) + .update(base_string) + .digest('base64') + } + } + + // https://oauth.net/core/1.0a/#anchor18 + // 9.3. RSA-SHA1 + if (signatureMethod.startsWith('RSA')) { + return (base_string, unused_key) => { + const rsaPrivateKey = fs.readFileSync(absoluteFilePath(request.oauth1.rsaPrivateKey), 'utf8'); + return crypto + .createSign(signatureMethod) + .update(base_string) + .sign(rsaPrivateKey, 'base64') + } + } + + // https://oauth.net/core/1.0a/#anchor21 + // 9.4. PLAINTEXT + if(signatureMethod === 'PLAINTEXT') { + return (base_string, key) => { + return key; + } + } + + throw new Error('Unsupported signature method'); + } + + const oauth = OAuth({ + consumer: { + key: request.oauth1.consumerKey, + secret: request.oauth1.consumerSecret + }, + signature_method: request.oauth1.signatureMethod, + hash_function: hash_function(request.oauth1.signatureMethod) + }); + + // https://oauth.net/core/1.0a/#auth_step1 + // 6.1. Obtaining an Unauthorized Request Token + const getRequestToken = async () => { + try { + const requestTokenRequest = { + url: request.oauth1.requestTokenUrl, + method: 'POST', + data: { + oauth_callback: request.oauth1.callbackUrl + } + }; + const signingToken = { + // requestToken does not need signing + } + const authHeader = oauth.toHeader(oauth.authorize(requestTokenRequest, signingToken)); + console.log("Requesting temporary request token from", requestTokenRequest.url); + const requestTokenResponse = await axios.post(requestTokenRequest.url, requestTokenRequest.data, { headers: authHeader }); + const formattedResponse = Object.fromEntries(new URLSearchParams(requestTokenResponse.data)); + console.info('Request Token Response:', requestTokenResponse.status, formattedResponse); + return formattedResponse; + } catch (error) { + console.error('Error obtaining request token:', error.message); + throw new Error(error.message); + } + }; + + // https://oauth.net/core/1.0a/#auth_step2 + // 6.2. Obtaining User Authorization + const requestUserAuthorization = (oauthToken) => { + return new Promise(async (resolve, reject) => { + const matchesCallbackUrl = (url, callbackUrl) => { + return url ? url.href.startsWith(callbackUrl.href) : false; + }; + + let finalUrl; + let window; + const onRedirect = (_, url) => { + try { + if (matchesCallbackUrl(new URL(url), new URL(request.oauth1.callbackUrl))) { + finalUrl = url; + window.close(); + } + } catch (error) { + console.error("Error parsing redirect URL:", error); + } + }; + + try { + window = new BrowserWindow({ + webPreferences: { + nodeIntegration: false + } + }); + window.on('ready-to-show', () => window.show()); + window.webContents.on('certificate-error', (event, url, error, certificate, callback) => { + event.preventDefault(); + callback(!preferencesUtil.shouldVerifyTls()); + }); + window.webContents.on('did-navigate', onRedirect); + window.webContents.on('will-redirect', onRedirect); + + window.on('close', () => { + window.webContents.removeListener('did-navigate', onRedirect); + window.webContents.removeListener('will-redirect', onRedirect); + + if (finalUrl) { + try { + const callbackUrlWithVerifier = new URL(finalUrl); + const params = Object.fromEntries(callbackUrlWithVerifier.searchParams); + console.log("Authorization successful:", params); + resolve(params); + } catch (error) { + console.error("Error processing final URL:", error); + reject(new Error('Invalid authorization response')); + } + } else { + console.warn('Authorization window closed by user'); + reject(new Error('Authorization window closed by user')); + } + }); + + const authorizeUrl = new URL(request.oauth1.authorizeUrl); + authorizeUrl.searchParams.append('oauth_token', oauthToken); + + console.log("Requesting user authorization from", authorizeUrl.toString()); + window.loadURL(authorizeUrl.toString()).catch((error) => { + console.error("Error loading authorization URL:", error); + reject(new Error('Failed to load authorization URL')); + window.close(); + }); + } catch (error) { + console.error("Error during user authorization setup:", error); + if (window) window.close(); + reject(new Error('User authorization setup failed')); + } + }); + }; + + // https://oauth.net/core/1.0a/#auth_step3 + // 6.3. Obtaining an Access Token + const exchangeRequestTokenForAccessToken = async (oauthToken, oauthTokenSecret, verifier) => { + try { + const accessTokenRequestData = { + url: request.oauth1.accessTokenUrl, + method: 'POST', + data: { + oauth_token: oauthToken, + oauth_verifier: verifier + } + }; + const signingToken = { + key: request.oauth1.consumerSecret, + secret: oauthTokenSecret + } + const authHeader = oauth.toHeader(oauth.authorize(accessTokenRequestData, signingToken)); + console.log("Requesting Access Token from", accessTokenRequestData.url); + const accessTokenResponse = await axios.post(accessTokenRequestData.url, {},{ headers: authHeader }); + const formattedResponse = Object.fromEntries(new URLSearchParams(accessTokenResponse.data)); + console.log("Access Token Response", accessTokenResponse.status, formattedResponse); + return formattedResponse; + } catch (error) { + console.error("Invalid Access Token Response", error.code, error.message); + throw new Error(error.message); + } + }; + + // https://oauth.net/core/1.0a/#anchor13 + // 9.1.1. Normalize Request Parameters + const isPostWithUrlEncodedFormData = (request) => { + return request.headers['content-type'] === 'application/x-www-form-urlencoded' && request.method === 'POST'; + } + + // https://oauth.net/core/1.0a/#anchor12 + // 7. Accessing Protected Resources + const evaluateOAuth1Parameters = (request, accessToken, accessTokenSecret) => { + let requestData; + if (isPostWithUrlEncodedFormData(request)) { + requestData = { + url: request.url, + method: request.method, + data: Object.fromEntries(new URLSearchParams(request.data).entries()) + } + } else { + requestData = { + url: request.url, + method: request.method, + } + } + + const signingToken = { + key: accessToken, + secret: accessTokenSecret + } + + return oauth.authorize(requestData, signingToken); + } + + // https://oauth.net/core/1.0a/#consumer_req_param + // 5.2. Consumer Request Parameters + const addAuthorizationToRequest = (request, oauth1RequestParameters, parameterTransmissionMethod) => { + switch (parameterTransmissionMethod) { + case 'authorization_header': { + const authHeader = oauth.toHeader(oauth1RequestParameters); + request.headers['authorization'] = authHeader.Authorization; + break; + } + case 'request_body': { + if(! isPostWithUrlEncodedFormData(request)) { + throw new Error('"Parameter Transmission Method: Request Body" is only supported ' + + 'for POST request with a content-type of application/x-www-form-urlencoded.'); + } + request.data = new URLSearchParams(oauth1RequestParameters).toString(); + break; + } + case 'query_param': { + const url = new URL(request.url); + Object.entries(oauth1RequestParameters).forEach(([key, value]) => { + url.searchParams.append(key, value); + }) + request.url = url.toString(); + break; + } + } + } + + // With Access Token provided, we may skip directly to authorizing of user's request + // With verifier provided - we may skip user authorization (third leg) step + let { accessToken, accessTokenSecret, verifier, parameterTransmissionMethod } = request.oauth1; + + try { + if(!accessToken || !accessTokenSecret) { + const { oauth_token: oauthToken, oauth_token_secret: oauthTokenSecret} = await getRequestToken(); + if(!verifier) { + ({ oauth_verifier: verifier } = await requestUserAuthorization(oauthToken)); + } + ({ oauth_token: accessToken, oauth_token_secret: accessTokenSecret } = await exchangeRequestTokenForAccessToken(oauthToken, oauthTokenSecret, verifier)); + } + const requestParameters = evaluateOAuth1Parameters(request, accessToken, accessTokenSecret); + addAuthorizationToRequest(request, requestParameters, parameterTransmissionMethod) + + } catch (error) { + console.error("OAuth flow failed", error.message); + throw error; + } +}; + +module.exports = { addOAuth1Authorization }; \ No newline at end of file diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 6c7672e7d6..d5fe3b6b81 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -40,7 +40,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { password: get(collectionAuth, 'ntlm.password'), domain: get(collectionAuth, 'ntlm.domain') }; - break; + break; case 'wsse': const username = get(request, 'auth.wsse.username', ''); const password = get(request, 'auth.wsse.password', ''); @@ -103,7 +103,23 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { password: get(request, 'auth.ntlm.password'), domain: get(request, 'auth.ntlm.domain') }; - break; + break; + case 'oauth1': + axiosRequest.oauth1 = { + consumerKey: get(request, 'auth.oauth1.consumerKey'), + consumerSecret: get(request, 'auth.oauth1.consumerSecret'), + requestTokenUrl: get(request, 'auth.oauth1.requestTokenUrl'), + accessTokenUrl: get(request, 'auth.oauth1.accessTokenUrl'), + authorizeUrl: get(request, 'auth.oauth1.authorizeUrl'), + callbackUrl: get(request, 'auth.oauth1.callbackUrl'), + verifier: get(request, 'auth.oauth1.verifier'), + accessToken: get(request, 'auth.oauth1.accessToken'), + accessTokenSecret: get(request, 'auth.oauth1.accessTokenSecret'), + rsaPrivateKey: get(request, 'auth.oauth1.rsaPrivateKey'), + parameterTransmissionMethod: get(request, 'auth.oauth1.parameterTransmissionMethod'), + signatureMethod: get(request, 'auth.oauth1.signatureMethod') + }; + break; case 'oauth2': const grantType = get(request, 'auth.oauth2.grantType'); switch (grantType) { diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index 2fe5fb472a..f2ed868d41 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 | params | headers | auths | bodies | varsandassert | script | tests | docs)* - auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth2 | authwsse | authapikey + auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth1 | authOAuth2 | authwsse | authapikey bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body bodyforms = bodyformurlencoded | bodymultipart params = paramspath | paramsquery @@ -88,6 +88,7 @@ const grammar = ohm.grammar(`Bru { authbearer = "auth:bearer" dictionary authdigest = "auth:digest" dictionary authNTLM = "auth:ntlm" dictionary + authOAuth1 = "auth:oauth1" dictionary authOAuth2 = "auth:oauth2" dictionary authwsse = "auth:wsse" dictionary authapikey = "auth:apikey" dictionary @@ -470,7 +471,40 @@ const sem = grammar.createSemantics().addAttribute('ast', { } } }; - }, + }, + authOAuth1(_1, dictionary) { + const auth = mapPairListToKeyValPairs(dictionary.ast, false); + const consumerKey = _.find(auth, { name: 'consumerKey' }); + const consumerSecret = _.find(auth, { name: 'consumerSecret' }); + const requestTokenUrl = _.find(auth, { name: 'requestTokenUrl' }); + const accessTokenUrl = _.find(auth, { name: 'accessTokenUrl' }); + const authorizeUrl = _.find(auth, { name: 'authorizeUrl' }); + const callbackUrl = _.find(auth, { name: 'callbackUrl' }); + const verifier = _.find(auth, { name: 'verifier' }); + const accessToken = _.find(auth, { name: 'accessToken' }); + const accessTokenSecret = _.find(auth, { name: 'accessTokenSecret' }); + const rsaPrivateKey = _.find(auth, { name: 'rsaPrivateKey' }); + const parameterTransmissionMethod = _.find(auth, { name: 'parameterTransmissionMethod' }); + const signatureMethod = _.find(auth, { name: 'signatureMethod' }); + return { + auth: { + oauth1: { + consumerKey: consumerKey ? consumerKey.value : '', + consumerSecret: consumerSecret ? consumerSecret.value : '', + requestTokenUrl: requestTokenUrl ? requestTokenUrl.value : '', + accessTokenUrl: accessTokenUrl ? accessTokenUrl.value : '', + authorizeUrl: authorizeUrl ? authorizeUrl.value : '', + callbackUrl: callbackUrl ? callbackUrl.value : '', + verifier: verifier ? verifier.value : '', + accessToken: accessToken ? accessToken.value : '', + accessTokenSecret: accessTokenSecret ? accessTokenSecret.value : '', + rsaPrivateKey: rsaPrivateKey ? rsaPrivateKey.value : '', + parameterTransmissionMethod: parameterTransmissionMethod ? parameterTransmissionMethod.value : '', + signatureMethod: signatureMethod ? signatureMethod.value : '' + } + } + }; + }, authOAuth2(_1, dictionary) { const auth = mapPairListToKeyValPairs(dictionary.ast, false); const grantTypeKey = _.find(auth, { name: 'grant_type' }); diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 62b31c2f99..9fa0601090 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -165,7 +165,6 @@ ${indentString(`password: ${auth?.digest?.password || ''}`)} `; } - if (auth && auth.ntlm) { bru += `auth:ntlm { ${indentString(`username: ${auth?.ntlm?.username || ''}`)} @@ -175,7 +174,26 @@ ${indentString(`domain: ${auth?.ntlm?.domain || ''}`)} } `; - } + } + + if (auth && auth.oauth1) { + bru += `auth:oauth1 { +${indentString(`consumerKey: ${auth?.oauth1?.consumerKey || ''}`)} +${indentString(`consumerSecret: ${auth?.oauth1?.consumerSecret || ''}`)} +${indentString(`requestTokenUrl: ${auth?.oauth1?.requestTokenUrl || ''}`)} +${indentString(`accessTokenUrl: ${auth?.oauth1?.accessTokenUrl || ''}`)} +${indentString(`authorizeUrl: ${auth?.oauth1?.authorizeUrl || ''}`)} +${indentString(`callbackUrl: ${auth?.oauth1?.callbackUrl || ''}`)} +${indentString(`verifier: ${auth?.oauth1?.verifier || ''}`)} +${indentString(`accessToken: ${auth?.oauth1?.accessToken || ''}`)} +${indentString(`accessTokenSecret: ${auth?.oauth1?.accessTokenSecret || ''}`)} +${indentString(`rsaPrivateKey: ${auth?.oauth1?.rsaPrivateKey || ''}`)} +${indentString(`parameterTransmissionMethod: ${auth?.oauth1?.parameterTransmissionMethod || ''}`)} +${indentString(`signatureMethod: ${auth?.oauth1?.signatureMethod || ''}`)} +} + +`; + } if (auth && auth.oauth2) { switch (auth?.oauth2?.grantType) { diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index b6e044ae47..fa40bb3abf 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -127,8 +127,6 @@ const authDigestSchema = Yup.object({ .noUnknown(true) .strict(); - - const authNTLMSchema = Yup.object({ username: Yup.string().nullable(), password: Yup.string().nullable(), @@ -136,7 +134,7 @@ const authDigestSchema = Yup.object({ }) .noUnknown(true) - .strict(); + .strict(); const authApiKeySchema = Yup.object({ key: Yup.string().nullable(), @@ -146,6 +144,23 @@ const authApiKeySchema = Yup.object({ .noUnknown(true) .strict(); +const oauth1Schema = Yup.object({ + consumerKey: Yup.string().required(), + consumerSecret: Yup.string().required(), + requestTokenUrl: Yup.string().nullable(), + accessTokenUrl: Yup.string().nullable(), + authorizeUrl: Yup.string().nullable(), + callbackUrl: Yup.string().nullable(), + verifier: Yup.string().nullable(), + accessToken: Yup.string().nullable(), + accessTokenSecret: Yup.string().nullable(), + rsaPrivateKey: Yup.string().nullable(), + parameterTransmissionMethod: Yup.string().oneOf(['authorization_header', 'request_body', 'query_param']).nullable(), + signatureMethod: Yup.string().oneOf(['HMAC-SHA1', 'HMAC-SHA256', 'HMAC-SHA512', 'RSA-SHA1', 'RSA-SHA256', 'RSA-SHA512', 'PLAINTEXT']).nullable() +}) + .noUnknown(true) + .strict(); + const oauth2Schema = Yup.object({ grantType: Yup.string() .oneOf(['client_credentials', 'password', 'authorization_code']) @@ -206,13 +221,14 @@ const oauth2Schema = Yup.object({ const authSchema = Yup.object({ mode: Yup.string() - .oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest', 'ntlm', 'oauth2', 'wsse', 'apikey']) + .oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest', 'ntlm', 'oauth1', 'oauth2', 'wsse', 'apikey']) .required('mode is required'), awsv4: authAwsV4Schema.nullable(), basic: authBasicSchema.nullable(), bearer: authBearerSchema.nullable(), ntlm: authNTLMSchema.nullable(), digest: authDigestSchema.nullable(), + oauth1: oauth1Schema.nullable(), oauth2: oauth2Schema.nullable(), wsse: authWsseSchema.nullable(), apikey: authApiKeySchema.nullable() From d7e5cdb21cffecfb87af312b9dc769daff9e16ba Mon Sep 17 00:00:00 2001 From: Mateusz Pietryga Date: Sun, 15 Sep 2024 19:47:22 +0200 Subject: [PATCH 3/3] feat: Implement OAuth 1.0 - collection level --- .../CollectionSettings/Auth/AuthMode/index.js | 2 +- .../Auth/OAuth1/StyledWrapper.js | 20 +++ .../CollectionSettings/Auth/OAuth1/index.js | 127 ++++++++++++++++++ .../Auth/OAuth1/inputsConfig.js | 79 +++++++++++ .../CollectionSettings/Auth/index.js | 7 +- .../ReduxStore/slices/collections/index.js | 3 + .../src/ipc/network/prepare-request.js | 16 +++ .../bruno-lang/v2/src/collectionBruToJson.js | 38 +++++- .../bruno-lang/v2/src/jsonToCollectionBru.js | 19 +++ 9 files changed, 306 insertions(+), 5 deletions(-) create mode 100644 packages/bruno-app/src/components/CollectionSettings/Auth/OAuth1/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/CollectionSettings/Auth/OAuth1/index.js create mode 100644 packages/bruno-app/src/components/CollectionSettings/Auth/OAuth1/inputsConfig.js diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js index 08b59aae30..04ce278959 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js @@ -13,7 +13,7 @@ const AuthMode = ({ collection }) => { const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); const authMode = get(collection, 'root.request.auth.mode'); - const authModes = ['awsv4', 'basic', 'bearer', 'digest', 'ntlm', 'oauth2', 'wsse', 'apikey', 'none']; + const authModes = ['awsv4', 'basic', 'bearer', 'digest', 'ntlm', 'oauth1', 'oauth2', 'wsse', 'apikey', 'none']; const Icon = forwardRef((props, ref) => { return ( diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth1/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth1/StyledWrapper.js new file mode 100644 index 0000000000..356d4ecf1e --- /dev/null +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth1/StyledWrapper.js @@ -0,0 +1,20 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div` + label { + font-size: 0.8125rem; + } + .single-line-editor-wrapper { + max-width: 400px; + padding: 0.15rem 0.4rem; + border-radius: 3px; + border: solid 1px ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.input.bg}; + } + .file-picker-wrapper { + max-width: 400px; + border-radius: 3px; + } +`; + +export default Wrapper; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth1/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth1/index.js new file mode 100644 index 0000000000..6c927be61a --- /dev/null +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth1/index.js @@ -0,0 +1,127 @@ +import React, { forwardRef, useCallback, useRef } from 'react'; +import get from 'lodash/get'; +import { useTheme } from 'providers/Theme'; +import { useDispatch } from 'react-redux'; +import SingleLineEditor from 'components/SingleLineEditor'; +import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections'; +import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions'; +import StyledWrapper from './StyledWrapper'; +import { inputsConfig } from './inputsConfig'; +import { useTranslation } from 'react-i18next'; +import Dropdown from 'components/Dropdown'; +import { IconCaretDown } from '@tabler/icons'; +import FilePickerEditor from 'components/FilePickerEditor'; +import path from 'path'; +import { isWindowsOS } from 'utils/common/platform'; +import slash from 'utils/common/slash'; + +const OAuth1 = ({ collection }) => { + const dispatch = useDispatch(); + const { t } = useTranslation(); + const { storedTheme } = useTheme(); + + const oAuth1 = get(collection, 'root.request.auth.oauth1', {}); + + const handleSave = () => dispatch(saveCollectionRoot(collection.uid)); + + const refs = useRef(new Map()); + const setRef = useCallback((ref, key) => { + if (ref) { + refs.current.set(key, ref); + } else { + refs.current.delete(key); + } + }, []); + + const hideDropdown = (key) => { + const dropdown = refs.current.get(key); + if (dropdown?.hide) { + dropdown.hide(); + } + }; + + const handleChange = (key, val) => { + console.log(key, val); + dispatch( + updateCollectionAuth({ + mode: 'oauth1', + collectionUid: collection.uid, + content: { + ...oAuth1, [key]: val + } + }) + ); + }; + + const relativeFile = (file) => { + if (file) { + if (isWindowsOS()) { + return slash((path.win32.relative(collection.pathname, file))); + } else { + return path.posix.relative(collection.pathname, file); + } + } else { + return ''; + } + }; + + const optionDisplayName = (options, key) => { + const option = (options.find(option => option.key === key)); + return option?.label ? t(option.label) : key; + }; + + return ( + + {inputsConfig.map((input) => { + const { key, label, type, options } = input; + return ( +
+ + {type === 'Dropdown' ? +
+ {optionDisplayName(options, oAuth1[key])} +
)} + onCreate={(ref) => setRef(ref, key)} + placement="bottom-end" + children={options.map((option) => ( +
{ + hideDropdown(key); + handleChange(key, option.key ? option.key : option); + }}> + {option.label ? t(option.label) : option} +
+ ))} + /> +
+ : ''} + {type === 'SingleLineEditor' ? +
+ handleChange(key, val)} + collection={collection} + /> +
+ : ''} + {type === 'FilePickerEditor' ? +
+ handleChange(key, relativeFile(val[0]))} + collection={collection} + /> +
+ : ''} +
+ ); + })} + + ); +}; + +export default OAuth1; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth1/inputsConfig.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth1/inputsConfig.js new file mode 100644 index 0000000000..eefdcd66ec --- /dev/null +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth1/inputsConfig.js @@ -0,0 +1,79 @@ +const inputsConfig = [ + { + key: 'consumerKey', + label: 'AUTHORIZATION.OAUTH1.CONSUMER_KEY_FIELD', + type: 'SingleLineEditor' + }, + { + key: 'consumerSecret', + label: 'AUTHORIZATION.OAUTH1.CONSUMER_SECRET_FIELD', + type: 'SingleLineEditor' + }, + { + key: 'requestTokenUrl', + label: 'AUTHORIZATION.OAUTH1.REQUEST_TOKEN_URL_FIELD', + type: 'SingleLineEditor' + }, + { + key: 'accessTokenUrl', + label: 'AUTHORIZATION.OAUTH1.ACCESS_TOKEN_URL_FIELD', + type: 'SingleLineEditor' + }, + { + key: 'authorizeUrl', + label: 'AUTHORIZATION.OAUTH1.AUTHORIZE_URL_FIELD', + type: 'SingleLineEditor' + }, + { + key: 'callbackUrl', + label: 'AUTHORIZATION.OAUTH1.CALLBACK_TOKEN_URL_FIELD', + type: 'SingleLineEditor' + }, + { + key: 'verifier', + label: 'AUTHORIZATION.OAUTH1.OAUTH_VERIFIER_FIELD', + type: 'SingleLineEditor' + }, + { + key: 'accessToken', + label: 'AUTHORIZATION.OAUTH1.ACCESS_TOKEN_FIELD', + type: 'SingleLineEditor' + }, + { + key: 'accessTokenSecret', + label: 'AUTHORIZATION.OAUTH1.ACCESS_TOKEN_SECRET_FIELD', + type: 'SingleLineEditor' + }, + { + key: 'rsaPrivateKey', + label: 'AUTHORIZATION.OAUTH1.RSA_PRIVATE_KEY_FIELD', + type: 'FilePickerEditor' + }, + { + key: 'signatureMethod', + label: 'AUTHORIZATION.OAUTH1.SIGNATURE_METHOD_FIELD', + type: 'Dropdown', + options: ['HMAC-SHA1', 'HMAC-SHA256', 'HMAC-SHA512', 'RSA-SHA1', 'RSA-SHA256', 'RSA-SHA512', 'PLAINTEXT'] + }, + { + key: 'parameterTransmissionMethod', + label: 'AUTHORIZATION.OAUTH1.PARAM_TRANSMISSION_METHOD_FIELD', + type: 'Dropdown', + options: [ + { + key: 'authorization_header', + label: 'AUTHORIZATION.OAUTH1.PARAM_TRANSMISSION_METHOD.AUTHORIZATION_HEADER' + }, + { + key: 'request_body', + label: 'AUTHORIZATION.OAUTH1.PARAM_TRANSMISSION_METHOD.REQUEST_BODY' + }, + { + key: 'query_param', + label: 'AUTHORIZATION.OAUTH1.PARAM_TRANSMISSION_METHOD.QUERY_PARAM' + } + ] + } +]; + +export { inputsConfig }; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/index.js index c19ae98738..8228686e7f 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/index.js @@ -11,9 +11,9 @@ import ApiKeyAuth from './ApiKeyAuth/'; import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; import OAuth2 from './OAuth2'; +import OAuth1 from 'components/CollectionSettings/Auth/OAuth1'; import NTLMAuth from './NTLMAuth'; - const Auth = ({ collection }) => { const authMode = get(collection, 'root.request.auth.mode'); const dispatch = useDispatch(); @@ -36,7 +36,10 @@ const Auth = ({ collection }) => { } case 'ntlm': { return ; - } + } + case 'oauth1': { + return ; + } case 'oauth2': { return ; } 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 8fc79b3475..5f6c8ab6c6 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -1155,6 +1155,9 @@ export const collectionsSlice = createSlice({ case 'ntlm': set(collection, 'root.request.auth.ntlm', action.payload.content); break; + case 'oauth1': + set(collection, 'root.request.auth.oauth1', action.payload.content); + break; case 'oauth2': set(collection, 'root.request.auth.oauth2', action.payload.content); break; diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index d5fe3b6b81..648090365c 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -67,6 +67,22 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { axiosRequest.apiKeyAuthValueForQueryParams = apiKeyAuth; } break; + case 'oauth1': + axiosRequest.oauth1 = { + consumerKey: get(collectionAuth, 'oauth1.consumerKey'), + consumerSecret: get(collectionAuth, 'oauth1.consumerSecret'), + requestTokenUrl: get(collectionAuth, 'oauth1.requestTokenUrl'), + accessTokenUrl: get(collectionAuth, 'oauth1.accessTokenUrl'), + authorizeUrl: get(collectionAuth, 'oauth1.authorizeUrl'), + callbackUrl: get(collectionAuth, 'oauth1.callbackUrl'), + verifier: get(collectionAuth, 'oauth1.verifier'), + accessToken: get(collectionAuth, 'oauth1.accessToken'), + accessTokenSecret: get(collectionAuth, 'oauth1.accessTokenSecret'), + rsaPrivateKey: get(collectionAuth, 'oauth1.rsaPrivateKey'), + parameterTransmissionMethod: get(collectionAuth, 'oauth1.parameterTransmissionMethod'), + signatureMethod: get(collectionAuth, 'oauth1.signatureMethod') + }; + break; } } diff --git a/packages/bruno-lang/v2/src/collectionBruToJson.js b/packages/bruno-lang/v2/src/collectionBruToJson.js index 61d373d91e..65a8c2881f 100644 --- a/packages/bruno-lang/v2/src/collectionBruToJson.js +++ b/packages/bruno-lang/v2/src/collectionBruToJson.js @@ -4,7 +4,7 @@ const { outdentString } = require('../../v1/src/utils'); const grammar = ohm.grammar(`Bru { BruFile = (meta | query | headers | auth | auths | vars | script | tests | docs)* - auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM |authOAuth2 | authwsse | authapikey + auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth1 | authOAuth2 | authwsse | authapikey nl = "\\r"? "\\n" st = " " | "\\t" @@ -43,6 +43,7 @@ const grammar = ohm.grammar(`Bru { authbearer = "auth:bearer" dictionary authdigest = "auth:digest" dictionary authNTLM = "auth:ntlm" dictionary + authOAuth1 = "auth:oauth1" dictionary authOAuth2 = "auth:oauth2" dictionary authwsse = "auth:wsse" dictionary authapikey = "auth:apikey" dictionary @@ -266,6 +267,39 @@ const sem = grammar.createSemantics().addAttribute('ast', { } }; }, + authOAuth1(_1, dictionary) { + const auth = mapPairListToKeyValPairs(dictionary.ast, false); + const consumerKey = _.find(auth, { name: 'consumerKey' }); + const consumerSecret = _.find(auth, { name: 'consumerSecret' }); + const requestTokenUrl = _.find(auth, { name: 'requestTokenUrl' }); + const accessTokenUrl = _.find(auth, { name: 'accessTokenUrl' }); + const authorizeUrl = _.find(auth, { name: 'authorizeUrl' }); + const verifier = _.find(auth, { name: 'verifier' }); + const callbackUrl = _.find(auth, { name: 'callbackUrl' }); + const accessToken = _.find(auth, { name: 'accessToken' }); + const accessTokenSecret = _.find(auth, { name: 'accessTokenSecret' }); + const rsaPrivateKey = _.find(auth, { name: 'rsaPrivateKey' }); + const signatureMethod = _.find(auth, { name: 'signatureMethod' }); + const parameterTransmissionMethod = _.find(auth, { name: 'parameterTransmissionMethod' }); + return { + auth: { + oauth1: { + consumerKey: consumerKey ? consumerKey.value : '', + consumerSecret: consumerSecret ? consumerSecret.value : '', + requestTokenUrl: requestTokenUrl ? requestTokenUrl.value : '', + accessTokenUrl: accessTokenUrl ? accessTokenUrl.value : '', + authorizeUrl: authorizeUrl ? authorizeUrl.value : '', + callbackUrl: callbackUrl ? callbackUrl.value : '', + verifier: verifier ? verifier.value : '', + accessToken: accessToken ? accessToken.value : '', + accessTokenSecret: accessTokenSecret ? accessTokenSecret.value : '', + rsaPrivateKey: rsaPrivateKey ? rsaPrivateKey.value : '', + parameterTransmissionMethod: parameterTransmissionMethod ? parameterTransmissionMethod.value : '', + signatureMethod: signatureMethod ? signatureMethod.value : '' + } + } + }; + }, authOAuth2(_1, dictionary) { const auth = mapPairListToKeyValPairs(dictionary.ast, false); const grantTypeKey = _.find(auth, { name: 'grant_type' }); @@ -330,7 +364,7 @@ const sem = grammar.createSemantics().addAttribute('ast', { } } } - }, + }, authapikey(_1, dictionary) { const auth = mapPairListToKeyValPairs(dictionary.ast, false); diff --git a/packages/bruno-lang/v2/src/jsonToCollectionBru.js b/packages/bruno-lang/v2/src/jsonToCollectionBru.js index c2a843dc6e..ad1cf1cfbb 100644 --- a/packages/bruno-lang/v2/src/jsonToCollectionBru.js +++ b/packages/bruno-lang/v2/src/jsonToCollectionBru.js @@ -140,6 +140,25 @@ ${indentString(`key: ${auth?.apikey?.key || ''}`)} ${indentString(`value: ${auth?.apikey?.value || ''}`)} ${indentString(`placement: ${auth?.apikey?.placement || ''}`)} } +`; + } + + if (auth && auth.oauth1) { + bru += `auth:oauth1 { +${indentString(`consumerKey: ${auth?.oauth1?.consumerKey || ''}`)} +${indentString(`consumerSecret: ${auth?.oauth1?.consumerSecret || ''}`)} +${indentString(`requestTokenUrl: ${auth?.oauth1?.requestTokenUrl || ''}`)} +${indentString(`accessTokenUrl: ${auth?.oauth1?.accessTokenUrl || ''}`)} +${indentString(`authorizeUrl: ${auth?.oauth1?.authorizeUrl || ''}`)} +${indentString(`callbackUrl: ${auth?.oauth1?.callbackUrl || ''}`)} +${indentString(`verifier: ${auth?.oauth1?.verifier || ''}`)} +${indentString(`accessToken: ${auth?.oauth1?.accessToken || ''}`)} +${indentString(`accessTokenSecret: ${auth?.oauth1?.accessTokenSecret || ''}`)} +${indentString(`rsaPrivateKey: ${auth?.oauth1?.rsaPrivateKey || ''}`)} +${indentString(`parameterTransmissionMethod: ${auth?.oauth1?.parameterTransmissionMethod || ''}`)} +${indentString(`signatureMethod: ${auth?.oauth1?.signatureMethod || ''}`)} +} + `; }