diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AdditionalParams/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AdditionalParams/StyledWrapper.js
new file mode 100644
index 000000000..cc8980abe
--- /dev/null
+++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AdditionalParams/StyledWrapper.js
@@ -0,0 +1,45 @@
+import styled from 'styled-components';
+
+const StyledWrapper = styled.div`
+ .tabs {
+ .tab {
+ cursor: pointer;
+ padding: 4px 8px !important;
+ font-size: 12px;
+ border-radius: 4px;
+
+ &:hover {
+ background-color: ${(props) => props.theme.mode === 'dark' ? 'rgba(99, 102, 241, 0.1)' : 'rgba(99, 102, 241, 0.1)'};
+ }
+
+ &.active {
+ background-color: ${(props) => props.theme.mode === 'dark' ? 'rgba(99, 102, 241, 0.2)' : 'rgba(99, 102, 241, 0.1)'};
+ color: ${(props) => props.theme.mode === 'dark' ? '#6366f1' : '#4f46e5'};
+ font-weight: 500;
+ }
+ }
+ }
+
+ .additional-parameter-sends-in-selector {
+ select {
+ height: 32px;
+ width: 100%;
+ border: 1px solid ${(props) => props.theme.input.border};
+ border-radius: 4px;
+ padding: 0 8px;
+
+ &:focus {
+ outline: none;
+ border-color: ${(props) => props.theme.mode === 'dark' ? '#6366f1' : '#4f46e5'};
+ }
+ }
+ }
+
+ .add-additional-param-actions {
+ &:hover {
+ color: ${(props) => props.theme.mode === 'dark' ? '#6366f1' : '#4f46e5'};
+ }
+ }
+`
+
+export default StyledWrapper;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AdditionalParams/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AdditionalParams/index.js
new file mode 100644
index 000000000..b9b6f66bc
--- /dev/null
+++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AdditionalParams/index.js
@@ -0,0 +1,291 @@
+import { useDispatch } from "react-redux";
+import React, { forwardRef, useState } from 'react';
+import get from 'lodash/get';
+import { useTheme } from 'providers/Theme';
+import { IconPlus, IconCaretDown, IconTrash, IconAdjustmentsHorizontal } from '@tabler/icons';
+import { cloneDeep } from "lodash";
+import SingleLineEditor from "components/SingleLineEditor/index";
+import StyledWrapper from "./StyledWrapper";
+import Table from "components/Table/index";
+
+const AdditionalParams = ({ item = {}, request, updateAuth, collection }) => {
+ const dispatch = useDispatch();
+ const { storedTheme } = useTheme();
+ const [activeTab, setActiveTab] = useState('authorization');
+
+ const oAuth = get(request, 'auth.oauth2', {});
+ const {
+ grantType,
+ additionalParameters = {}
+ } = oAuth;
+
+ const isEmptyParam = (param) => {
+ return !param.name.trim() && !param.value.trim();
+ };
+
+ const hasEmptyRow = () => {
+ const tabParams = additionalParameters[activeTab] || [];
+ return tabParams.some(isEmptyParam);
+ };
+
+ const updateAdditionalParams = ({ updatedAdditionalParams }) => {
+ const filteredParams = cloneDeep(updatedAdditionalParams);
+
+ Object.keys(filteredParams).forEach(paramType => {
+ if (filteredParams[paramType]?.length) {
+ filteredParams[paramType] = filteredParams[paramType].filter(param =>
+ param.name.trim() || param.value.trim()
+ );
+
+ if (filteredParams[paramType].length === 0) {
+ delete filteredParams[paramType];
+ }
+ } else if (Array.isArray(filteredParams[paramType]) && filteredParams[paramType].length === 0) {
+ // Remove empty arrays
+ delete filteredParams[paramType];
+ }
+ });
+
+ dispatch(
+ updateAuth({
+ mode: 'oauth2',
+ collectionUid: collection.uid,
+ itemUid: item.uid,
+ content: {
+ ...oAuth,
+ additionalParameters: Object.keys(filteredParams).length > 0 ? filteredParams : undefined
+ }
+ })
+ );
+ }
+
+ const handleUpdateAdditionalParam = ({ paramType, key, paramIndex, value }) => {
+ const updatedAdditionalParams = cloneDeep(additionalParameters);
+
+ if (!updatedAdditionalParams[paramType]) {
+ updatedAdditionalParams[paramType] = [];
+ }
+
+ if (!updatedAdditionalParams[paramType][paramIndex]) {
+ updatedAdditionalParams[paramType][paramIndex] = {
+ name: '',
+ value: '',
+ sendIn: 'headers',
+ enabled: true
+ };
+ }
+
+ updatedAdditionalParams[paramType][paramIndex][key] = value;
+
+ // Only filter when updating a parameter
+ updateAdditionalParams({ updatedAdditionalParams });
+ }
+
+ const handleDeleteAdditionalParam = ({ paramType, paramIndex }) => {
+ const updatedAdditionalParams = cloneDeep(additionalParameters);
+
+ if (updatedAdditionalParams[paramType]?.length) {
+ updatedAdditionalParams[paramType] = updatedAdditionalParams[paramType].filter((_, index) => index !== paramIndex);
+
+ // If the array is now empty, ensure we're not sending empty arrays
+ if (updatedAdditionalParams[paramType].length === 0) {
+ delete updatedAdditionalParams[paramType];
+ }
+ }
+
+ updateAdditionalParams({ updatedAdditionalParams });
+ }
+
+ const handleAddNewAdditionalParam = () => {
+ // Prevent adding multiple empty rows
+ if (hasEmptyRow()) {
+ return;
+ }
+
+ const paramType = activeTab;
+ const localAdditionalParameters = cloneDeep(additionalParameters);
+
+ if (!localAdditionalParameters[paramType]) {
+ localAdditionalParameters[paramType] = [];
+ }
+
+ localAdditionalParameters[paramType] = [
+ ...localAdditionalParameters[paramType],
+ {
+ name: '',
+ value: '',
+ sendIn: 'headers',
+ enabled: true
+ }
+ ];
+
+ // Don't filter here to allow the empty row to display in UI
+ // But don't permanently store it in state until it has values
+ dispatch(
+ updateAuth({
+ mode: 'oauth2',
+ collectionUid: collection.uid,
+ itemUid: item.uid,
+ content: {
+ ...oAuth,
+ additionalParameters: localAdditionalParameters,
+ }
+ })
+ );
+ }
+
+ // Add a class to the Add Parameter button if it's disabled
+ const addButtonDisabled = hasEmptyRow();
+
+ return (
+
+
+
+
+
+
+ Additional Parameters
+
+
+
+
+
setActiveTab('authorization')}>Authorization
+
setActiveTab('token')}>Token
+
setActiveTab('refresh')}>Refresh
+
+
+
+ {(additionalParameters?.[activeTab] || []).map((param, index) =>
+
+ |
+ handleUpdateAdditionalParam({
+ paramType: activeTab,
+ key: 'name',
+ paramIndex: index,
+ value
+ })}
+ collection={collection}
+ />
+ |
+
+ handleUpdateAdditionalParam({
+ paramType: activeTab,
+ key: 'value',
+ paramIndex: index,
+ value
+ })}
+ collection={collection}
+ />
+ |
+
+
+
+
+ |
+
+
+ {
+ handleUpdateAdditionalParam({
+ paramType: activeTab,
+ key: 'enabled',
+ paramIndex: index,
+ value: e.target.checked
+ })
+ }}
+ />
+
+
+ |
+
+ )}
+
+
+
+
+ Add Parameter
+
+
+ )
+}
+
+export default AdditionalParams;
+
+const Icon = forwardRef((props, ref) => {
+ const { value } = props
+ return (
+
+ );
+});
+
+const sendInOptionsMap = {
+ 'authorization_code': {
+ 'authorization': ['headers', 'queryparams'],
+ 'token': ['headers', 'queryparams', 'body'],
+ 'refresh': ['headers', 'queryparams', 'body']
+ },
+ 'password': {
+ 'authorization': ['headers', 'queryparams'],
+ 'token': ['headers', 'queryparams', 'body'],
+ 'refresh': ['headers', 'queryparams', 'body']
+ },
+ 'client_credentials': {
+ 'authorization': ['headers', 'queryparams'],
+ 'token': ['headers', 'queryparams', 'body'],
+ 'refresh': ['headers', 'queryparams', 'body']
+ }
+}
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js
index c00964d82..d61bbf013 100644
--- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js
+++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js
@@ -9,6 +9,7 @@ import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
+import AdditionalParams from '../AdditionalParams/index';
const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAuth, collection, folder }) => {
const dispatch = useDispatch();
@@ -33,7 +34,8 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
tokenQueryKey,
refreshTokenUrl,
autoRefreshToken,
- autoFetchToken
+ autoFetchToken,
+ additionalParameters
} = oAuth;
const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== '';
@@ -83,6 +85,7 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
refreshTokenUrl,
autoRefreshToken,
autoFetchToken,
+ additionalParameters,
[key]: value,
}
})
@@ -110,6 +113,7 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
tokenHeaderPrefix,
tokenQueryKey,
autoFetchToken,
+ additionalParameters,
pkce: !Boolean(oAuth?.['pkce'])
}
})
@@ -326,6 +330,12 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
+
);
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js
index 98b3e4607..f9fc99973 100644
--- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js
+++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js
@@ -9,6 +9,7 @@ import { inputsConfig } from './inputsConfig';
import Dropdown from 'components/Dropdown';
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
+import AdditionalParams from '../AdditionalParams/index';
const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAuth, collection }) => {
const dispatch = useDispatch();
@@ -30,7 +31,8 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
tokenQueryKey,
refreshTokenUrl,
autoRefreshToken,
- autoFetchToken
+ autoFetchToken,
+ additionalParameters
} = oAuth;
const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== '';
@@ -77,6 +79,7 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
refreshTokenUrl,
autoRefreshToken,
autoFetchToken,
+ additionalParameters,
[key]: value
}
})
@@ -295,7 +298,12 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
-
+
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js
index 47f6fc5b2..f12f50c92 100644
--- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js
+++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js
@@ -9,6 +9,7 @@ import { inputsConfig } from './inputsConfig';
import Dropdown from 'components/Dropdown';
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
+import AdditionalParams from '../AdditionalParams/index';
const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, updateAuth, collection }) => {
const dispatch = useDispatch();
@@ -32,7 +33,8 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
tokenQueryKey,
refreshTokenUrl,
autoRefreshToken,
- autoFetchToken
+ autoFetchToken,
+ additionalParameters
} = oAuth;
const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== '';
@@ -80,6 +82,7 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
refreshTokenUrl,
autoRefreshToken,
autoFetchToken,
+ additionalParameters,
[key]: value
}
})
@@ -298,6 +301,12 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
+
);
diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js
index 73049b918..256874730 100644
--- a/packages/bruno-app/src/utils/collections/index.js
+++ b/packages/bruno-app/src/utils/collections/index.js
@@ -1,8 +1,6 @@
import {cloneDeep, isEqual, sortBy, filter, map, isString, findIndex, find, each, get } from 'lodash';
import { uuid } from 'utils/common';
import path from 'utils/common/path';
-import brunoCommon from '@usebruno/common';
-const { interpolate } = brunoCommon;
const replaceTabsWithSpaces = (str, numSpaces = 2) => {
if (!str || !str.length || !isString(str)) {
@@ -650,7 +648,6 @@ export const transformRequestToSaveToFilesystem = (item) => {
json: replaceTabsWithSpaces(itemToSave.request.body.json)
};
}
-
return itemToSave;
};
diff --git a/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js b/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js
index 7d2e23abc..cb2903d7c 100644
--- a/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js
+++ b/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js
@@ -5,7 +5,7 @@ const matchesCallbackUrl = (url, callbackUrl) => {
return url ? url.href.startsWith(callbackUrl.href) : false;
};
-const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => {
+const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session, additionalHeaders = {} }) => {
return new Promise(async (resolve, reject) => {
let finalUrl = null;
let debugInfo = {
@@ -75,6 +75,14 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => {
webSession.webRequest.onBeforeSendHeaders((details, callback) => {
const { id: requestId, requestHeaders, method, url } = details;
+
+ if (details.resourceType === 'mainFrame' && Object.keys(additionalHeaders).length > 0) {
+ // Add our custom headers
+ for (const [name, value] of Object.entries(additionalHeaders)) {
+ requestHeaders[name] = value;
+ }
+ }
+
if (currentMainRequest?.requestId === requestId) {
currentMainRequest.request = {
url,
diff --git a/packages/bruno-electron/src/utils/oauth2.js b/packages/bruno-electron/src/utils/oauth2.js
index 882f39767..50b1f73ed 100644
--- a/packages/bruno-electron/src/utils/oauth2.js
+++ b/packages/bruno-electron/src/utils/oauth2.js
@@ -61,6 +61,7 @@ const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, fo
credentialsId,
autoRefreshToken,
autoFetchToken,
+ additionalParameters,
} = oAuth;
const url = requestCopy?.oauth2?.accessTokenUrl;
if (!forceFetch) {
@@ -140,6 +141,12 @@ const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, fo
if (scope) {
data.scope = scope;
}
+
+ // Apply additional parameters to token request
+ if (additionalParameters?.token?.length) {
+ applyAdditionalParameters(requestCopy, data, additionalParameters.token);
+ }
+
requestCopy.data = qs.stringify(data);
requestCopy.url = url;
requestCopy.responseType = 'arraybuffer';
@@ -249,7 +256,7 @@ const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, fo
const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => {
return new Promise(async (resolve, reject) => {
const { oauth2 } = request;
- const { callbackUrl, clientId, authorizationUrl, scope, state, pkce, accessTokenUrl } = oauth2;
+ const { callbackUrl, clientId, authorizationUrl, scope, state, pkce, accessTokenUrl, additionalParameters } = oauth2;
const authorizationUrlWithQueryParams = new URL(authorizationUrl);
authorizationUrlWithQueryParams.searchParams.append('response_type', 'code');
@@ -267,12 +274,23 @@ const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => {
if (state) {
authorizationUrlWithQueryParams.searchParams.append('state', state);
}
+ if (additionalParameters?.authorization?.length) {
+ additionalParameters.authorization.forEach(param => {
+ if (param.enabled && param.name) {
+ if (param.sendIn === 'queryparams') {
+ authorizationUrlWithQueryParams.searchParams.append(param.name, param.value || '');
+ }
+ }
+ });
+ }
+
try {
const authorizeUrl = authorizationUrlWithQueryParams.toString();
const { authorizationCode, debugInfo } = await authorizeUserInWindow({
authorizeUrl,
callbackUrl,
- session: oauth2Store.getSessionIdOfCollection({ collectionUid, url: accessTokenUrl })
+ session: oauth2Store.getSessionIdOfCollection({ collectionUid, url: accessTokenUrl }),
+ additionalHeaders: getAdditionalHeaders(additionalParameters?.authorization)
});
resolve({ authorizationCode, debugInfo });
} catch (err) {
@@ -281,6 +299,21 @@ const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => {
});
};
+const getAdditionalHeaders = (params) => {
+ if (!params || !params.length) {
+ return {};
+ }
+
+ const headers = {};
+ params.forEach(param => {
+ if (param.enabled && param.name && param.sendIn === 'headers') {
+ headers[param.name] = param.value || '';
+ }
+ });
+
+ return headers;
+};
+
// CLIENT CREDENTIALS
const getOAuth2TokenUsingClientCredentials = async ({ request, collectionUid, forceFetch = false, certsAndProxyConfig }) => {
@@ -294,6 +327,7 @@ const getOAuth2TokenUsingClientCredentials = async ({ request, collectionUid, fo
credentialsId,
autoRefreshToken,
autoFetchToken,
+ additionalParameters,
} = oAuth;
const url = requestCopy?.oauth2?.accessTokenUrl;
@@ -366,6 +400,10 @@ const getOAuth2TokenUsingClientCredentials = async ({ request, collectionUid, fo
if (scope) {
data.scope = scope;
}
+ if (additionalParameters?.token?.length) {
+ applyAdditionalParameters(requestCopy, data, additionalParameters.token);
+ }
+
requestCopy.data = qs.stringify(data);
requestCopy.url = url;
requestCopy.responseType = 'arraybuffer';
@@ -480,6 +518,7 @@ const getOAuth2TokenUsingPasswordCredentials = async ({ request, collectionUid,
credentialsId,
autoRefreshToken,
autoFetchToken,
+ additionalParameters,
} = oAuth;
const url = requestCopy?.oauth2?.accessTokenUrl;
@@ -554,6 +593,10 @@ const getOAuth2TokenUsingPasswordCredentials = async ({ request, collectionUid,
if (scope) {
data.scope = scope;
}
+ if (additionalParameters?.token?.length) {
+ applyAdditionalParameters(requestCopy, data, additionalParameters.token);
+ }
+
requestCopy.data = qs.stringify(data);
requestCopy.url = url;
requestCopy.responseType = 'arraybuffer';
@@ -671,6 +714,10 @@ const refreshOauth2Token = async ({ requestCopy, collectionUid, certsAndProxyCon
if (clientSecret) {
data.client_secret = clientSecret;
}
+ if (oAuth.additionalParameters?.refresh?.length) {
+ applyAdditionalParameters(requestCopy, data, oAuth.additionalParameters.refresh);
+ }
+
requestCopy.method = 'POST';
requestCopy.headers['content-type'] = 'application/x-www-form-urlencoded';
requestCopy.headers['Accept'] = 'application/json';
@@ -678,7 +725,7 @@ const refreshOauth2Token = async ({ requestCopy, collectionUid, certsAndProxyCon
requestCopy.url = url;
requestCopy.responseType = 'arraybuffer';
- // Initialize variables to hold request and response data for debugging
+ // Initialize variables to hold request and response data for debugging
let axiosRequestInfo = null;
let axiosResponseInfo = null;
let debugInfo = { data: [] };
@@ -797,6 +844,38 @@ const generateCodeChallenge = (codeVerifier) => {
return base64Hash;
};
+// Apply additional parameters to a request
+const applyAdditionalParameters = (requestCopy, data, params) => {
+ if (!params || !params.length) {
+ return;
+ }
+
+ params.forEach(param => {
+ if (!param.enabled || !param.name) {
+ return;
+ }
+
+ switch (param.sendIn) {
+ case 'headers':
+ requestCopy.headers[param.name] = param.value || '';
+ break;
+ case 'queryparams':
+ // For query params, add to URL
+ if (!requestCopy.url.includes('?')) {
+ requestCopy.url += '?';
+ } else if (!requestCopy.url.endsWith('&') && !requestCopy.url.endsWith('?')) {
+ requestCopy.url += '&';
+ }
+ requestCopy.url += `${encodeURIComponent(param.name)}=${encodeURIComponent(param.value || '')}`;
+ break;
+ case 'body':
+ // For body, add to data object
+ data[param.name] = param.value || '';
+ break;
+ }
+ });
+};
+
module.exports = {
getOAuth2TokenUsingAuthorizationCode,
getOAuth2AuthorizationCode,
diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js
index 819272240..9db992d9e 100644
--- a/packages/bruno-lang/v2/src/bruToJson.js
+++ b/packages/bruno-lang/v2/src/bruToJson.js
@@ -1,6 +1,6 @@
const ohm = require('ohm-js');
const _ = require('lodash');
-const { safeParseJson, outdentString } = require('./utils');
+const { safeParseJson, outdentString, mergeOauth2AdditionalParameters } = require('./utils');
/**
* A Bru file is made up of blocks.
@@ -22,12 +22,18 @@ const { safeParseJson, outdentString } = require('./utils');
*
*/
const grammar = ohm.grammar(`Bru {
- BruFile = (meta | http | query | params | headers | auths | bodies | varsandassert | script | tests | docs)*
+ BruFile = (meta | http | query | params | headers | auths | bodies | varsandassert | script | tests | docs | authOAuth2Configs)*
auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth2 | authwsse | authapikey
bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body
bodyforms = bodyformurlencoded | bodymultipart | bodyfile
params = paramspath | paramsquery
-
+
+ // Oauth2 additional parameters
+ authOAuth2Configs = oAuth2AuthorizationConfig | oAuth2TokenConfig | oAuth2RefreshConfig
+ oAuth2AuthorizationConfig = oAuth2AuthorizationHeaders | oAuth2AuthorizationQueryParams
+ oAuth2TokenConfig = oAuth2TokenHeaders | oAuth2TokenQueryParams | oAuth2TokenBodyValues
+ oAuth2RefreshConfig = oAuth2RefreshHeaders | oAuth2RefreshQueryParams | oAuth2RefreshBodyValues
+
nl = "\\r"? "\\n"
st = " " | "\\t"
stnl = st | nl
@@ -92,6 +98,15 @@ const grammar = ohm.grammar(`Bru {
authwsse = "auth:wsse" dictionary
authapikey = "auth:apikey" dictionary
+ oAuth2AuthorizationHeaders = "auth:oauth2:authorization_headers" dictionary
+ oAuth2AuthorizationQueryParams = "auth:oauth2:authorization_queryparams" dictionary
+ oAuth2TokenHeaders = "auth:oauth2:token_headers" dictionary
+ oAuth2TokenQueryParams = "auth:oauth2:token_queryparams" dictionary
+ oAuth2TokenBodyValues = "auth:oauth2:token_bodyvalues" dictionary
+ oAuth2RefreshHeaders = "auth:oauth2:refresh_headers" dictionary
+ oAuth2RefreshQueryParams = "auth:oauth2:refresh_queryparams" dictionary
+ oAuth2RefreshBodyValues = "auth:oauth2:refresh_bodyvalues" dictionary
+
body = "body" st* "{" nl* textblock tagend
bodyjson = "body:json" st* "{" nl* textblock tagend
bodytext = "body:text" st* "{" nl* textblock tagend
@@ -588,6 +603,46 @@ const sem = grammar.createSemantics().addAttribute('ast', {
}
};
},
+ oAuth2AuthorizationHeaders(_1, dictionary) {
+ return {
+ oauth2_additional_parameters_authorization_headers: mapPairListToKeyValPairs(dictionary.ast)
+ };
+ },
+ oAuth2AuthorizationQueryParams(_1, dictionary) {
+ return {
+ oauth2_additional_parameters_authorization_queryparams: mapPairListToKeyValPairs(dictionary.ast)
+ };
+ },
+ oAuth2TokenHeaders(_1, dictionary) {
+ return {
+ oauth2_additional_parameters_token_headers: mapPairListToKeyValPairs(dictionary.ast)
+ };
+ },
+ oAuth2TokenQueryParams(_1, dictionary) {
+ return {
+ oauth2_additional_parameters_token_queryparams: mapPairListToKeyValPairs(dictionary.ast)
+ };
+ },
+ oAuth2TokenBodyValues(_1, dictionary) {
+ return {
+ oauth2_additional_parameters_token_bodyvalues: mapPairListToKeyValPairs(dictionary.ast)
+ };
+ },
+ oAuth2RefreshHeaders(_1, dictionary) {
+ return {
+ oauth2_additional_parameters_refresh_headers: mapPairListToKeyValPairs(dictionary.ast)
+ };
+ },
+ oAuth2RefreshQueryParams(_1, dictionary) {
+ return {
+ oauth2_additional_parameters_refresh_queryparams: mapPairListToKeyValPairs(dictionary.ast)
+ };
+ },
+ oAuth2RefreshBodyValues(_1, dictionary) {
+ return {
+ oauth2_additional_parameters_refresh_bodyvalues: mapPairListToKeyValPairs(dictionary.ast)
+ };
+ },
authwsse(_1, dictionary) {
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
@@ -775,11 +830,14 @@ const parser = (input) => {
const match = grammar.match(input);
if (match.succeeded()) {
- return sem(match).ast;
+ let ast = sem(match).ast
+
+ ast = mergeOauth2AdditionalParameters(ast);
+
+ return ast;
} else {
throw new Error(match.message);
}
};
-module.exports = parser;
-
\ No newline at end of file
+module.exports = parser;
\ No newline at end of file
diff --git a/packages/bruno-lang/v2/src/collectionBruToJson.js b/packages/bruno-lang/v2/src/collectionBruToJson.js
index 16a0c8d79..178afd87a 100644
--- a/packages/bruno-lang/v2/src/collectionBruToJson.js
+++ b/packages/bruno-lang/v2/src/collectionBruToJson.js
@@ -1,11 +1,17 @@
const ohm = require('ohm-js');
const _ = require('lodash');
-const { safeParseJson, outdentString } = require('./utils');
+const { safeParseJson, outdentString, mergeOauth2AdditionalParameters } = require('./utils');
const grammar = ohm.grammar(`Bru {
- BruFile = (meta | query | headers | auth | auths | vars | script | tests | docs)*
+ BruFile = (meta | query | headers | auth | auths | vars | script | tests | docs | authOAuth2Configs)*
auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM |authOAuth2 | authwsse | authapikey
+ // Oauth2 additional parameters
+ authOAuth2Configs = oAuth2AuthorizationConfig | oAuth2TokenConfig | oAuth2RefreshConfig
+ oAuth2AuthorizationConfig = oAuth2AuthorizationHeaders | oAuth2AuthorizationQueryParams
+ oAuth2TokenConfig = oAuth2TokenHeaders | oAuth2TokenQueryParams | oAuth2TokenBodyValues
+ oAuth2RefreshConfig = oAuth2RefreshHeaders | oAuth2RefreshQueryParams | oAuth2RefreshBodyValues
+
nl = "\\r"? "\\n"
st = " " | "\\t"
stnl = st | nl
@@ -30,6 +36,15 @@ const grammar = ohm.grammar(`Bru {
auth = "auth" dictionary
+ oAuth2AuthorizationHeaders = "auth:oauth2:authorization_headers" dictionary
+ oAuth2AuthorizationQueryParams = "auth:oauth2:authorization_queryparams" dictionary
+ oAuth2TokenHeaders = "auth:oauth2:token_headers" dictionary
+ oAuth2TokenQueryParams = "auth:oauth2:token_queryparams" dictionary
+ oAuth2TokenBodyValues = "auth:oauth2:token_bodyvalues" dictionary
+ oAuth2RefreshHeaders = "auth:oauth2:refresh_headers" dictionary
+ oAuth2RefreshQueryParams = "auth:oauth2:refresh_queryparams" dictionary
+ oAuth2RefreshBodyValues = "auth:oauth2:refresh_bodyvalues" dictionary
+
headers = "headers" dictionary
query = "query" dictionary
@@ -348,6 +363,46 @@ const sem = grammar.createSemantics().addAttribute('ast', {
}
};
},
+ oAuth2AuthorizationHeaders(_1, dictionary) {
+ return {
+ oauth2_additional_parameters_authorization_headers: mapPairListToKeyValPairs(dictionary.ast)
+ };
+ },
+ oAuth2AuthorizationQueryParams(_1, dictionary) {
+ return {
+ oauth2_additional_parameters_authorization_queryparams: mapPairListToKeyValPairs(dictionary.ast)
+ };
+ },
+ oAuth2TokenHeaders(_1, dictionary) {
+ return {
+ oauth2_additional_parameters_token_headers: mapPairListToKeyValPairs(dictionary.ast)
+ };
+ },
+ oAuth2TokenQueryParams(_1, dictionary) {
+ return {
+ oauth2_additional_parameters_token_queryparams: mapPairListToKeyValPairs(dictionary.ast)
+ };
+ },
+ oAuth2TokenBodyValues(_1, dictionary) {
+ return {
+ oauth2_additional_parameters_token_bodyvalues: mapPairListToKeyValPairs(dictionary.ast)
+ };
+ },
+ oAuth2RefreshHeaders(_1, dictionary) {
+ return {
+ oauth2_additional_parameters_refresh_headers: mapPairListToKeyValPairs(dictionary.ast)
+ };
+ },
+ oAuth2RefreshQueryParams(_1, dictionary) {
+ return {
+ oauth2_additional_parameters_refresh_queryparams: mapPairListToKeyValPairs(dictionary.ast)
+ };
+ },
+ oAuth2RefreshBodyValues(_1, dictionary) {
+ return {
+ oauth2_additional_parameters_refresh_bodyvalues: mapPairListToKeyValPairs(dictionary.ast)
+ };
+ },
authwsse(_1, dictionary) {
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
const userKey = _.find(auth, { name: 'username' });
@@ -451,7 +506,11 @@ const parser = (input) => {
const match = grammar.match(input);
if (match.succeeded()) {
- return sem(match).ast;
+ let ast = sem(match).ast;
+
+ ast = mergeOauth2AdditionalParameters(ast);
+
+ return ast;
} else {
throw new Error(match.message);
}
diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js
index 776cca7d5..0e848df41 100644
--- a/packages/bruno-lang/v2/src/jsonToBru.js
+++ b/packages/bruno-lang/v2/src/jsonToBru.js
@@ -249,6 +249,114 @@ ${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken ?? false).
`;
break;
}
+
+ if (auth?.oauth2?.additionalParameters) {
+ const { authorization: authorizationParams, token: tokenParams, refresh: refreshParams } = auth?.oauth2?.additionalParameters;
+ const authorizationHeaders = authorizationParams?.filter(p => p?.sendIn == 'headers');
+ if (authorizationHeaders?.length) {
+ bru += `auth:oauth2:authorization_headers {
+${indentString(
+ enabled(authorizationHeaders)
+ .filter(item => item?.name?.length)
+ .map((item) => `${item.name}: ${item.value}`)
+ .join('\n')
+ )}
+}
+
+`;
+ }
+ const authorizationQueryParams = authorizationParams?.filter(p => p?.sendIn == 'queryparams');
+ if (authorizationQueryParams?.length) {
+ bru += `auth:oauth2:authorization_queryparams {
+${indentString(
+ enabled(authorizationQueryParams)
+ .filter(item => item?.name?.length)
+ .map((item) => `${item.name}: ${item.value}`)
+ .join('\n')
+ )}
+}
+
+`;
+ }
+ const tokenHeaders = tokenParams?.filter(p => p?.sendIn == 'headers');
+ if (tokenHeaders?.length) {
+ bru += `auth:oauth2:token_headers {
+${indentString(
+ enabled(tokenHeaders)
+ .filter(item => item?.name?.length)
+ .map((item) => `${item.name}: ${item.value}`)
+ .join('\n')
+ )}
+}
+
+`;
+ }
+ const tokenQueryParams = tokenParams?.filter(p => p?.sendIn == 'queryparams');
+ if (tokenQueryParams?.length) {
+ bru += `auth:oauth2:token_queryparams {
+${indentString(
+ enabled(tokenQueryParams)
+ .filter(item => item?.name?.length)
+ .map((item) => `${item.name}: ${item.value}`)
+ .join('\n')
+ )}
+}
+
+`;
+ }
+ const tokenBodyValues = tokenParams?.filter(p => p?.sendIn == 'body');
+ if (tokenBodyValues?.length) {
+ bru += `auth:oauth2:token_bodyvalues {
+${indentString(
+ enabled(tokenBodyValues)
+ .filter(item => item?.name?.length)
+ .map((item) => `${item.name}: ${item.value}`)
+ .join('\n')
+ )}
+}
+
+`;
+ }
+ const refreshHeaders = refreshParams?.filter(p => p?.sendIn == 'headers');
+ if (refreshHeaders?.length) {
+ bru += `auth:oauth2:refresh_headers {
+${indentString(
+ enabled(refreshHeaders)
+ .filter(item => item?.name?.length)
+ .map((item) => `${item.name}: ${item.value}`)
+ .join('\n')
+ )}
+}
+
+`;
+ }
+ const refreshQueryParams = refreshParams?.filter(p => p?.sendIn == 'queryparams');
+ if (refreshQueryParams?.length) {
+ bru += `auth:oauth2:refresh_queryparams {
+${indentString(
+ enabled(refreshQueryParams)
+ .filter(item => item?.name?.length)
+ .map((item) => `${item.name}: ${item.value}`)
+ .join('\n')
+ )}
+}
+
+`;
+ }
+ const refreshBodyValues = refreshParams?.filter(p => p?.sendIn == 'body');
+ if (refreshBodyValues?.length) {
+ bru += `auth:oauth2:refresh_bodyvalues {
+${indentString(
+ enabled(refreshBodyValues)
+ .filter(item => item?.name?.length)
+ .map((item) => `${item.name}: ${item.value}`)
+ .join('\n')
+ )}
+}
+
+`;
+ }
+ }
}
if (auth && auth.apikey) {
diff --git a/packages/bruno-lang/v2/src/jsonToCollectionBru.js b/packages/bruno-lang/v2/src/jsonToCollectionBru.js
index 2812798a5..b1dbfd481 100644
--- a/packages/bruno-lang/v2/src/jsonToCollectionBru.js
+++ b/packages/bruno-lang/v2/src/jsonToCollectionBru.js
@@ -215,6 +215,114 @@ ${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken ?? false).
`;
break;
}
+
+ if (auth?.oauth2?.additionalParameters) {
+ const { authorization: authorizationParams, token: tokenParams, refresh: refreshParams } = auth?.oauth2?.additionalParameters;
+ const authorizationHeaders = authorizationParams?.filter(p => p?.sendIn == 'headers');
+ if (authorizationHeaders?.length) {
+ bru += `auth:oauth2:authorization_headers {
+${indentString(
+ enabled(authorizationHeaders)
+ .filter(item => item?.name?.length)
+ .map((item) => `${item.name}: ${item.value}`)
+ .join('\n')
+ )}
+}
+
+`;
+ }
+ const authorizationQueryParams = authorizationParams?.filter(p => p?.sendIn == 'queryparams');
+ if (authorizationQueryParams?.length) {
+ bru += `auth:oauth2:authorization_queryparams {
+${indentString(
+ enabled(authorizationQueryParams)
+ .filter(item => item?.name?.length)
+ .map((item) => `${item.name}: ${item.value}`)
+ .join('\n')
+ )}
+}
+
+`;
+ }
+ const tokenHeaders = tokenParams?.filter(p => p?.sendIn == 'headers');
+ if (tokenHeaders?.length) {
+ bru += `auth:oauth2:token_headers {
+${indentString(
+ enabled(tokenHeaders)
+ .filter(item => item?.name?.length)
+ .map((item) => `${item.name}: ${item.value}`)
+ .join('\n')
+ )}
+}
+
+`;
+ }
+ const tokenQueryParams = tokenParams?.filter(p => p?.sendIn == 'queryparams');
+ if (tokenQueryParams?.length) {
+ bru += `auth:oauth2:token_queryparams {
+${indentString(
+ enabled(tokenQueryParams)
+ .filter(item => item?.name?.length)
+ .map((item) => `${item.name}: ${item.value}`)
+ .join('\n')
+ )}
+}
+
+`;
+ }
+ const tokenBodyValues = tokenParams?.filter(p => p?.sendIn == 'body');
+ if (tokenBodyValues?.length) {
+ bru += `auth:oauth2:token_bodyvalues {
+${indentString(
+ enabled(tokenBodyValues)
+ .filter(item => item?.name?.length)
+ .map((item) => `${item.name}: ${item.value}`)
+ .join('\n')
+ )}
+}
+
+`;
+ }
+ const refreshHeaders = refreshParams?.filter(p => p?.sendIn == 'headers');
+ if (refreshHeaders?.length) {
+ bru += `auth:oauth2:refresh_headers {
+${indentString(
+ enabled(refreshHeaders)
+ .filter(item => item?.name?.length)
+ .map((item) => `${item.name}: ${item.value}`)
+ .join('\n')
+ )}
+}
+
+`;
+ }
+ const refreshQueryParams = refreshParams?.filter(p => p?.sendIn == 'queryparams');
+ if (refreshQueryParams?.length) {
+ bru += `auth:oauth2:refresh_queryparams {
+${indentString(
+ enabled(refreshQueryParams)
+ .filter(item => item?.name?.length)
+ .map((item) => `${item.name}: ${item.value}`)
+ .join('\n')
+ )}
+}
+
+`;
+ }
+ const refreshBodyValues = refreshParams?.filter(p => p?.sendIn == 'body');
+ if (refreshBodyValues?.length) {
+ bru += `auth:oauth2:refresh_bodyvalues {
+${indentString(
+ enabled(refreshBodyValues)
+ .filter(item => item?.name?.length)
+ .map((item) => `${item.name}: ${item.value}`)
+ .join('\n')
+ )}
+}
+
+`;
+ }
+ }
}
let reqvars = _.get(vars, 'req');
diff --git a/packages/bruno-lang/v2/src/utils.js b/packages/bruno-lang/v2/src/utils.js
index 74b22c952..64d377aee 100644
--- a/packages/bruno-lang/v2/src/utils.js
+++ b/packages/bruno-lang/v2/src/utils.js
@@ -29,8 +29,84 @@ const outdentString = (str) => {
.join('\n');
};
+const mergeOauth2AdditionalParameters = (ast) => {
+ let additionalParameters = {};
+ const authorizationHeaders = ast?.oauth2_additional_parameters_authorization_headers;
+ const authorizationQueryParams = ast?.oauth2_additional_parameters_authorization_queryparams;
+ const tokenHeaders = ast?.oauth2_additional_parameters_token_headers;
+ const tokenQueryParams = ast?.oauth2_additional_parameters_token_queryparams;
+ const tokenBodyValues = ast?.oauth2_additional_parameters_token_bodyvalues;
+ const refreshHeaders = ast?.oauth2_additional_parameters_refresh_headers;
+ const refreshQueryParams = ast?.oauth2_additional_parameters_refresh_queryparams;
+ const refreshBodyValues = ast?.oauth2_additional_parameters_refresh_bodyvalues;
+
+ if (authorizationHeaders?.length || authorizationQueryParams?.length) {
+ additionalParameters['authorization'] = []
+ }
+ if (authorizationHeaders?.length) {
+ additionalParameters['authorization'] = [
+ ...authorizationHeaders?.map(_ => ({ ..._, sendIn: 'headers' }))
+ ]
+ }
+ if (authorizationQueryParams?.length) {
+ additionalParameters['authorization'] = [
+ ...additionalParameters['authorization'] || [],
+ ...authorizationQueryParams?.map(_ => ({ ..._, sendIn: 'queryparams' }))
+ ]
+ }
+
+ if (tokenHeaders?.length || tokenQueryParams?.length || tokenBodyValues?.length) {
+ additionalParameters['token'] = []
+ }
+ if (tokenHeaders?.length) {
+ additionalParameters['token'] = [
+ ...tokenHeaders?.map(_ => ({ ..._, sendIn: 'headers' }))
+ ]
+ }
+ if (tokenQueryParams?.length) {
+ additionalParameters['token'] = [
+ ...additionalParameters['token'] || [],
+ ...tokenQueryParams?.map(_ => ({ ..._, sendIn: 'queryparams' }))
+ ]
+ }
+ if (tokenBodyValues?.length) {
+ additionalParameters['token'] = [
+ ...additionalParameters['token'] || [],
+ ...tokenBodyValues?.map(_ => ({ ..._, sendIn: 'body' }))
+ ]
+ }
+
+ if (refreshHeaders?.length || refreshQueryParams?.length || refreshBodyValues?.length) {
+ additionalParameters['refresh'] = []
+ }
+ if (refreshHeaders?.length) {
+ additionalParameters['refresh'] = [
+ ...refreshHeaders?.map(_ => ({ ..._, sendIn: 'headers' }))
+ ]
+ }
+ if (refreshQueryParams?.length) {
+ additionalParameters['refresh'] = [
+ ...additionalParameters['refresh'] || [],
+ ...refreshQueryParams?.map(_ => ({ ..._, sendIn: 'queryparams' }))
+ ]
+ }
+ if (refreshBodyValues?.length) {
+ additionalParameters['refresh'] = [
+ ...additionalParameters['refresh'] || [],
+ ...refreshBodyValues?.map(_ => ({ ..._, sendIn: 'body' }))
+ ]
+ }
+
+ if(ast?.auth?.oauth2 && Object.keys(additionalParameters)?.length) {
+ ast.auth.oauth2.additionalParameters = additionalParameters;
+ }
+
+ return ast;
+}
+
module.exports = {
safeParseJson,
indentString,
- outdentString
+ outdentString,
+ mergeOauth2AdditionalParameters
};
diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js
index 3914e6bfa..253d644cd 100644
--- a/packages/bruno-schema/src/collections/index.js
+++ b/packages/bruno-schema/src/collections/index.js
@@ -157,6 +157,28 @@ const authApiKeySchema = Yup.object({
.noUnknown(true)
.strict();
+const oauth2AuthorizationAdditionalParametersSchema = Yup.object({
+ name: Yup.string().nullable(),
+ value: Yup.string().nullable(),
+ sendIn: Yup.string()
+ .oneOf(['headers', 'queryparams'])
+ .required('send in property is required'),
+ enabled: Yup.boolean()
+})
+ .noUnknown(true)
+ .strict();
+
+const oauth2AdditionalParametersSchema = Yup.object({
+ name: Yup.string().nullable(),
+ value: Yup.string().nullable(),
+ sendIn: Yup.string()
+ .oneOf(['headers', 'queryparams', 'body'])
+ .required('send in property is required'),
+ enabled: Yup.boolean()
+ })
+ .noUnknown(true)
+ .strict();
+
const oauth2Schema = Yup.object({
grantType: Yup.string()
.oneOf(['client_credentials', 'password', 'authorization_code'])
@@ -252,6 +274,11 @@ const oauth2Schema = Yup.object({
is: (val) => ['authorization_code'].includes(val),
then: Yup.boolean().default(true),
otherwise: Yup.boolean()
+ }),
+ additionalParameters: Yup.object({
+ authorization: Yup.array().of(oauth2AuthorizationAdditionalParametersSchema).optional(),
+ token: Yup.array().of(oauth2AdditionalParametersSchema).optional(),
+ refresh: Yup.array().of(oauth2AdditionalParametersSchema).optional()
})
})
.noUnknown(true)