Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions packages/bruno-app/src/components/RequestPane/Settings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ const DEFAULT_SETTINGS = {
encodeUrl: false,
followRedirects: true,
maxRedirects: 5,
timeout: 'inherit'
timeout: 'inherit',
forwardAuthorizationHeader: true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we please change the name to something like forwardAuthorizationOnRedirect or shouldForwardAuthorizationHeader?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Earlier it was named forwardAuthorizationOnRedirect but after discussing with Bijin and Anoop, forwardAuthorizationHeader was finalised yesterday.

};

const Settings = ({ item, collection }) => {
Expand All @@ -26,7 +27,7 @@ const Settings = ({ item, collection }) => {

const rawSettings = getPropertyFromDraftOrRequest('settings');
const settings = { ...DEFAULT_SETTINGS, ...rawSettings };
const { encodeUrl, followRedirects, maxRedirects, timeout } = settings;
const { encodeUrl, followRedirects, maxRedirects, timeout, forwardAuthorizationHeader } = settings;

// Reusable function to update settings
const updateSetting = useCallback((settingUpdate) => {
Expand All @@ -45,6 +46,9 @@ const Settings = ({ item, collection }) => {
const onToggleFollowRedirects = useCallback(() =>
updateSetting({ followRedirects: !followRedirects }), [followRedirects, updateSetting]);

const onToggleForwardAuthorizationOnRedirect = useCallback(() =>
updateSetting({ forwardAuthorizationHeader: !forwardAuthorizationHeader }), [forwardAuthorizationHeader, updateSetting]);

const onMaxRedirectsChange = useCallback((e) => {
const value = e.target.value;
// Only allow empty string or digits
Expand Down Expand Up @@ -107,7 +111,7 @@ const Settings = ({ item, collection }) => {
<Tags item={item} collection={collection} />
</div>

<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4 pr-1">

<div className="flex flex-col gap-4">
<ToggleSelector
Expand All @@ -131,6 +135,17 @@ const Settings = ({ item, collection }) => {
/>
</div>

<div className="flex flex-col gap-4">
<ToggleSelector
checked={forwardAuthorizationHeader}
onChange={onToggleForwardAuthorizationOnRedirect}
label="Forward Authorization on Redirect"
description="Send Authorization and Proxy-Authorization headers when a redirect points to a different origin"
size="medium"
data-testid="forward-authorization-toggle"
/>
</div>

<SettingsInput
id="maxRedirects"
label="Max Redirects"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
} else if (values.requestType === 'from-curl') {
const request = getRequestFromCurlCommand(values.curlCommand, curlRequestTypeDetected);
const settings = { encodeUrl: false };
const settings = { encodeUrl: false, forwardAuthorizationHeader: false };

dispatch(
newHttpRequest({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { collectionSchema, environmentSchema, itemSchema } from '@usebruno/schema';
import { parseQueryParams, extractPromptVariables, getDataTypeFromValue } from '@usebruno/common/utils';
import { REQUEST_TYPES, DEFAULT_COLLECTION_FORMAT } from 'utils/common/constants';
import { REQUEST_TYPES, DEFAULT_COLLECTION_FORMAT, DEFAULT_HTTP_ITEM_SETTINGS } from 'utils/common/constants';
import cloneDeep from 'lodash/cloneDeep';
import filter from 'lodash/filter';
import find from 'lodash/find';
Expand Down Expand Up @@ -1400,9 +1400,7 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
mode: 'inherit'
}
},
settings: settings ?? {
encodeUrl: true
}
settings: settings ?? cloneDeep(DEFAULT_HTTP_ITEM_SETTINGS)
};

// itemUid is null when we are creating a new request at the root level
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import toast from 'react-hot-toast';
import mime from 'mime-types';
import path from 'utils/common/path';
import { getUniqueTagsFromItems } from 'utils/collections/index';
import { DEFAULT_HTTP_ITEM_SETTINGS } from 'utils/common/constants';
import { getCollectionEnvironmentPath } from 'utils/snapshot';
import { getDataTypeFromValue } from '@usebruno/common/utils';
import * as exampleReducers from './exampleReducers';
Expand Down Expand Up @@ -963,6 +964,7 @@ export const collectionsSlice = createSlice({
content: null
}
},
settings: cloneDeep(DEFAULT_HTTP_ITEM_SETTINGS),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need cloneDeep here? DEFAULT_HTTP_ITEM_SETTINGS is lain flat object with only primitive values.

draft: null
};
item.draft = cloneDeep(item);
Expand Down
5 changes: 5 additions & 0 deletions packages/bruno-app/src/utils/common/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export const PRESET_REQUEST_TYPES = {

export const DEFAULT_PRESET_REQUEST_TYPE = PRESET_REQUEST_TYPES.HTTP;

export const DEFAULT_HTTP_ITEM_SETTINGS = {
encodeUrl: true,
forwardAuthorizationHeader: false
};

export const AUTH_MODES = {
AWSV4: 'awsv4',
BASIC: 'basic',
Expand Down
4 changes: 4 additions & 0 deletions packages/bruno-cli/src/runner/run-single-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,9 @@ const runSingleRequest = async function (
// Get followRedirects setting, default to true for backward compatibility
const followRedirects = request.settings?.followRedirects ?? true;

// Get forwardAuthorizationHeader setting, default to true for backward compatibility
const forwardAuthorizationHeader = request.settings?.forwardAuthorizationHeader ?? true;

// Get maxRedirects from request settings, fallback to request.maxRedirects, then default to 5
let requestMaxRedirects = request.settings?.maxRedirects ?? request.maxRedirects ?? 5;

Expand Down Expand Up @@ -620,6 +623,7 @@ const runSingleRequest = async function (
requestMaxRedirects: requestMaxRedirects,
disableCookies: options.disableCookies,
followRedirects: followRedirects,
forwardAuthorizationHeader: forwardAuthorizationHeader,
proxyMode,
proxyConfig,
systemProxyConfig: cachedSystemProxy,
Expand Down
22 changes: 22 additions & 0 deletions packages/bruno-cli/src/utils/axios-instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ const createRedirectConfig = (error, redirectUrl) => {
return requestConfig;
};

const isSameOrigin = (url1, url2) => {
try {
const parsed1 = new global.URL(url1);
const parsed2 = new global.URL(url2);
return parsed1.origin === parsed2.origin;
} catch (err) {
return false;
}
};

/**
* Function that configures axios with timing interceptors
* Important to note here that the timings are not completely accurate.
Expand All @@ -76,6 +86,7 @@ function makeAxiosInstance({
requestMaxRedirects = 5,
disableCookies,
followRedirects = true,
forwardAuthorizationHeader = true,
proxyMode,
proxyConfig,
systemProxyConfig,
Expand Down Expand Up @@ -179,6 +190,17 @@ function makeAxiosInstance({

const requestConfig = createRedirectConfig(error, redirectUrl);

if (!forwardAuthorizationHeader) {
if (!isSameOrigin(error.config.url, redirectUrl)) {
Object.keys(requestConfig.headers).forEach((key) => {
const lowerKey = key.toLowerCase();
if (lowerKey === 'authorization' || lowerKey === 'proxy-authorization') {
delete requestConfig.headers[key];
}
});
}
}

await setupProxyAgents({
requestConfig,
proxyMode,
Expand Down
134 changes: 134 additions & 0 deletions packages/bruno-cli/tests/utils/axios-instance.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,138 @@ describe('makeAxiosInstance', () => {

expect(stubAdapter.getConfig().headers['User-Agent']).toMatch(/^bruno-runtime\//);
});

describe('cross-origin redirects authorization stripping', () => {
function createRedirectingStubAdapter(redirectUrl, redirectStatus = 302) {
const calls = [];
const adapter = (config) => {
calls.push(config);
if (calls.length === 1) {
const err = new Error('Redirect ' + redirectStatus);
err.config = config;
err.response = {
status: redirectStatus,
statusText: 'Found',
headers: {
location: redirectUrl
},
data: {}
};
return Promise.reject(err);
}
return Promise.resolve({
data: { success: true },
status: 200,
statusText: 'OK',
headers: {},
config
});
};
adapter.getCalls = () => calls;
return adapter;
}

it('should strip Authorization and Proxy-Authorization headers on cross-origin redirect when forwardAuthorizationHeader is false', async () => {
const stubAdapter = createRedirectingStubAdapter('https://other-domain.com/target');
const instance = makeAxiosInstance({
followRedirects: true,
forwardAuthorizationHeader: false
});

await instance({
url: 'https://api.example.com/start',
method: 'get',
headers: {
'Authorization': 'Bearer my-token',
'Proxy-Authorization': 'Bearer proxy-token',
'Custom-Header': 'keep-me'
},
adapter: stubAdapter
});

const calls = stubAdapter.getCalls();
expect(calls.length).toBe(2);

// First call should have headers
expect(calls[0].headers['Authorization']).toBe('Bearer my-token');
expect(calls[0].headers['Proxy-Authorization']).toBe('Bearer proxy-token');
expect(calls[0].headers['Custom-Header']).toBe('keep-me');

// Redirected call should strip auth headers but keep custom headers
expect(calls[1].headers['Authorization']).toBeUndefined();
expect(calls[1].headers['Proxy-Authorization']).toBeUndefined();
expect(calls[1].headers['Custom-Header']).toBe('keep-me');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also add expect(calls[1].url).toBe('https://other-domain.com/target');
This ensures the second request was made to the redirected URL, not just with modified headers.
We can also add the same assertions to other tests.

});

it('should preserve Authorization and Proxy-Authorization headers on cross-origin redirect when forwardAuthorizationHeader is true', async () => {
const stubAdapter = createRedirectingStubAdapter('https://other-domain.com/target');
const instance = makeAxiosInstance({
followRedirects: true,
forwardAuthorizationHeader: true
});

await instance({
url: 'https://api.example.com/start',
method: 'get',
headers: {
'authorization': 'Bearer my-token',
'proxy-authorization': 'Bearer proxy-token',
'Custom-Header': 'keep-me'
},
adapter: stubAdapter
});

const calls = stubAdapter.getCalls();
expect(calls.length).toBe(2);
expect(calls[1].headers['authorization']).toBe('Bearer my-token');
expect(calls[1].headers['proxy-authorization']).toBe('Bearer proxy-token');
expect(calls[1].headers['Custom-Header']).toBe('keep-me');
});

it('should preserve Authorization and Proxy-Authorization headers on same-origin redirect even if forwardAuthorizationHeader is false', async () => {
const stubAdapter = createRedirectingStubAdapter('https://api.example.com/target');
const instance = makeAxiosInstance({
followRedirects: true,
forwardAuthorizationHeader: false
});

await instance({
url: 'https://api.example.com/start',
method: 'get',
headers: {
'Authorization': 'Bearer my-token',
'Proxy-Authorization': 'Bearer proxy-token'
},
adapter: stubAdapter
});

const calls = stubAdapter.getCalls();
expect(calls.length).toBe(2);
expect(calls[1].headers['Authorization']).toBe('Bearer my-token');
expect(calls[1].headers['Proxy-Authorization']).toBe('Bearer proxy-token');
});

it('should preserve Authorization and Proxy-Authorization headers on relative redirect even if forwardAuthorizationHeader is false', async () => {
const stubAdapter = createRedirectingStubAdapter('/relative-target');
const instance = makeAxiosInstance({
followRedirects: true,
forwardAuthorizationHeader: false
});

await instance({
url: 'https://api.example.com/start',
method: 'get',
headers: {
'Authorization': 'Bearer my-token',
'Proxy-Authorization': 'Bearer proxy-token'
},
adapter: stubAdapter
});

const calls = stubAdapter.getCalls();
expect(calls.length).toBe(2);
expect(calls[1].headers['Authorization']).toBe('Bearer my-token');
expect(calls[1].headers['Proxy-Authorization']).toBe('Bearer proxy-token');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@ const transformInsomniaRequestItem = (request, index, allRequests) => {
}

const settings = {
encodeUrl: request.settings?.encodeUrl !== false && request.settingEncodeUrl !== false // handles v4 and v5 import
encodeUrl: request.settings?.encodeUrl !== false && request.settingEncodeUrl !== false, // handles v4 and v5 import
forwardAuthorizationHeader: false
};

brunoRequestItem.settings = settings;
Expand Down
3 changes: 3 additions & 0 deletions packages/bruno-converters/src/openapi/openapi-to-bruno.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ const transformOpenapiRequestItem = (request, usedNames = new Set(), options = {
uid: uuid(),
name: operationName,
type: 'http-request',
settings: {
forwardAuthorizationHeader: false
},
tags: sanitizeTags(request.operationObject.tags || [], options),
request: {
docs: _operationObject.description,
Expand Down
3 changes: 3 additions & 0 deletions packages/bruno-converters/src/openapi/swagger2-to-bruno.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ const transformSwaggerRequestItem = (request, usedNames = new Set(), options = {
uid: uuid(),
name: operationName,
type: 'http-request',
settings: {
forwardAuthorizationHeader: false
},
tags: sanitizeTags(op.tags || [], options),
request: {
docs: op.description,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ export const fromOpenCollectionGraphqlItem = (item: GraphQLRequest): BrunoItem =
if (settings.maxRedirects !== undefined) {
(brunoItem.settings as Record<string, unknown>).maxRedirects = settings.maxRedirects;
}
if (settings.forwardAuthorizationHeader !== undefined) {
(brunoItem.settings as Record<string, unknown>).forwardAuthorizationHeader = settings.forwardAuthorizationHeader;
}
}

if (info.tags?.length) {
Expand Down Expand Up @@ -181,7 +184,8 @@ export const toOpenCollectionGraphqlItem = (item: BrunoItem): GraphQLRequest =>
encodeUrl: typeof brunoSettings.encodeUrl === 'boolean' ? brunoSettings.encodeUrl : true,
timeout: typeof brunoSettings.timeout === 'number' ? brunoSettings.timeout : 0,
followRedirects: typeof brunoSettings.followRedirects === 'boolean' ? brunoSettings.followRedirects : true,
maxRedirects: typeof brunoSettings.maxRedirects === 'number' ? brunoSettings.maxRedirects : 5
maxRedirects: typeof brunoSettings.maxRedirects === 'number' ? brunoSettings.maxRedirects : 5,
forwardAuthorizationHeader: typeof brunoSettings.forwardAuthorizationHeader === 'boolean' ? brunoSettings.forwardAuthorizationHeader : true
};
ocRequest.settings = settings;

Expand Down
6 changes: 4 additions & 2 deletions packages/bruno-converters/src/opencollection/items/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ export const fromOpenCollectionHttpItem = (ocRequest: HttpRequest): BrunoItem =>
encodeUrl: typeof ocRequest.settings.encodeUrl === 'boolean' ? ocRequest.settings.encodeUrl : true,
timeout: typeof ocRequest.settings.timeout === 'number' ? ocRequest.settings.timeout : 0,
followRedirects: typeof ocRequest.settings.followRedirects === 'boolean' ? ocRequest.settings.followRedirects : true,
maxRedirects: typeof ocRequest.settings.maxRedirects === 'number' ? ocRequest.settings.maxRedirects : 5
maxRedirects: typeof ocRequest.settings.maxRedirects === 'number' ? ocRequest.settings.maxRedirects : 5,
forwardAuthorizationHeader: typeof ocRequest.settings.forwardAuthorizationHeader === 'boolean' ? ocRequest.settings.forwardAuthorizationHeader : true
};
brunoItem.settings = settings;
}
Expand Down Expand Up @@ -223,7 +224,8 @@ export const toOpenCollectionHttpItem = (item: BrunoItem): HttpRequest => {
encodeUrl: typeof brunoSettings?.encodeUrl === 'boolean' ? brunoSettings.encodeUrl : true,
timeout: typeof brunoSettings?.timeout === 'number' ? brunoSettings.timeout : 0,
followRedirects: typeof brunoSettings?.followRedirects === 'boolean' ? brunoSettings.followRedirects : true,
maxRedirects: typeof brunoSettings?.maxRedirects === 'number' ? brunoSettings.maxRedirects : 5
maxRedirects: typeof brunoSettings?.maxRedirects === 'number' ? brunoSettings.maxRedirects : 5,
forwardAuthorizationHeader: typeof brunoSettings?.forwardAuthorizationHeader === 'boolean' ? brunoSettings.forwardAuthorizationHeader : true
};
ocRequest.settings = settings;

Expand Down
3 changes: 2 additions & 1 deletion packages/bruno-converters/src/postman/postman-to-bruno.js
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,8 @@ const importPostmanV2CollectionItem = (brunoParent, item, { useWorkers = false }
};

const settings = {
encodeUrl: i.protocolProfileBehavior?.disableUrlEncoding !== true
encodeUrl: i.protocolProfileBehavior?.disableUrlEncoding !== true,
forwardAuthorizationHeader: false
};

// Handle followRedirects setting
Expand Down
Loading