Skip to content
Draft
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',
forwardAuthorizationOnRedirect: true
};

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, forwardAuthorizationOnRedirect } = 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({ forwardAuthorizationOnRedirect: !forwardAuthorizationOnRedirect }), [forwardAuthorizationOnRedirect, 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={forwardAuthorizationOnRedirect}
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, forwardAuthorizationOnRedirect: false };

dispatch(
newHttpRequest({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1401,7 +1401,8 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
}
},
settings: settings ?? {
encodeUrl: true
encodeUrl: true,
forwardAuthorizationOnRedirect: false
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,10 @@ export const collectionsSlice = createSlice({
content: null
}
},
settings: {
encodeUrl: true,
forwardAuthorizationOnRedirect: false
},
draft: null
};
item.draft = cloneDeep(item);
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 forwardAuthorizationOnRedirect setting, default to true for backward compatibility
const forwardAuthorizationOnRedirect = request.settings?.forwardAuthorizationOnRedirect ?? 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,
forwardAuthorizationOnRedirect: forwardAuthorizationOnRedirect,
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,
forwardAuthorizationOnRedirect = true,
proxyMode,
proxyConfig,
systemProxyConfig,
Expand Down Expand Up @@ -179,6 +190,17 @@ function makeAxiosInstance({

const requestConfig = createRedirectConfig(error, redirectUrl);

if (!forwardAuthorizationOnRedirect) {
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 forwardAuthorizationOnRedirect is false', async () => {
const stubAdapter = createRedirectingStubAdapter('https://other-domain.com/target');
const instance = makeAxiosInstance({
followRedirects: true,
forwardAuthorizationOnRedirect: 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');
});

it('should preserve Authorization and Proxy-Authorization headers on cross-origin redirect when forwardAuthorizationOnRedirect is true', async () => {
const stubAdapter = createRedirectingStubAdapter('https://other-domain.com/target');
const instance = makeAxiosInstance({
followRedirects: true,
forwardAuthorizationOnRedirect: 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 forwardAuthorizationOnRedirect is false', async () => {
const stubAdapter = createRedirectingStubAdapter('https://api.example.com/target');
const instance = makeAxiosInstance({
followRedirects: true,
forwardAuthorizationOnRedirect: 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 forwardAuthorizationOnRedirect is false', async () => {
const stubAdapter = createRedirectingStubAdapter('/relative-target');
const instance = makeAxiosInstance({
followRedirects: true,
forwardAuthorizationOnRedirect: 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 @@ -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.forwardAuthorizationOnRedirect !== undefined) {
(brunoItem.settings as Record<string, unknown>).forwardAuthorizationOnRedirect = settings.forwardAuthorizationOnRedirect;
}
}

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,
forwardAuthorizationOnRedirect: typeof brunoSettings.forwardAuthorizationOnRedirect === 'boolean' ? brunoSettings.forwardAuthorizationOnRedirect : false
};
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,
forwardAuthorizationOnRedirect: typeof ocRequest.settings.forwardAuthorizationOnRedirect === 'boolean' ? ocRequest.settings.forwardAuthorizationOnRedirect : false
};
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,
forwardAuthorizationOnRedirect: typeof brunoSettings?.forwardAuthorizationOnRedirect === 'boolean' ? brunoSettings.forwardAuthorizationOnRedirect : false
};
ocRequest.settings = settings;

Expand Down
12 changes: 12 additions & 0 deletions packages/bruno-converters/src/opencollection/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,16 @@ export interface BrunoCollectionRoot {
name?: string;
seq?: number;
};
}
// FIX ME (draft) - need to update this in @opencollection/types package
declare module '@opencollection/types/requests/graphql' {
export interface GraphQLRequestSettings {
forwardAuthorizationOnRedirect?: boolean | 'inherit';
}
}

declare module '@opencollection/types/requests/http' {
export interface HttpRequestSettings {
forwardAuthorizationOnRedirect?: boolean | 'inherit';
}
}
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,
forwardAuthorizationOnRedirect: false
};

// Handle followRedirects setting
Expand Down
Loading