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..712367fd7 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AdditionalParams/StyledWrapper.js @@ -0,0 +1,66 @@ +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; + } + } + } + + table { + width: 100%; + border-collapse: collapse; + font-weight: 600; + table-layout: fixed; + + thead, + td { + border: 1px solid ${(props) => props.theme.table.border}; + } + + thead { + color: ${(props) => props.theme.table.thead.color}; + font-size: 0.8125rem; + user-select: none; + } + td { + padding: 6px 10px; + } + } + + .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..1d2f81bee --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AdditionalParams/index.js @@ -0,0 +1,306 @@ +import { useDispatch } from "react-redux"; +import React, { useState } from 'react'; +import get from 'lodash/get'; +import { useTheme } from 'providers/Theme'; +import { IconPlus, 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, handleSave }) => { + const dispatch = useDispatch(); + const { storedTheme } = useTheme(); + + const oAuth = get(request, 'auth.oauth2', {}); + const { + grantType, + additionalParameters = {} + } = oAuth; + + const [activeTab, setActiveTab] = useState( + (grantType == 'authorization_code' || grantType == 'implicit') ? 'authorization' : 'token' + ); + + const isEmptyParam = (param) => { + return !param.name.trim() && !param.value.trim(); + }; + + const hasEmptyRow = () => { + const tabParams = additionalParameters[activeTab] || []; + return tabParams.some(isEmptyParam); + }; + + const updateAdditionalParameters = ({ updatedAdditionalParameters }) => { + const filteredParams = cloneDeep(updatedAdditionalParameters); + + 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 updatedAdditionalParameters = cloneDeep(additionalParameters); + + if (!updatedAdditionalParameters[paramType]) { + updatedAdditionalParameters[paramType] = []; + } + + if (!updatedAdditionalParameters[paramType][paramIndex]) { + updatedAdditionalParameters[paramType][paramIndex] = { + name: '', + value: '', + sendIn: 'headers', + enabled: true + }; + } + + updatedAdditionalParameters[paramType][paramIndex][key] = value; + + // Only filter when updating a parameter + updateAdditionalParameters({ updatedAdditionalParameters }); + } + + const handleDeleteAdditionalParam = ({ paramType, paramIndex }) => { + const updatedAdditionalParameters = cloneDeep(additionalParameters); + + if (updatedAdditionalParameters[paramType]?.length) { + updatedAdditionalParameters[paramType] = updatedAdditionalParameters[paramType].filter((_, index) => index !== paramIndex); + + // If the array is now empty, ensure we're not sending empty arrays + if (updatedAdditionalParameters[paramType].length === 0) { + delete updatedAdditionalParameters[paramType]; + } + } + + updateAdditionalParameters({ updatedAdditionalParameters }); + } + + 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(); + + // Define available tabs for each grant type + const getAvailableTabs = (grantType) => { + const tabConfig = { + 'authorization_code': ['authorization', 'token', 'refresh'], + 'implicit': ['authorization'], + 'password': ['token', 'refresh'], + 'client_credentials': ['token', 'refresh'] + }; + return tabConfig[grantType] || ['token', 'refresh']; + }; + + const availableTabs = getAvailableTabs(grantType); + + const renderTab = (tabKey, tabLabel) => ( +
setActiveTab(tabKey)} + > + {tabLabel} +
+ ); + + return ( + +
+
+ +
+ + Additional Parameters + +
+ +
+ {availableTabs.includes('authorization') && renderTab('authorization', 'Authorization')} + {availableTabs.includes('token') && renderTab('token', 'Token')} + {availableTabs.includes('refresh') && renderTab('refresh', 'Refresh')} +
+ + + {(additionalParameters?.[activeTab] || []).map((param, index) => + + + + + + + )} + +
+ handleUpdateAdditionalParam({ + paramType: activeTab, + key: 'name', + paramIndex: index, + value + })} + collection={collection} + onSave={handleSave} + /> + + handleUpdateAdditionalParam({ + paramType: activeTab, + key: 'value', + paramIndex: index, + value + })} + collection={collection} + onSave={handleSave} + /> + +
+ +
+
+
+ { + handleUpdateAdditionalParam({ + paramType: activeTab, + key: 'enabled', + paramIndex: index, + value: e.target.checked + }) + }} + /> + +
+
+
+ + Add Parameter +
+
+ ) +} + +export default AdditionalParams; + +const sendInOptionsMap = { + 'authorization_code': { + 'authorization': ['headers', 'queryparams'], + 'token': ['headers', 'queryparams', 'body'], + 'refresh': ['headers', 'queryparams', 'body'] + }, + 'password': { + 'token': ['headers', 'queryparams', 'body'], + 'refresh': ['headers', 'queryparams', 'body'] + }, + 'client_credentials': { + 'token': ['headers', 'queryparams', 'body'], + 'refresh': ['headers', 'queryparams', 'body'] + }, + 'implicit': { + 'authorization': ['headers', 'queryparams'] + } +} \ 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 c46ce951d..7c890513e 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 @@ -10,6 +10,7 @@ import StyledWrapper from './StyledWrapper'; import { inputsConfig } from './inputsConfig'; import Oauth2TokenViewer from '../Oauth2TokenViewer/index'; import Oauth2ActionButtons from '../Oauth2ActionButtons/index'; +import AdditionalParams from '../AdditionalParams/index'; import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAuth, collection, folder }) => { @@ -35,7 +36,8 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu tokenQueryKey, refreshTokenUrl, autoRefreshToken, - autoFetchToken + autoFetchToken, + additionalParameters } = oAuth; const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== ''; @@ -85,6 +87,7 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu refreshTokenUrl, autoRefreshToken, autoFetchToken, + additionalParameters, [key]: value, } }) @@ -112,6 +115,7 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu tokenHeaderPrefix, tokenQueryKey, autoFetchToken, + additionalParameters, pkce: !Boolean(oAuth?.['pkce']) } }) @@ -332,6 +336,13 @@ 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 667e965a6..253adeac0 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 @@ -10,6 +10,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'; import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAuth, collection }) => { @@ -32,7 +33,8 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu tokenQueryKey, refreshTokenUrl, autoRefreshToken, - autoFetchToken + autoFetchToken, + additionalParameters } = oAuth; const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== ''; @@ -79,6 +81,7 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu refreshTokenUrl, autoRefreshToken, autoFetchToken, + additionalParameters, [key]: value } }) @@ -290,7 +293,13 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu - + diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Implicit/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Implicit/index.js index 3952180b0..9715aa3ce 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Implicit/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Implicit/index.js @@ -1,16 +1,15 @@ -import React, { useRef, forwardRef, useState, useMemo } from 'react'; +import React, { useRef, forwardRef, useMemo } from 'react'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; -import { IconCaretDown, IconLoader2, IconSettings, IconKey, IconHelp, IconAdjustmentsHorizontal } from '@tabler/icons'; +import { IconCaretDown, IconSettings, IconKey, IconHelp, IconAdjustmentsHorizontal } from '@tabler/icons'; import Dropdown from 'components/Dropdown'; import SingleLineEditor from 'components/SingleLineEditor'; -import { clearOauth2Cache, fetchOauth2Credentials } from 'providers/ReduxStore/slices/collections/actions'; import Wrapper from './StyledWrapper'; import { inputsConfig } from './inputsConfig'; -import toast from 'react-hot-toast'; import Oauth2TokenViewer from '../Oauth2TokenViewer/index'; -import { cloneDeep } from 'lodash'; +import Oauth2ActionButtons from '../Oauth2ActionButtons/index'; +import AdditionalParams from '../AdditionalParams/index'; import { getAllVariables } from 'utils/collections/index'; import { interpolate } from '@usebruno/common'; @@ -19,7 +18,6 @@ const OAuth2Implicit = ({ save, item = {}, request, handleRun, updateAuth, colle const { storedTheme } = useTheme(); const dropdownTippyRef = useRef(); const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); - const [fetchingToken, toggleFetchingToken] = useState(false); const oAuth = get(request, 'auth.oauth2', {}); const { @@ -49,38 +47,6 @@ const OAuth2Implicit = ({ save, item = {}, request, handleRun, updateAuth, colle ); }); - const handleFetchOauth2Credentials = async () => { - let requestCopy = cloneDeep(request); - requestCopy.oauth2 = requestCopy?.auth.oauth2; - requestCopy.headers = {}; - toggleFetchingToken(true); - try { - const result = await dispatch(fetchOauth2Credentials({ - itemUid: item.uid, - request: requestCopy, - collection, - folderUid: folder?.uid || null, - forceGetToken: true - })); - - toggleFetchingToken(false); - - // Check if the result contains error or if access_token is missing - if (result?.error || !result?.access_token) { - const errorMessage = result?.error || 'No access token received from authorization server'; - toast.error(errorMessage); - return; - } - - toast.success('Token fetched successfully!'); - } - catch (error) { - console.error(error); - toggleFetchingToken(false); - toast.error(error?.message || 'An error occurred while fetching token!'); - } - } - const handleSave = () => { save(); }; const handleChange = (key, value) => { @@ -111,16 +77,6 @@ const OAuth2Implicit = ({ save, item = {}, request, handleRun, updateAuth, colle handleChange('autoFetchToken', e.target.checked); }; - const handleClearCache = (e) => { - dispatch(clearOauth2Cache({ collectionUid: collection?.uid, url: interpolatedAuthUrl, credentialsId })) - .then(() => { - toast.success('Cleared cache successfully'); - }) - .catch((err) => { - toast.error(err.message); - }); - }; - return ( @@ -262,18 +218,14 @@ const OAuth2Implicit = ({ save, item = {}, request, handleRun, updateAuth, colle -
- - -
+ +
); }; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Oauth2ActionButtons/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Oauth2ActionButtons/index.js index de729fdd5..3d3dc697d 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Oauth2ActionButtons/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Oauth2ActionButtons/index.js @@ -99,12 +99,22 @@ const Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, c return (
- - {creds?.refresh_token ? : null} + {creds?.refresh_token ? + + : null} 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 db969d8cc..f048183e6 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 @@ -10,6 +10,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'; import SensitiveFieldWarning from 'components/SensitiveFieldWarning/index'; const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, updateAuth, collection }) => { @@ -34,7 +35,8 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update tokenQueryKey, refreshTokenUrl, autoRefreshToken, - autoFetchToken + autoFetchToken, + additionalParameters } = oAuth; const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== ''; @@ -82,6 +84,7 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update refreshTokenUrl, autoRefreshToken, autoFetchToken, + additionalParameters, [key]: value } }) @@ -293,6 +296,13 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
+ ); diff --git a/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Request/index.js b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Request/index.js index 400f3ffe5..053be2916 100644 --- a/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Request/index.js +++ b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Request/index.js @@ -26,7 +26,7 @@ const Request = ({ collection, request, item }) => {
{/* Method and URL */}
-
{url}
+
{url}
{/* Headers */} diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index a9a5af093..75b5ecc55 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -1589,7 +1589,7 @@ export const refreshOauth2Credentials = (payload) => async (dispatch, getState) request.globalEnvironmentVariables = globalEnvironmentVariables; return new Promise((resolve, reject) => { window.ipcRenderer - .invoke('renderer:refresh-oauth2-credentials', { request, collection }) + .invoke('renderer:refresh-oauth2-credentials', { itemUid, request, collection }) .then(({ credentials, url, collectionUid, debugInfo, credentialsId }) => { dispatch( collectionAddOauth2CredentialsByUrl({ diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index c3b1c1048..30cf11111 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -973,9 +973,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection const processEnvVars = getProcessEnvVars(collectionUid); const partialItem = { uid: itemUid }; const requestTreePath = getTreePathFromCollectionToItem(collection, partialItem); - if (requestTreePath && requestTreePath.length > 0) { - mergeVars(collection, requestCopy, requestTreePath); - } + mergeVars(collection, requestCopy, requestTreePath); interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); const certsAndProxyConfig = await getCertsAndProxyConfig({ @@ -1096,7 +1094,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } }); - ipcMain.handle('renderer:refresh-oauth2-credentials', async (event, { request, collection }) => { + ipcMain.handle('renderer:refresh-oauth2-credentials', async (event, { itemUid, request, collection }) => { try { if (request.oauth2) { let requestCopy = _.cloneDeep(request); @@ -1104,7 +1102,11 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection const environment = _.find(environments, (e) => e.uid === activeEnvironmentUid); const envVars = getEnvVars(environment); const processEnvVars = getProcessEnvVars(collectionUid); + const partialItem = { uid: itemUid }; + const requestTreePath = getTreePathFromCollectionToItem(collection, partialItem); + mergeVars(collection, requestCopy, requestTreePath); interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); + const certsAndProxyConfig = await getCertsAndProxyConfig({ collectionUid, request: requestCopy, 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 0cf45d70c..4c671fb62 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, grantType = 'authorization_code' }) => { +const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session, additionalHeaders = {}, grantType = 'authorization_code' }) => { return new Promise(async (resolve, reject) => { let finalUrl = null; let debugInfo = { @@ -75,6 +75,14 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session, grantType = 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, @@ -211,4 +219,4 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session, grantType = }); }; -module.exports = { authorizeUserInWindow, matchesCallbackUrl }; \ No newline at end of file +module.exports = { authorizeUserInWindow, matchesCallbackUrl }; diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index db7e56b8e..4aef1e73f 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -243,6 +243,39 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc default: break; } + + // Interpolate additional parameters for all OAuth2 grant types + if (request.oauth2.additionalParameters) { + // Interpolate authorization parameters + if (Array.isArray(request.oauth2.additionalParameters.authorization)) { + request.oauth2.additionalParameters.authorization.forEach(param => { + if (param && param.enabled !== false) { + param.name = _interpolate(param.name) || ''; + param.value = _interpolate(param.value) || ''; + } + }); + } + + // Interpolate token parameters + if (Array.isArray(request.oauth2.additionalParameters.token)) { + request.oauth2.additionalParameters.token.forEach(param => { + if (param && param.enabled !== false) { + param.name = _interpolate(param.name) || ''; + param.value = _interpolate(param.value) || ''; + } + }); + } + + // Interpolate refresh parameters + if (Array.isArray(request.oauth2.additionalParameters.refresh)) { + request.oauth2.additionalParameters.refresh.forEach(param => { + if (param && param.enabled !== false) { + param.name = _interpolate(param.name) || ''; + param.value = _interpolate(param.value) || ''; + } + }); + } + } } // interpolate vars for aws sigv4 auth diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 0a95657c7..375422164 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -88,7 +88,8 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'), tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'), autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'), - autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken') + autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken'), + additionalParameters: get(collectionAuth, 'oauth2.additionalParameters', { authorization: [], token: [], refresh: [] }) }; break; case 'authorization_code': @@ -109,7 +110,8 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'), tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'), autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'), - autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken') + autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken'), + additionalParameters: get(collectionAuth, 'oauth2.additionalParameters', { authorization: [], token: [], refresh: [] }) }; break; case 'implicit': @@ -124,7 +126,8 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'), tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'), tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'), - autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken') + autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'), + additionalParameters: get(collectionAuth, 'oauth2.additionalParameters', { authorization: [], token: [], refresh: [] }) }; break; case 'client_credentials': @@ -141,7 +144,8 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'), tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'), autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'), - autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken') + autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken'), + additionalParameters: get(collectionAuth, 'oauth2.additionalParameters', { authorization: [], token: [], refresh: [] }) }; break; } @@ -201,7 +205,8 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'), tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'), autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'), - autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken') + autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken'), + additionalParameters: get(request, 'auth.oauth2.additionalParameters', { authorization: [], token: [], refresh: [] }) }; break; case 'authorization_code': @@ -222,7 +227,8 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'), tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'), autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'), - autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken') + autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken'), + additionalParameters: get(request, 'auth.oauth2.additionalParameters', { authorization: [], token: [], refresh: [] }) }; break; case 'implicit': @@ -237,7 +243,8 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'), tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'), tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'), - autoFetchToken: get(request, 'auth.oauth2.autoFetchToken') + autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'), + additionalParameters: get(request, 'auth.oauth2.additionalParameters', { authorization: [], token: [], refresh: [] }) }; break; case 'client_credentials': @@ -254,7 +261,8 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'), tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'), autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'), - autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken') + autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken'), + additionalParameters: get(request, 'auth.oauth2.additionalParameters', { authorization: [], token: [], refresh: [] }) }; break; } diff --git a/packages/bruno-electron/src/utils/collection.js b/packages/bruno-electron/src/utils/collection.js index 2d5bae473..1ab774211 100644 --- a/packages/bruno-electron/src/utils/collection.js +++ b/packages/bruno-electron/src/utils/collection.js @@ -47,7 +47,7 @@ const mergeHeaders = (collection, request, requestTreePath) => { request.headers = Array.from(headers, ([name, value]) => ({ name, value, enabled: true })); }; -const mergeVars = (collection, request, requestTreePath) => { +const mergeVars = (collection, request, requestTreePath = []) => { let reqVars = new Map(); let collectionRequestVars = get(collection, 'root.request.vars.req', []); let collectionVariables = {}; diff --git a/packages/bruno-electron/src/utils/oauth2.js b/packages/bruno-electron/src/utils/oauth2.js index c4e01e364..61d3d80c3 100644 --- a/packages/bruno-electron/src/utils/oauth2.js +++ b/packages/bruno-electron/src/utils/oauth2.js @@ -148,6 +148,7 @@ const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, fo credentialsId, autoRefreshToken, autoFetchToken, + additionalParameters, } = oAuth; const url = requestCopy?.oauth2?.accessTokenUrl; @@ -242,7 +243,7 @@ const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, fo } // Fetch new token process - const { authorizationCode, debugInfo } = await getOAuth2AuthorizationCode(requestCopy, codeChallenge, collectionUid); + let { authorizationCode, debugInfo } = await getOAuth2AuthorizationCode(requestCopy, codeChallenge, collectionUid); let axiosRequestConfig = {}; axiosRequestConfig.method = 'POST'; @@ -268,9 +269,13 @@ const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, fo data['code_verifier'] = codeVerifier; } - axiosRequestConfig.data = qs.stringify(data); axiosRequestConfig.url = url; axiosRequestConfig.responseType = 'arraybuffer'; + // Apply additional parameters to token request + if (additionalParameters?.token?.length) { + applyAdditionalParameters(axiosRequestConfig, data, additionalParameters.token); + } + axiosRequestConfig.data = qs.stringify(data); try { const { credentials, requestDetails } = await getCredentialsFromTokenUrl({ requestConfig: axiosRequestConfig, certsAndProxyConfig }); @@ -292,7 +297,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'); @@ -310,12 +315,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) { @@ -324,6 +340,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 }) => { @@ -337,6 +368,7 @@ const getOAuth2TokenUsingClientCredentials = async ({ request, collectionUid, fo credentialsId, autoRefreshToken, autoFetchToken, + additionalParameters, } = oAuth; const url = requestCopy?.oauth2?.accessTokenUrl; @@ -433,9 +465,12 @@ const getOAuth2TokenUsingClientCredentials = async ({ request, collectionUid, fo if (scope && scope.trim() !== '') { data.scope = scope; } - axiosRequestConfig.data = qs.stringify(data); axiosRequestConfig.url = url; axiosRequestConfig.responseType = 'arraybuffer'; + if (additionalParameters?.token?.length) { + applyAdditionalParameters(axiosRequestConfig, data, additionalParameters.token); + } + axiosRequestConfig.data = qs.stringify(data); let debugInfo = { data: [] }; try { const { credentials, requestDetails } = await getCredentialsFromTokenUrl({ requestConfig: axiosRequestConfig, certsAndProxyConfig }); @@ -462,6 +497,7 @@ const getOAuth2TokenUsingPasswordCredentials = async ({ request, collectionUid, credentialsId, autoRefreshToken, autoFetchToken, + additionalParameters, } = oAuth; const url = requestCopy?.oauth2?.accessTokenUrl; @@ -578,9 +614,12 @@ const getOAuth2TokenUsingPasswordCredentials = async ({ request, collectionUid, if (scope && scope.trim() !== '') { data.scope = scope; } - axiosRequestConfig.data = qs.stringify(data); axiosRequestConfig.url = url; axiosRequestConfig.responseType = 'arraybuffer'; + if (additionalParameters?.token?.length) { + applyAdditionalParameters(axiosRequestConfig, data, additionalParameters.token); + } + axiosRequestConfig.data = qs.stringify(data); let debugInfo = { data: [] }; try { const { credentials, requestDetails } = await getCredentialsFromTokenUrl({ requestConfig: axiosRequestConfig, certsAndProxyConfig }); @@ -594,7 +633,7 @@ const getOAuth2TokenUsingPasswordCredentials = async ({ request, collectionUid, const refreshOauth2Token = async ({ requestCopy, collectionUid, certsAndProxyConfig }) => { const oAuth = get(requestCopy, 'oauth2', {}); - const { clientId, clientSecret, credentialsId, credentialsPlacement } = oAuth; + const { clientId, clientSecret, credentialsId, credentialsPlacement, additionalParameters } = oAuth; const url = oAuth.refreshTokenUrl ? oAuth.refreshTokenUrl : oAuth.accessTokenUrl; const credentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId }); @@ -622,9 +661,12 @@ const refreshOauth2Token = async ({ requestCopy, collectionUid, certsAndProxyCon if (credentialsPlacement === "basic_auth_header") { axiosRequestConfig.headers['Authorization'] = `Basic ${Buffer.from(`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`).toString('base64')}`; } - axiosRequestConfig.data = qs.stringify(data); axiosRequestConfig.url = url; axiosRequestConfig.responseType = 'arraybuffer'; + if (additionalParameters?.refresh?.length) { + applyAdditionalParameters(axiosRequestConfig, data, additionalParameters.refresh); + } + axiosRequestConfig.data = qs.stringify(data); let debugInfo = { data: [] }; try { const { credentials, requestDetails } = await getCredentialsFromTokenUrl({ requestConfig: axiosRequestConfig, certsAndProxyConfig }); @@ -659,6 +701,36 @@ const generateCodeChallenge = (codeVerifier) => { return base64Hash; }; +// Apply additional parameters to a request +const applyAdditionalParameters = (requestCopy, data, params = []) => { + 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 + try { + let url = new URL(requestCopy.url); + url.searchParams.append(param.name, param.value || ''); + requestCopy.url = url.href; + } + catch (error) { + console.error('invalid token/refresh url', requestCopy.url); + } + break; + case 'body': + // For body, add to data object + data[param.name] = param.value || ''; + break; + } + }); +} + const getOAuth2TokenUsingImplicitGrant = async ({ request, collectionUid, forceFetch = false }) => { const { oauth2 = {} } = request; const { @@ -668,7 +740,8 @@ const getOAuth2TokenUsingImplicitGrant = async ({ request, collectionUid, forceF state = '', callbackUrl, credentialsId = 'credentials', - autoFetchToken = true + autoFetchToken = true, + additionalParameters } = oauth2; // Validate required fields @@ -753,6 +826,15 @@ const getOAuth2TokenUsingImplicitGrant = async ({ request, collectionUid, forceF 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 || ''); + } + } + }); + } const authorizeUrl = authorizationUrlWithQueryParams.toString(); @@ -761,7 +843,8 @@ const getOAuth2TokenUsingImplicitGrant = async ({ request, collectionUid, forceF authorizeUrl, callbackUrl, session: oauth2Store.getSessionIdOfCollection({ collectionUid, url: authorizationUrl }), - grantType: 'implicit' + grantType: 'implicit', + additionalHeaders: getAdditionalHeaders(additionalParameters?.authorization) }); if (!implicitTokens || !implicitTokens.access_token) { diff --git a/packages/bruno-filestore/src/formats/bru/index.ts b/packages/bruno-filestore/src/formats/bru/index.ts index c5358d23f..79095e945 100644 --- a/packages/bruno-filestore/src/formats/bru/index.ts +++ b/packages/bruno-filestore/src/formats/bru/index.ts @@ -7,6 +7,7 @@ import { collectionBruToJson as _collectionBruToJson, jsonToCollectionBru as _jsonToCollectionBru } from '@usebruno/lang'; +import { getOauth2AdditionalParameters } from './utils/oauth2-additional-params'; export const bruRequestToJson = (data: string | any, parsed: boolean = false): any => { try { @@ -72,6 +73,16 @@ export const bruRequestToJson = (data: string | any, parsed: boolean = false): a transformedJson.request.body.mode = _.get(json, 'http.body', 'none'); } + // add oauth2 additional parameters if they exist + const hasOauth2GrantType = json?.auth?.oauth2?.grantType; + if (hasOauth2GrantType) { + const additionalParameters = getOauth2AdditionalParameters(json); + const hasAdditionalParameters = Object.keys(additionalParameters || {}).length > 0; + if (hasAdditionalParameters) { + transformedJson.request.auth.oauth2.additionalParameters = additionalParameters; + } + } + return transformedJson; } catch (error) { throw error; @@ -200,6 +211,16 @@ export const bruCollectionToJson = (data: string | any, parsed: boolean = false) } } + // add oauth2 additional parameters if they exist + const hasOauth2GrantType = json?.auth?.oauth2?.grantType; + if (hasOauth2GrantType) { + const additionalParameters = getOauth2AdditionalParameters(json); + const hasAdditionalParameters = Object.keys(additionalParameters).length > 0; + if (hasAdditionalParameters) { + transformedJson.request.auth.oauth2.additionalParameters = additionalParameters; + } + } + return transformedJson; } catch (error) { return Promise.reject(error); diff --git a/packages/bruno-filestore/src/formats/bru/tests/fixtures/oauth2-additional-params.js b/packages/bruno-filestore/src/formats/bru/tests/fixtures/oauth2-additional-params.js new file mode 100644 index 000000000..c3498340c --- /dev/null +++ b/packages/bruno-filestore/src/formats/bru/tests/fixtures/oauth2-additional-params.js @@ -0,0 +1,116 @@ +const getBruJsonWithAdditionalParams = (grantType) => ({ + "meta": { + "name": "OAuth2 Additional Params Test", + "type": "http", + "seq": 1 + }, + "http": { + "method": "get", + "url": "https://api.usebruno.com/protected" + }, + "auth": { + "oauth2": { + "grantType": grantType, + }, + }, + "oauth2_additional_parameters_auth_req_headers": [ + { + "name": "auth-header", + "value": "auth-header-value", + "enabled": true + }, + { + "name": "disabled-auth-header", + "value": "disabled-auth-header-value", + "enabled": false + } + ], + "oauth2_additional_parameters_auth_req_queryparams": [ + { + "name": "auth-query-param", + "value": "auth-query-param-value", + "enabled": true + }, + { + "name": "disabled-auth-query-param", + "value": "disabled-auth-query-param-value", + "enabled": false + } + ], + "oauth2_additional_parameters_access_token_req_headers": [ + { + "name": "token-header", + "value": "token-header-value", + "enabled": true + }, + { + "name": "disabled-token-header", + "value": "disabled-token-header-value", + "enabled": false + } + ], + "oauth2_additional_parameters_access_token_req_queryparams": [ + { + "name": "token-query-param", + "value": "token-query-param-value", + "enabled": true + }, + { + "name": "disabled-token-query-param", + "value": "disabled-token-query-param-value", + "enabled": false + } + ], + "oauth2_additional_parameters_access_token_req_bodyvalues": [ + { + "name": "token-body", + "value": "token-body-value", + "enabled": true + }, + { + "name": "disabled-token-body", + "value": "disabled-token-body-value", + "enabled": false + } + ], + "oauth2_additional_parameters_refresh_token_req_headers": [ + { + "name": "refresh-header", + "value": "refresh-header-value", + "enabled": true + }, + { + "name": "disabled-refresh-header", + "value": "disabled-refresh-header-value", + "enabled": false + } + ], + "oauth2_additional_parameters_refresh_token_req_queryparams": [ + { + "name": "refresh-query-param", + "value": "refresh-query-param-value", + "enabled": true + }, + { + "name": "disabled-refresh-query-param", + "value": "disabled-refresh-query-param-value", + "enabled": false + } + ], + "oauth2_additional_parameters_refresh_token_req_bodyvalues": [ + { + "name": "refresh-body", + "value": "refresh-body-value", + "enabled": true + }, + { + "name": "disabled-refresh-body", + "value": "disabled-refresh-body-value", + "enabled": false + } + ] +}) + +export { + getBruJsonWithAdditionalParams +}; diff --git a/packages/bruno-filestore/src/formats/bru/tests/oauth2-additional-params.spec.js b/packages/bruno-filestore/src/formats/bru/tests/oauth2-additional-params.spec.js new file mode 100644 index 000000000..707c6127c --- /dev/null +++ b/packages/bruno-filestore/src/formats/bru/tests/oauth2-additional-params.spec.js @@ -0,0 +1,45 @@ +const { getOauth2AdditionalParameters } = require('../utils/oauth2-additional-params'); +const { bruRequestToJson, bruCollectionToJson } = require('../index'); +const { getBruJsonWithAdditionalParams } = require('./fixtures/oauth2-additional-params'); + +describe('getOauth2AdditionalParameters', () => { + it('authorization_code', () => { + const additionalParameters = getOauth2AdditionalParameters(getBruJsonWithAdditionalParams('authorization_code')); + expect(additionalParameters.authorization).toHaveLength(4); + expect(additionalParameters.token).toHaveLength(6); + expect(additionalParameters.refresh).toHaveLength(6); + + expect(additionalParameters.authorization.map(p => p.sendIn).sort()).toEqual(['headers', 'headers', 'queryparams', 'queryparams']); + expect(additionalParameters.token.map(p => p.sendIn).sort()).toEqual(['body', 'body', 'headers', 'headers', 'queryparams', 'queryparams']); + expect(additionalParameters.refresh.map(p => p.sendIn).sort()).toEqual(['body', 'body', 'headers', 'headers', 'queryparams', 'queryparams']); + }); + + it('client_credentials', () => { + const additionalParameters = getOauth2AdditionalParameters(getBruJsonWithAdditionalParams('client_credentials')); + expect(additionalParameters.authorization).toBeUndefined(); + expect(additionalParameters.token).toHaveLength(6); + expect(additionalParameters.refresh).toHaveLength(6); + + expect(additionalParameters.token.map(p => p.sendIn).sort()).toEqual(['body', 'body', 'headers', 'headers', 'queryparams', 'queryparams']); + expect(additionalParameters.refresh.map(p => p.sendIn).sort()).toEqual(['body', 'body', 'headers', 'headers', 'queryparams', 'queryparams']); + }); + + it('password', () => { + const additionalParameters = getOauth2AdditionalParameters(getBruJsonWithAdditionalParams('password')); + expect(additionalParameters.authorization).toBeUndefined(); + expect(additionalParameters.token).toHaveLength(6); + expect(additionalParameters.refresh).toHaveLength(6); + + expect(additionalParameters.token.map(p => p.sendIn).sort()).toEqual(['body', 'body', 'headers', 'headers', 'queryparams', 'queryparams']); + expect(additionalParameters.refresh.map(p => p.sendIn).sort()).toEqual(['body', 'body', 'headers', 'headers', 'queryparams', 'queryparams']); + }); + + it('implicit', () => { + const additionalParameters = getOauth2AdditionalParameters(getBruJsonWithAdditionalParams('implicit')); + expect(additionalParameters.authorization).toHaveLength(4); + expect(additionalParameters.token).toBeUndefined(); + expect(additionalParameters.refresh).toBeUndefined(); + + expect(additionalParameters.authorization.map(p => p.sendIn).sort()).toEqual(['headers', 'headers', 'queryparams', 'queryparams']); + }); +}); \ No newline at end of file diff --git a/packages/bruno-filestore/src/formats/bru/utils/oauth2-additional-params.ts b/packages/bruno-filestore/src/formats/bru/utils/oauth2-additional-params.ts new file mode 100644 index 000000000..54a613807 --- /dev/null +++ b/packages/bruno-filestore/src/formats/bru/utils/oauth2-additional-params.ts @@ -0,0 +1,141 @@ +type T_Oauth2ParameterType = 'authorization' | 'token' | 'refresh'; +type T_Oauth2ParameterSendInType = 'headers' | 'queryparams' | 'body'; + +export interface T_OAuth2AdditionalParam { + name: string; + value: string; + enabled: boolean; + sendIn: T_Oauth2ParameterSendInType +} + +export interface T_OAuth2AdditionalParameters { + authorization?: T_OAuth2AdditionalParam[]; + token?: T_OAuth2AdditionalParam[]; + refresh?: T_OAuth2AdditionalParam[]; +} + +export interface T_Oauth2Auth { + grantType: string; + additionalParameters?: T_OAuth2AdditionalParameters; +} + +export interface T_BruJson { + auth: { + oauth2: T_Oauth2Auth; + }; + oauth2_additional_parameters_auth_req_headers?: any[]; + oauth2_additional_parameters_auth_req_queryparams?: any[]; + oauth2_additional_parameters_access_token_req_headers?: any[]; + oauth2_additional_parameters_access_token_req_queryparams?: any[]; + oauth2_additional_parameters_access_token_req_bodyvalues?: any[]; + oauth2_additional_parameters_refresh_token_req_headers?: any[]; + oauth2_additional_parameters_refresh_token_req_queryparams?: any[]; + oauth2_additional_parameters_refresh_token_req_bodyvalues?: any[]; +} + +interface T_Oauth2ParameterMapping { + type: T_Oauth2ParameterType; + sendIn: T_Oauth2ParameterSendInType; + source: keyof T_BruJson; +} + +const PARAMETER_MAPPINGS: T_Oauth2ParameterMapping[] = [ + // Authorization parameters (only for authorization_code grant type) + { type: 'authorization', sendIn: 'headers', source: 'oauth2_additional_parameters_auth_req_headers' }, + { type: 'authorization', sendIn: 'queryparams', source: 'oauth2_additional_parameters_auth_req_queryparams' }, + + // Token parameters (for all grant types) + { type: 'token', sendIn: 'headers', source: 'oauth2_additional_parameters_access_token_req_headers' }, + { type: 'token', sendIn: 'queryparams', source: 'oauth2_additional_parameters_access_token_req_queryparams' }, + { type: 'token', sendIn: 'body', source: 'oauth2_additional_parameters_access_token_req_bodyvalues' }, + + // Refresh parameters (for grant types that support refresh) + { type: 'refresh', sendIn: 'headers', source: 'oauth2_additional_parameters_refresh_token_req_headers' }, + { type: 'refresh', sendIn: 'queryparams', source: 'oauth2_additional_parameters_refresh_token_req_queryparams' }, + { type: 'refresh', sendIn: 'body', source: 'oauth2_additional_parameters_refresh_token_req_bodyvalues' }, +]; + +/** + * Maps source parameters to T_OAuth2AdditionalParam format + */ +const mapParametersFromSource = (sourceParams: any[], sendIn: T_Oauth2ParameterSendInType): T_OAuth2AdditionalParam[] => { + if (!sourceParams?.length) { + return []; + } + + return sourceParams.map(param => ({ + ...param, + sendIn + })); +}; + +/** + * Checks if a parameter type should be included based on grant type + */ +const shouldIncludeParameterType = (type: T_Oauth2ParameterType, grantType: string): boolean => { + // Authorization parameters are only valid for authorization_code grant type + if (type === 'authorization') { + return grantType === 'authorization_code' || grantType === 'implicit'; + } + + if (type === 'token' || type === 'refresh') { + return grantType !== 'implicit'; + } + + // Token and refresh parameters are valid for all grant types + return true; +}; + +/** + * Collects all parameters for a specific type (authorization, token, or refresh) + */ +const collectParametersForType = ( + json: T_BruJson, + type: T_Oauth2ParameterType, + grantType: string +): T_OAuth2AdditionalParam[] => { + if (!shouldIncludeParameterType(type, grantType)) { + return []; + } + + const relevantMappings = PARAMETER_MAPPINGS.filter(mapping => mapping.type === type); + const allParams: T_OAuth2AdditionalParam[] = []; + + for (const mapping of relevantMappings) { + const sourceParams = json[mapping.source] as any[]; + const mappedParams = mapParametersFromSource(sourceParams, mapping.sendIn); + allParams.push(...mappedParams); + } + + return allParams; +}; + +/** + * This function extracts OAuth2 additional parameters from various sources in the bru json data and organizes + * them into a structured format based on their usage context (authorization, token, refresh). + * + * @param json - json object containing OAuth2 configuration and additional parameters + * @returns OAuth2 additional parameters + */ +export const getOauth2AdditionalParameters = (json: T_BruJson): T_OAuth2AdditionalParameters => { + const grantType = json.auth.oauth2.grantType; + const additionalParameters: T_OAuth2AdditionalParameters = {}; + + try { + // Collect parameters for each type + const parameterTypes: T_Oauth2ParameterType[] = ['authorization', 'token', 'refresh']; + + for (const type of parameterTypes) { + const params = collectParametersForType(json, type, grantType); + if (params.length > 0) { + additionalParameters[type] = params; + } + } + } + catch(error) { + console.error(error); + console.error("Error while getting the oauth2 additional parameters!"); + } + + return additionalParameters; +}; \ No newline at end of file diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index 68c2a6916..baa5b4ead 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -30,11 +30,17 @@ const { safeParseJson, outdentString } = require('./utils'); */ const grammar = ohm.grammar(`Bru { BruFile = (meta | http | grpc | query | params | headers | metadata | auths | bodies | varsandassert | script | tests | settings | docs)* - auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth2 | authwsse | authapikey + auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth2 | authwsse | authapikey | authOauth2Configs bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body | bodygrpc bodyforms = bodyformurlencoded | bodymultipart | bodyfile params = paramspath | paramsquery - + + // Oauth2 additional parameters + authOauth2Configs = oauth2AuthReqConfig | oauth2AccessTokenReqConfig | oauth2RefreshTokenReqConfig + oauth2AuthReqConfig = oauth2AuthReqHeaders | oauth2AuthReqQueryParams + oauth2AccessTokenReqConfig = oauth2AccessTokenReqHeaders | oauth2AccessTokenReqQueryParams | oauth2AccessTokenReqBody + oauth2RefreshTokenReqConfig = oauth2RefreshTokenReqHeaders | oauth2RefreshTokenReqQueryParams | oauth2RefreshTokenReqBody + nl = "\\r"? "\\n" st = " " | "\\t" stnl = st | nl @@ -109,6 +115,15 @@ const grammar = ohm.grammar(`Bru { authwsse = "auth:wsse" dictionary authapikey = "auth:apikey" dictionary + oauth2AuthReqHeaders = "auth:oauth2:additional_params:auth_req:headers" dictionary + oauth2AuthReqQueryParams = "auth:oauth2:additional_params:auth_req:queryparams" dictionary + oauth2AccessTokenReqHeaders = "auth:oauth2:additional_params:access_token_req:headers" dictionary + oauth2AccessTokenReqQueryParams = "auth:oauth2:additional_params:access_token_req:queryparams" dictionary + oauth2AccessTokenReqBody = "auth:oauth2:additional_params:access_token_req:body" dictionary + oauth2RefreshTokenReqHeaders = "auth:oauth2:additional_params:refresh_token_req:headers" dictionary + oauth2RefreshTokenReqQueryParams = "auth:oauth2:additional_params:refresh_token_req:queryparams" dictionary + oauth2RefreshTokenReqBody = "auth:oauth2:additional_params:refresh_token_req:body" dictionary + body = "body" st* "{" nl* textblock tagend bodyjson = "body:json" st* "{" nl* textblock tagend bodytext = "body:text" st* "{" nl* textblock tagend @@ -662,6 +677,46 @@ const sem = grammar.createSemantics().addAttribute('ast', { } }; }, + oauth2AuthReqHeaders(_1, dictionary) { + return { + oauth2_additional_parameters_auth_req_headers: mapPairListToKeyValPairs(dictionary.ast) + }; + }, + oauth2AuthReqQueryParams(_1, dictionary) { + return { + oauth2_additional_parameters_auth_req_queryparams: mapPairListToKeyValPairs(dictionary.ast) + }; + }, + oauth2AccessTokenReqHeaders(_1, dictionary) { + return { + oauth2_additional_parameters_access_token_req_headers: mapPairListToKeyValPairs(dictionary.ast) + }; + }, + oauth2AccessTokenReqQueryParams(_1, dictionary) { + return { + oauth2_additional_parameters_access_token_req_queryparams: mapPairListToKeyValPairs(dictionary.ast) + }; + }, + oauth2AccessTokenReqBody(_1, dictionary) { + return { + oauth2_additional_parameters_access_token_req_bodyvalues: mapPairListToKeyValPairs(dictionary.ast) + }; + }, + oauth2RefreshTokenReqHeaders(_1, dictionary) { + return { + oauth2_additional_parameters_refresh_token_req_headers: mapPairListToKeyValPairs(dictionary.ast) + }; + }, + oauth2RefreshTokenReqQueryParams(_1, dictionary) { + return { + oauth2_additional_parameters_refresh_token_req_queryparams: mapPairListToKeyValPairs(dictionary.ast) + }; + }, + oauth2RefreshTokenReqBody(_1, dictionary) { + return { + oauth2_additional_parameters_refresh_token_req_bodyvalues: mapPairListToKeyValPairs(dictionary.ast) + }; + }, authwsse(_1, dictionary) { const auth = mapPairListToKeyValPairs(dictionary.ast, false); @@ -883,11 +938,12 @@ const parser = (input) => { const match = grammar.match(input); if (match.succeeded()) { - return sem(match).ast; + let ast = sem(match).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 d50926014..f3925ad62 100644 --- a/packages/bruno-lang/v2/src/collectionBruToJson.js +++ b/packages/bruno-lang/v2/src/collectionBruToJson.js @@ -4,7 +4,13 @@ const { safeParseJson, outdentString } = require('./utils'); const grammar = ohm.grammar(`Bru { BruFile = (meta | query | headers | auth | auths | vars | script | tests | docs)* - auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM |authOAuth2 | authwsse | authapikey + auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM |authOAuth2 | authwsse | authapikey | authOauth2Configs + + // Oauth2 additional parameters + authOauth2Configs = oauth2AuthReqConfig | oauth2AccessTokenReqConfig | oauth2RefreshTokenReqConfig + oauth2AuthReqConfig = oauth2AuthReqHeaders | oauth2AuthReqQueryParams + oauth2AccessTokenReqConfig = oauth2AccessTokenReqHeaders | oauth2AccessTokenReqQueryParams | oauth2AccessTokenReqBody + oauth2RefreshTokenReqConfig = oauth2RefreshTokenReqHeaders | oauth2RefreshTokenReqQueryParams | oauth2RefreshTokenReqBody nl = "\\r"? "\\n" st = " " | "\\t" @@ -30,6 +36,15 @@ const grammar = ohm.grammar(`Bru { auth = "auth" dictionary + oauth2AuthReqHeaders = "auth:oauth2:additional_params:auth_req:headers" dictionary + oauth2AuthReqQueryParams = "auth:oauth2:additional_params:auth_req:queryparams" dictionary + oauth2AccessTokenReqHeaders = "auth:oauth2:additional_params:access_token_req:headers" dictionary + oauth2AccessTokenReqQueryParams = "auth:oauth2:additional_params:access_token_req:queryparams" dictionary + oauth2AccessTokenReqBody = "auth:oauth2:additional_params:access_token_req:body" dictionary + oauth2RefreshTokenReqHeaders = "auth:oauth2:additional_params:refresh_token_req:headers" dictionary + oauth2RefreshTokenReqQueryParams = "auth:oauth2:additional_params:refresh_token_req:queryparams" dictionary + oauth2RefreshTokenReqBody = "auth:oauth2:additional_params:refresh_token_req:body" dictionary + headers = "headers" dictionary query = "query" dictionary @@ -362,6 +377,46 @@ const sem = grammar.createSemantics().addAttribute('ast', { } }; }, + oauth2AuthReqHeaders(_1, dictionary) { + return { + oauth2_additional_parameters_auth_req_headers: mapPairListToKeyValPairs(dictionary.ast) + }; + }, + oauth2AuthReqQueryParams(_1, dictionary) { + return { + oauth2_additional_parameters_auth_req_queryparams: mapPairListToKeyValPairs(dictionary.ast) + }; + }, + oauth2AccessTokenReqHeaders(_1, dictionary) { + return { + oauth2_additional_parameters_access_token_req_headers: mapPairListToKeyValPairs(dictionary.ast) + }; + }, + oauth2AccessTokenReqQueryParams(_1, dictionary) { + return { + oauth2_additional_parameters_access_token_req_queryparams: mapPairListToKeyValPairs(dictionary.ast) + }; + }, + oauth2AccessTokenReqBody(_1, dictionary) { + return { + oauth2_additional_parameters_access_token_req_bodyvalues: mapPairListToKeyValPairs(dictionary.ast) + }; + }, + oauth2RefreshTokenReqHeaders(_1, dictionary) { + return { + oauth2_additional_parameters_refresh_token_req_headers: mapPairListToKeyValPairs(dictionary.ast) + }; + }, + oauth2RefreshTokenReqQueryParams(_1, dictionary) { + return { + oauth2_additional_parameters_refresh_token_req_queryparams: mapPairListToKeyValPairs(dictionary.ast) + }; + }, + oauth2RefreshTokenReqBody(_1, dictionary) { + return { + oauth2_additional_parameters_refresh_token_req_bodyvalues: mapPairListToKeyValPairs(dictionary.ast) + }; + }, authwsse(_1, dictionary) { const auth = mapPairListToKeyValPairs(dictionary.ast, false); const userKey = _.find(auth, { name: 'username' }); @@ -465,7 +520,9 @@ const parser = (input) => { const match = grammar.match(input); if (match.succeeded()) { - return sem(match).ast; + let ast = sem(match).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 1e8f56241..26de19b51 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -339,6 +339,114 @@ ${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toStr `; 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:additional_params:auth_req:headers { +${indentString( + authorizationHeaders + .filter(item => item?.name?.length) + .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`) + .join('\n') + )} +} + +`; + } + const authorizationQueryParams = authorizationParams?.filter(p => p?.sendIn == 'queryparams'); + if (authorizationQueryParams?.length) { + bru += `auth:oauth2:additional_params:auth_req:queryparams { +${indentString( + authorizationQueryParams + .filter(item => item?.name?.length) + .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`) + .join('\n') + )} +} + +`; + } + const tokenHeaders = tokenParams?.filter(p => p?.sendIn == 'headers'); + if (tokenHeaders?.length) { + bru += `auth:oauth2:additional_params:access_token_req:headers { +${indentString( + tokenHeaders + .filter(item => item?.name?.length) + .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`) + .join('\n') + )} +} + +`; + } + const tokenQueryParams = tokenParams?.filter(p => p?.sendIn == 'queryparams'); + if (tokenQueryParams?.length) { + bru += `auth:oauth2:additional_params:access_token_req:queryparams { +${indentString( + tokenQueryParams + .filter(item => item?.name?.length) + .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`) + .join('\n') + )} +} + +`; + } + const tokenBodyValues = tokenParams?.filter(p => p?.sendIn == 'body'); + if (tokenBodyValues?.length) { + bru += `auth:oauth2:additional_params:access_token_req:body { +${indentString( + tokenBodyValues + .filter(item => item?.name?.length) + .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`) + .join('\n') + )} +} + +`; + } + const refreshHeaders = refreshParams?.filter(p => p?.sendIn == 'headers'); + if (refreshHeaders?.length) { + bru += `auth:oauth2:additional_params:refresh_token_req:headers { +${indentString( + refreshHeaders + .filter(item => item?.name?.length) + .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`) + .join('\n') + )} +} + +`; + } + const refreshQueryParams = refreshParams?.filter(p => p?.sendIn == 'queryparams'); + if (refreshQueryParams?.length) { + bru += `auth:oauth2:additional_params:refresh_token_req:queryparams { +${indentString( + refreshQueryParams + .filter(item => item?.name?.length) + .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`) + .join('\n') + )} +} + +`; + } + const refreshBodyValues = refreshParams?.filter(p => p?.sendIn == 'body'); + if (refreshBodyValues?.length) { + bru += `auth:oauth2:additional_params:refresh_token_req:body { +${indentString( + refreshBodyValues + .filter(item => item?.name?.length) + .map((item) => `${item.enabled ? '' : '~'}${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 adcdf6b06..d5aa1c1e0 100644 --- a/packages/bruno-lang/v2/src/jsonToCollectionBru.js +++ b/packages/bruno-lang/v2/src/jsonToCollectionBru.js @@ -234,6 +234,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:additional_params:auth_req:headers { +${indentString( + authorizationHeaders + .filter(item => item?.name?.length) + .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`) + .join('\n') + )} +} + +`; + } + const authorizationQueryParams = authorizationParams?.filter(p => p?.sendIn == 'queryparams'); + if (authorizationQueryParams?.length) { + bru += `auth:oauth2:additional_params:auth_req:queryparams { +${indentString( + authorizationQueryParams + .filter(item => item?.name?.length) + .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`) + .join('\n') + )} +} + +`; + } + const tokenHeaders = tokenParams?.filter(p => p?.sendIn == 'headers'); + if (tokenHeaders?.length) { + bru += `auth:oauth2:additional_params:access_token_req:headers { +${indentString( + tokenHeaders + .filter(item => item?.name?.length) + .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`) + .join('\n') + )} +} + +`; + } + const tokenQueryParams = tokenParams?.filter(p => p?.sendIn == 'queryparams'); + if (tokenQueryParams?.length) { + bru += `auth:oauth2:additional_params:access_token_req:queryparams { +${indentString( + tokenQueryParams + .filter(item => item?.name?.length) + .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`) + .join('\n') + )} +} + +`; + } + const tokenBodyValues = tokenParams?.filter(p => p?.sendIn == 'body'); + if (tokenBodyValues?.length) { + bru += `auth:oauth2:additional_params:access_token_req:body { +${indentString( + tokenBodyValues + .filter(item => item?.name?.length) + .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`) + .join('\n') + )} +} + +`; + } + const refreshHeaders = refreshParams?.filter(p => p?.sendIn == 'headers'); + if (refreshHeaders?.length) { + bru += `auth:oauth2:additional_params:refresh_token_req:headers { +${indentString( + refreshHeaders + .filter(item => item?.name?.length) + .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`) + .join('\n') + )} +} + +`; + } + const refreshQueryParams = refreshParams?.filter(p => p?.sendIn == 'queryparams'); + if (refreshQueryParams?.length) { + bru += `auth:oauth2:additional_params:refresh_token_req:queryparams { +${indentString( + refreshQueryParams + .filter(item => item?.name?.length) + .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`) + .join('\n') + )} +} + +`; + } + const refreshBodyValues = refreshParams?.filter(p => p?.sendIn == 'body'); + if (refreshBodyValues?.length) { + bru += `auth:oauth2:additional_params:refresh_token_req:body { +${indentString( + refreshBodyValues + .filter(item => item?.name?.length) + .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`) + .join('\n') + )} +} + +`; + } + } } let reqvars = _.get(vars, 'req'); diff --git a/packages/bruno-lang/v2/tests/oauth2-additional-params.spec.js b/packages/bruno-lang/v2/tests/oauth2-additional-params.spec.js new file mode 100644 index 000000000..c38ea7ef0 --- /dev/null +++ b/packages/bruno-lang/v2/tests/oauth2-additional-params.spec.js @@ -0,0 +1,329 @@ +const bruToJson = require('../src/bruToJson'); +const collectionBruToJson = require('../src/collectionBruToJson'); + +describe('OAuth2 Additional Parameters - request level', () => { + it('should parse all oauth2 additional parameters config types together', () => { + const input = ` +meta { + name: OAuth2 Additional Params Test + type: http +} + +get { + url: https://api.usebruno.com/protected +} + +auth:oauth2 { + grant_type: authorization_code + client_id: bruno-client-id + client_secret: bruno-client-secret + authorization_url: https://auth.usebruno.com/oauth/authorize + access_token_url: https://auth.usebruno.com/oauth/token +} + +auth:oauth2:additional_params:auth_req:headers { + auth-header: auth-header-value + ~disabled-auth-header: disabled-auth-header-value +} + +auth:oauth2:additional_params:auth_req:queryparams { + auth-query-param: auth-query-param-value + ~disabled-auth-query-param: disabled-auth-query-param-value +} + +auth:oauth2:additional_params:access_token_req:headers { + token-header: token-header-value + ~disabled-token-header: disabled-token-header-value +} + +auth:oauth2:additional_params:access_token_req:queryparams { + token-query-param: token-query-param-value + ~disabled-token-query-param: disabled-token-query-param-value +} + +auth:oauth2:additional_params:access_token_req:body { + token-body: token-body-value + ~disabled-token-body: disabled-token-body-value +} + +auth:oauth2:additional_params:refresh_token_req:headers { + refresh-header: refresh-header-value + ~disabled-refresh-header: disabled-refresh-header-value +} + +auth:oauth2:additional_params:refresh_token_req:queryparams { + refresh-query-param: refresh-query-param-value + ~disabled-refresh-query-param: disabled-refresh-query-param-value +} + +auth:oauth2:additional_params:refresh_token_req:body { + refresh-body: refresh-body-value + ~disabled-refresh-body: disabled-refresh-body-value +} + `.trim(); + + const result = bruToJson(input); + + // Verify all config types are present + expect(result).toHaveProperty('oauth2_additional_parameters_auth_req_headers'); + expect(result).toHaveProperty('oauth2_additional_parameters_auth_req_queryparams'); + expect(result).toHaveProperty('oauth2_additional_parameters_access_token_req_headers'); + expect(result).toHaveProperty('oauth2_additional_parameters_access_token_req_queryparams'); + expect(result).toHaveProperty('oauth2_additional_parameters_access_token_req_bodyvalues'); + expect(result).toHaveProperty('oauth2_additional_parameters_refresh_token_req_headers'); + expect(result).toHaveProperty('oauth2_additional_parameters_refresh_token_req_queryparams'); + expect(result).toHaveProperty('oauth2_additional_parameters_refresh_token_req_bodyvalues'); + + // Verify each has exactly one parameter + expect(result.oauth2_additional_parameters_auth_req_headers).toHaveLength(2); + expect(result.oauth2_additional_parameters_auth_req_queryparams).toHaveLength(2); + expect(result.oauth2_additional_parameters_access_token_req_headers).toHaveLength(2); + expect(result.oauth2_additional_parameters_access_token_req_queryparams).toHaveLength(2); + expect(result.oauth2_additional_parameters_access_token_req_bodyvalues).toHaveLength(2); + expect(result.oauth2_additional_parameters_refresh_token_req_headers).toHaveLength(2); + expect(result.oauth2_additional_parameters_refresh_token_req_queryparams).toHaveLength(2); + expect(result.oauth2_additional_parameters_refresh_token_req_bodyvalues).toHaveLength(2); + + // Verify parameter values + expect(result.oauth2_additional_parameters_auth_req_headers).toEqual([{ + name: 'auth-header', + value: 'auth-header-value', + enabled: true + }, { + name: 'disabled-auth-header', + value: 'disabled-auth-header-value', + enabled: false + }]); + + expect(result.oauth2_additional_parameters_auth_req_queryparams).toEqual([{ + name: 'auth-query-param', + value: 'auth-query-param-value', + enabled: true + }, { + name: 'disabled-auth-query-param', + value: 'disabled-auth-query-param-value', + enabled: false + }]); + + expect(result.oauth2_additional_parameters_access_token_req_headers).toEqual([{ + name: 'token-header', + value: 'token-header-value', + enabled: true + }, { + name: 'disabled-token-header', + value: 'disabled-token-header-value', + enabled: false + }]); + + expect(result.oauth2_additional_parameters_access_token_req_queryparams).toEqual([{ + name: 'token-query-param', + value: 'token-query-param-value', + enabled: true + }, { + name: 'disabled-token-query-param', + value: 'disabled-token-query-param-value', + enabled: false + }]); + + expect(result.oauth2_additional_parameters_access_token_req_bodyvalues).toEqual([{ + name: 'token-body', + value: 'token-body-value', + enabled: true + }, { + name: 'disabled-token-body', + value: 'disabled-token-body-value', + enabled: false + }]); + + expect(result.oauth2_additional_parameters_refresh_token_req_headers).toEqual([{ + name: 'refresh-header', + value: 'refresh-header-value', + enabled: true + }, { + name: 'disabled-refresh-header', + value: 'disabled-refresh-header-value', + enabled: false + }]); + + expect(result.oauth2_additional_parameters_refresh_token_req_queryparams).toEqual([{ + name: 'refresh-query-param', + value: 'refresh-query-param-value', + enabled: true + }, { + name: 'disabled-refresh-query-param', + value: 'disabled-refresh-query-param-value', + enabled: false + }]); + + expect(result.oauth2_additional_parameters_refresh_token_req_bodyvalues).toEqual([{ + name: 'refresh-body', + value: 'refresh-body-value', + enabled: true + }, { + name: 'disabled-refresh-body', + value: 'disabled-refresh-body-value', + enabled: false + }]); + }); +}); + +describe('OAuth2 Additional Parameters - collection/folder level', () => { + it('should parse all oauth2 additional parameters config types together', () => { + const input = ` +auth { + mode: oauth2 +} + +auth:oauth2 { + grant_type: authorization_code + client_id: bruno-client-id + client_secret: bruno-client-secret + authorization_url: https://auth.usebruno.com/oauth/authorize + access_token_url: https://auth.usebruno.com/oauth/token +} + +auth:oauth2:additional_params:auth_req:headers { + auth-header: auth-header-value + ~disabled-auth-header: disabled-auth-header-value +} + +auth:oauth2:additional_params:auth_req:queryparams { + auth-query-param: auth-query-param-value + ~disabled-auth-query-param: disabled-auth-query-param-value +} + +auth:oauth2:additional_params:access_token_req:headers { + token-header: token-header-value + ~disabled-token-header: disabled-token-header-value +} + +auth:oauth2:additional_params:access_token_req:queryparams { + token-query-param: token-query-param-value + ~disabled-token-query-param: disabled-token-query-param-value +} + +auth:oauth2:additional_params:access_token_req:body { + token-body: token-body-value + ~disabled-token-body: disabled-token-body-value +} + +auth:oauth2:additional_params:refresh_token_req:headers { + refresh-header: refresh-header-value + ~disabled-refresh-header: disabled-refresh-header-value +} + +auth:oauth2:additional_params:refresh_token_req:queryparams { + refresh-query-param: refresh-query-param-value + ~disabled-refresh-query-param: disabled-refresh-query-param-value +} + +auth:oauth2:additional_params:refresh_token_req:body { + refresh-body: refresh-body-value + ~disabled-refresh-body: disabled-refresh-body-value +} + `.trim(); + + const result = collectionBruToJson(input); + + // Verify all config types are present + expect(result).toHaveProperty('oauth2_additional_parameters_auth_req_headers'); + expect(result).toHaveProperty('oauth2_additional_parameters_auth_req_queryparams'); + expect(result).toHaveProperty('oauth2_additional_parameters_access_token_req_headers'); + expect(result).toHaveProperty('oauth2_additional_parameters_access_token_req_queryparams'); + expect(result).toHaveProperty('oauth2_additional_parameters_access_token_req_bodyvalues'); + expect(result).toHaveProperty('oauth2_additional_parameters_refresh_token_req_headers'); + expect(result).toHaveProperty('oauth2_additional_parameters_refresh_token_req_queryparams'); + expect(result).toHaveProperty('oauth2_additional_parameters_refresh_token_req_bodyvalues'); + + // Verify each has exactly one parameter + expect(result.oauth2_additional_parameters_auth_req_headers).toHaveLength(2); + expect(result.oauth2_additional_parameters_auth_req_queryparams).toHaveLength(2); + expect(result.oauth2_additional_parameters_access_token_req_headers).toHaveLength(2); + expect(result.oauth2_additional_parameters_access_token_req_queryparams).toHaveLength(2); + expect(result.oauth2_additional_parameters_access_token_req_bodyvalues).toHaveLength(2); + expect(result.oauth2_additional_parameters_refresh_token_req_headers).toHaveLength(2); + expect(result.oauth2_additional_parameters_refresh_token_req_queryparams).toHaveLength(2); + expect(result.oauth2_additional_parameters_refresh_token_req_bodyvalues).toHaveLength(2); + + // Verify parameter values + expect(result.oauth2_additional_parameters_auth_req_headers).toEqual([{ + name: 'auth-header', + value: 'auth-header-value', + enabled: true + }, { + name: 'disabled-auth-header', + value: 'disabled-auth-header-value', + enabled: false + }]); + + expect(result.oauth2_additional_parameters_auth_req_queryparams).toEqual([{ + name: 'auth-query-param', + value: 'auth-query-param-value', + enabled: true + }, { + name: 'disabled-auth-query-param', + value: 'disabled-auth-query-param-value', + enabled: false + }]); + + expect(result.oauth2_additional_parameters_access_token_req_headers).toEqual([{ + name: 'token-header', + value: 'token-header-value', + enabled: true + }, { + name: 'disabled-token-header', + value: 'disabled-token-header-value', + enabled: false + }]); + + expect(result.oauth2_additional_parameters_access_token_req_queryparams).toEqual([{ + name: 'token-query-param', + value: 'token-query-param-value', + enabled: true + }, { + name: 'disabled-token-query-param', + value: 'disabled-token-query-param-value', + enabled: false + }]); + + expect(result.oauth2_additional_parameters_access_token_req_bodyvalues).toEqual([{ + name: 'token-body', + value: 'token-body-value', + enabled: true + }, { + name: 'disabled-token-body', + value: 'disabled-token-body-value', + enabled: false + }]); + + expect(result.oauth2_additional_parameters_refresh_token_req_headers).toEqual([{ + name: 'refresh-header', + value: 'refresh-header-value', + enabled: true + }, { + name: 'disabled-refresh-header', + value: 'disabled-refresh-header-value', + enabled: false + }]); + + expect(result.oauth2_additional_parameters_refresh_token_req_queryparams).toEqual([{ + name: 'refresh-query-param', + value: 'refresh-query-param-value', + enabled: true + }, { + name: 'disabled-refresh-query-param', + value: 'disabled-refresh-query-param-value', + enabled: false + }]); + + expect(result.oauth2_additional_parameters_refresh_token_req_bodyvalues).toEqual([{ + name: 'refresh-body', + value: 'refresh-body-value', + enabled: true + }, { + name: 'disabled-refresh-body', + value: 'disabled-refresh-body-value', + enabled: false + }]); + }); +}); \ No newline at end of file diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index 6db0174b0..ff830ca99 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', 'implicit']) @@ -252,6 +274,15 @@ const oauth2Schema = Yup.object({ is: (val) => ['authorization_code', 'implicit'].includes(val), then: Yup.boolean().default(true), otherwise: Yup.boolean() + }), + additionalParameters: Yup.object({ + authorization: Yup.mixed().when('grantType', { + is: 'authorization_code', + then: Yup.array().of(oauth2AuthorizationAdditionalParametersSchema).required(), + otherwise: Yup.mixed().nullable().optional() + }), + token: Yup.array().of(oauth2AdditionalParametersSchema).optional(), + refresh: Yup.array().of(oauth2AdditionalParametersSchema).optional() }) }) .noUnknown(true)