Merge pull request #4416 from naman-bruno/feat/oauth2-additional-params

Oauth2 additional params
This commit is contained in:
lohit
2025-04-06 19:04:25 +05:30
committed by GitHub
14 changed files with 904 additions and 21 deletions

View File

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

View File

@@ -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 (
<StyledWrapper className="mt-4">
<div className="flex items-center gap-2.5 mb-3">
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
<IconAdjustmentsHorizontal size={14} className="text-indigo-500 dark:text-indigo-400" />
</div>
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
Additional Parameters
</span>
</div>
<div className="tabs flex w-full gap-2 my-2">
<div className={`tab ${activeTab == 'authorization' ? 'active': ''}`} onClick={e => setActiveTab('authorization')}>Authorization</div>
<div className={`tab ${activeTab == 'token' ? 'active': ''}`} onClick={e => setActiveTab('token')}>Token</div>
<div className={`tab ${activeTab == 'refresh' ? 'active': ''}`} onClick={e => setActiveTab('refresh')}>Refresh</div>
</div>
<Table
headers={[
{ name: 'Key', accessor: 'name', width: '30%' },
{ name: 'Value', accessor: 'value', width: '30%' },
{ name: 'Send In', accessor: 'sendIn', width: '150px' },
{ name: '', accessor: '', width: '15%' }
]}
>
<tbody>
{(additionalParameters?.[activeTab] || []).map((param, index) =>
<tr key={index}>
<td className='flex relative'>
<SingleLineEditor
value={param?.name || ''}
theme={storedTheme}
onChange={(value) => handleUpdateAdditionalParam({
paramType: activeTab,
key: 'name',
paramIndex: index,
value
})}
collection={collection}
/>
</td>
<td>
<SingleLineEditor
value={param?.value || ''}
theme={storedTheme}
onChange={(value) => handleUpdateAdditionalParam({
paramType: activeTab,
key: 'value',
paramIndex: index,
value
})}
collection={collection}
/>
</td>
<td>
<div className="w-full additional-parameter-sends-in-selector">
<select
value={param?.sendIn || 'headers'}
onChange={e => {
handleUpdateAdditionalParam({
paramType: activeTab,
key: 'sendIn',
paramIndex: index,
value: e.target.value
})
}}
className="mousetrap bg-transparent"
>
{sendInOptionsMap[grantType || 'authorization_code'][activeTab].map((optionValue) => (
<option key={optionValue} value={optionValue}>
{optionValue}
</option>
))}
</select>
</div>
</td>
<td>
<div className="flex items-center">
<input
type="checkbox"
checked={param?.enabled ?? true}
tabIndex="-1"
className="mr-3 mousetrap"
onChange={(e) => {
handleUpdateAdditionalParam({
paramType: activeTab,
key: 'enabled',
paramIndex: index,
value: e.target.checked
})
}}
/>
<button
tabIndex="-1"
onClick={() => {
handleDeleteAdditionalParam({
paramType: activeTab,
paramIndex: index
})
}}
>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
)}
</tbody>
</Table>
<div
className={`add-additional-param-actions flex items-center mt-2 ${addButtonDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
onClick={addButtonDisabled ? null : handleAddNewAdditionalParam}
>
<IconPlus size={16} strokeWidth={1.5} style={{ marginLeft: '2px' }} />
<span className="ml-1 text-sm text-gray-500">Add Parameter</span>
</div>
</StyledWrapper>
)
}
export default AdditionalParams;
const Icon = forwardRef((props, ref) => {
const { value } = props
return (
<div ref={ref} className="w-max textbox border p-2 rounded cursor-pointer flex items-center selector-label">
<div className="flex-grow font-medium">
{value}
</div>
<div>
<IconCaretDown className="caret mx-2" size={14} strokeWidth={2} />
</div>
</div>
);
});
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']
}
}

View File

@@ -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
</div>
</div>
</div>
<AdditionalParams
item={item}
request={request}
collection={collection}
updateAuth={updateAuth}
/>
<Oauth2ActionButtons item={item} request={request} collection={collection} url={accessTokenUrl} credentialsId={credentialsId} />
</StyledWrapper>
);

View File

@@ -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
</div>
</div>
</div>
<AdditionalParams
item={item}
request={request}
collection={collection}
updateAuth={updateAuth}
/>
<Oauth2ActionButtons item={item} request={request} collection={collection} url={accessTokenUrl} credentialsId={credentialsId} />
</StyledWrapper>

View File

@@ -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
</div>
</div>
</div>
<AdditionalParams
item={item}
request={request}
collection={collection}
updateAuth={updateAuth}
/>
<Oauth2ActionButtons item={item} request={request} collection={collection} url={accessTokenUrl} credentialsId={credentialsId} />
</StyledWrapper>
);

View File

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

View File

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

View File

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

View File

@@ -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;
module.exports = parser;

View File

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

View File

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

View File

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

View File

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

View File

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