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 ( +
+
+ {value} +
+
+ +
+
+ ); +}); + +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)