diff --git a/package-lock.json b/package-lock.json index be8bc091c2..a9c8d55c4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13282,6 +13282,12 @@ "version": "1.1.1", "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", "license": "Apache-2.0", @@ -18497,6 +18503,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.35", "style-loader": "^3.3.1", "tailwindcss": "^3.4.1", diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json index 7f1dfe7c54..5453ed1a8b 100644 --- a/packages/bruno-app/package.json +++ b/packages/bruno-app/package.json @@ -95,6 +95,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.35", "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 c12ceffe88..b6365588ca 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', 'oauth2', 'inherit', 'none']; + const authModes = ['awsv4', 'basic', 'bearer', 'digest', 'oauth1', 'oauth2', '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..1a5e25dcf0 --- /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: 'authorizationMethod', + 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 51a1450f1e..83392e481a 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/index.js @@ -1,13 +1,14 @@ 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 StyledWrapper from './StyledWrapper'; -import { humanizeRequestAuthMode } from 'utils/collections/index'; -import OAuth2 from './OAuth2/index'; +import OAuth1 from './OAuth1'; +import OAuth2 from './OAuth2'; const Auth = ({ item, collection }) => { const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode'); @@ -29,6 +30,9 @@ const Auth = ({ item, collection }) => { case 'digest': { 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 e8fb4d602b..254bd8300c 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -473,6 +473,10 @@ export const collectionsSlice = createSlice({ item.draft.request.auth.mode = 'digest'; item.draft.request.auth.digest = action.payload.content; 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; diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index d872a66851..32f96ad209 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -335,6 +335,22 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} password: get(si.request, 'auth.digest.password', '') }; 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) { @@ -657,6 +673,10 @@ export const humanizeRequestAuthMode = (mode) => { label = 'Digest 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 768505e271..362957e82e 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, @@ -262,6 +263,10 @@ const configureRequest = async ( }); } const axiosInstance = makeAxiosInstance(); + 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 b6aeaa078f..90afe52c90 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -136,6 +136,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..0f2892582b --- /dev/null +++ b/packages/bruno-electron/src/ipc/network/oauth1-helper.js @@ -0,0 +1,236 @@ +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.0/#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.0/#anchor14 + // 9.1.1. Normalize Request Parameters + const shouldIncludeRequestBodyParams = (request) => { + return request.headers['content-type'] === 'application/x-www-form-urlencoded' && request.method === 'POST'; + } + + // https://oauth.net/core/1.0/#anchor13 + // 7. Accessing Protected Resources + const addAuthorizationToRequest = (request, accessToken, accessTokenSecret) => { + let requestData; + if(shouldIncludeRequestBodyParams(request)) { + requestData = { + url: request.url, + method: request.method, + data: request.data + } + } else { + requestData = { + url: request.url, + method: request.method, + } + } + + const signingToken = { + key: accessToken, + secret: accessTokenSecret + } + const authHeader = oauth.toHeader(oauth.authorize(requestData, signingToken)); + request.headers['Authorization'] = authHeader.Authorization + } + + // 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 } = 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)); + } + addAuthorizationToRequest(request, accessToken, accessTokenSecret); + } 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 38f8f5d3cd..1e49b2573c 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -248,6 +248,22 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { password: get(request, 'auth.digest.password') }; 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 08e4332c59..110b15c460 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 | authOAuth2 + auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth1 | authOAuth2 bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body bodyforms = bodyformurlencoded | bodymultipart params = paramspath | paramsquery @@ -87,6 +87,7 @@ const grammar = ohm.grammar(`Bru { authbasic = "auth:basic" dictionary authbearer = "auth:bearer" dictionary authdigest = "auth:digest" dictionary + authOAuth1 = "auth:oauth1" dictionary authOAuth2 = "auth:oauth2" dictionary body = "body" st* "{" nl* textblock tagend @@ -433,6 +434,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 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/collectionBruToJson.js b/packages/bruno-lang/v2/src/collectionBruToJson.js index 3c02a62255..9b5f5797b7 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 | authOAuth2 + auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth1 | authOAuth2 nl = "\\r"? "\\n" st = " " | "\\t" @@ -42,6 +42,7 @@ const grammar = ohm.grammar(`Bru { authbasic = "auth:basic" dictionary authbearer = "auth:bearer" dictionary authdigest = "auth:digest" dictionary + authOAuth1 = "auth:oauth1" dictionary authOAuth2 = "auth:oauth2" dictionary script = scriptreq | scriptres @@ -243,6 +244,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' }); diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index dd3be5947f..00983169cb 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -153,6 +153,24 @@ ${indentString(`username: ${auth?.digest?.username || ''}`)} ${indentString(`password: ${auth?.digest?.password || ''}`)} } +`; + } + 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 || ''}`)} +} + `; } diff --git a/packages/bruno-lang/v2/src/jsonToCollectionBru.js b/packages/bruno-lang/v2/src/jsonToCollectionBru.js index 11df88da4f..16143c50bc 100644 --- a/packages/bruno-lang/v2/src/jsonToCollectionBru.js +++ b/packages/bruno-lang/v2/src/jsonToCollectionBru.js @@ -111,6 +111,25 @@ ${indentString(`username: ${auth.digest.username}`)} ${indentString(`password: ${auth.digest.password}`)} } +`; + } + + 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 || ''}`)} +} + `; } diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index eeb4e83d67..2c70371d45 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -119,6 +119,23 @@ const authDigestSchema = 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']) @@ -179,12 +196,13 @@ const oauth2Schema = Yup.object({ const authSchema = Yup.object({ mode: Yup.string() - .oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest', 'oauth2']) + .oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest', 'oauth1', 'oauth2']) .required('mode is required'), awsv4: authAwsV4Schema.nullable(), basic: authBasicSchema.nullable(), bearer: authBearerSchema.nullable(), digest: authDigestSchema.nullable(), + oauth1: oauth1Schema.nullable(), oauth2: oauth2Schema.nullable() }) .noUnknown(true)