feat: enhance OAuth2 support in snippet generation (#6592)

* feat: enhance OAuth2 support in snippet generation

* Updated getAuthHeaders function to handle OAuth2 authentication, including token retrieval and placement.
* Added tests for OAuth2 scenarios, ensuring correct Authorization header generation and handling of edge cases.
* Improved error handling for access token retrieval from stored credentials.

* refactor: standardize comparison operators in getAuthHeaders function

* Updated comparison operators in the getAuthHeaders function to use strict equality (===) for improved consistency and reliability in credential checks.

* fix: correct block structure in OAuth2 case of getAuthHeaders function

* Added missing block structure for the 'oauth2' case in the getAuthHeaders function to ensure proper execution flow and maintain code clarity.

* feat: enhance OAuth2 credential retrieval in getAuthHeaders function

* Updated getAuthHeaders function to support retrieval of stored OAuth2 credentials based on collection and item context.
* Improved access token handling by checking for existing credentials before falling back to default values.
* Enhanced test coverage for OAuth2 scenarios to ensure accurate token management and error handling.

* fix: preserve tokenHeaderPrefix value in OAuth2 configuration

* Updated snippet-generator.spec.js to ensure that the tokenHeaderPrefix from OAuth2 configuration is preserved, allowing for empty string scenarios.
* Default to 'Bearer' only if the tokenHeaderPrefix is undefined, enhancing flexibility in token management.

* fix: ensure consistent formatting of authorization header in OAuth2 handling

* Updated getAuthHeaders function to always trim the final result of the authorization header for consistent formatting.
* Adjusted snippet-generator.spec.js to reflect the same trimming logic for the access token, enhancing test reliability.

* fix: clarify token placement handling in getAuthHeaders function

* Updated comments in the getAuthHeaders function to specify that when tokenPlacement is 'url', no auth headers are added, and that token placement in the URL/query params must be managed separately.

* fix: ensure safe handling of OAuth2 credentials in getAuthHeaders function

* Updated getAuthHeaders function to default to an empty array when accessing oauth2Credentials, preventing potential errors when no credentials are available.
This commit is contained in:
Yash
2026-01-21 12:23:05 +05:30
committed by GitHub
parent 967b073ded
commit 0bf169562b
3 changed files with 291 additions and 2 deletions

View File

@@ -20,7 +20,7 @@ const generateSnippet = ({ language, item, collection, shouldInterpolate = false
// Add auth headers if needed
if (request.auth && request.auth.mode !== 'none') {
const collectionAuth = collection?.draft?.root ? get(collection, 'draft.root.request.auth', null) : get(collection, 'root.request.auth', null);
const authHeaders = getAuthHeaders(collectionAuth, request.auth);
const authHeaders = getAuthHeaders(collectionAuth, request.auth, collection, item);
headers = [...headers, ...authHeaders];
}

View File

@@ -554,3 +554,223 @@ describe('generateSnippet with edge-case bodies', () => {
expect(result).toMatch(/^curl -X POST/);
});
});
describe('generateSnippet with OAuth2 authentication', () => {
const language = { target: 'shell', client: 'curl' };
const baseCollection = { root: { request: { auth: { mode: 'none' }, headers: [] } } };
beforeEach(() => {
jest.clearAllMocks();
// Mock getAuthHeaders to return OAuth2 headers based on the auth config
const authUtils = require('utils/codegenerator/auth');
authUtils.getAuthHeaders.mockImplementation((collectionRootAuth, requestAuth, collection = null, item = null) => {
if (requestAuth?.mode === 'oauth2') {
const oauth2Config = requestAuth.oauth2 || {};
const tokenPlacement = oauth2Config.tokenPlacement || 'header';
// Use the actual value from config, defaulting to 'Bearer' only if undefined
// Empty string should be preserved to test no-prefix scenarios
const tokenHeaderPrefix = oauth2Config.tokenHeaderPrefix !== undefined
? oauth2Config.tokenHeaderPrefix
: 'Bearer';
let accessToken = oauth2Config.accessToken || '<access_token>';
// If collection and item are provided, try to look up stored credentials
if (collection && item && collection.oauth2Credentials) {
const grantType = oauth2Config.grantType || '';
const urlToLookup = grantType === 'implicit'
? oauth2Config.authorizationUrl || ''
: oauth2Config.accessTokenUrl || '';
const credentialsId = oauth2Config.credentialsId || 'credentials';
const collectionUid = collection.uid;
if (urlToLookup && collectionUid) {
// Look up stored credentials (simplified - assumes URL is already interpolated in test data)
const credentialsData = collection.oauth2Credentials.find(
(creds) =>
creds?.url === urlToLookup
&& creds?.collectionUid === collectionUid
&& creds?.credentialsId === credentialsId
);
if (credentialsData?.credentials?.access_token) {
accessToken = credentialsData.credentials.access_token;
}
}
}
if (tokenPlacement === 'header') {
// Always trim the final result for consistent formatting
const headerValue = tokenHeaderPrefix
? `${tokenHeaderPrefix} ${accessToken}`.trim()
: accessToken.trim();
return [
{
enabled: true,
name: 'Authorization',
value: headerValue
}
];
}
}
return [];
});
});
it('should include OAuth2 Bearer token in Authorization header when tokenPlacement is header', () => {
const item = {
uid: 'oauth-req',
request: {
method: 'GET',
url: 'https://api.example.com/users',
headers: [],
auth: {
mode: 'oauth2',
oauth2: {
grantType: 'client_credentials',
tokenPlacement: 'header',
tokenHeaderPrefix: 'Bearer',
accessToken: 'test-access-token-123'
}
}
}
};
generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });
const harUtils = require('utils/codegenerator/har');
const harCall = harUtils.buildHarRequest.mock.calls[0][0];
expect(harCall.headers).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: 'Authorization',
value: 'Bearer test-access-token-123'
})
])
);
});
it('should use custom tokenHeaderPrefix when provided', () => {
const item = {
uid: 'oauth-req-custom',
request: {
method: 'GET',
url: 'https://api.example.com/users',
headers: [],
auth: {
mode: 'oauth2',
oauth2: {
grantType: 'client_credentials',
tokenPlacement: 'header',
tokenHeaderPrefix: 'OAuth',
accessToken: 'custom-token-456'
}
}
}
};
generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });
const harUtils = require('utils/codegenerator/har');
const harCall = harUtils.buildHarRequest.mock.calls[0][0];
expect(harCall.headers).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: 'Authorization',
value: 'OAuth custom-token-456'
})
])
);
});
it('should not include Authorization header when tokenPlacement is url', () => {
const item = {
uid: 'oauth-req-url',
request: {
method: 'GET',
url: 'https://api.example.com/users',
headers: [],
auth: {
mode: 'oauth2',
oauth2: {
grantType: 'client_credentials',
tokenPlacement: 'url',
tokenQueryKey: 'access_token',
accessToken: 'token-in-url'
}
}
}
};
generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });
const harUtils = require('utils/codegenerator/har');
const harCall = harUtils.buildHarRequest.mock.calls[0][0];
const authHeader = harCall.headers.find((h) => h.name === 'Authorization');
expect(authHeader).toBeUndefined();
});
it('should use placeholder when accessToken is not available', () => {
const item = {
uid: 'oauth-req-placeholder',
request: {
method: 'GET',
url: 'https://api.example.com/users',
headers: [],
auth: {
mode: 'oauth2',
oauth2: {
grantType: 'client_credentials',
tokenPlacement: 'header',
tokenHeaderPrefix: 'Bearer'
}
}
}
};
generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });
const harUtils = require('utils/codegenerator/har');
const harCall = harUtils.buildHarRequest.mock.calls[0][0];
expect(harCall.headers).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: 'Authorization',
value: 'Bearer <access_token>'
})
])
);
});
it('should handle empty tokenHeaderPrefix', () => {
const item = {
uid: 'oauth-req-no-prefix',
request: {
method: 'GET',
url: 'https://api.example.com/users',
headers: [],
auth: {
mode: 'oauth2',
oauth2: {
grantType: 'client_credentials',
tokenPlacement: 'header',
tokenHeaderPrefix: '',
accessToken: 'token-without-prefix'
}
}
}
};
generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });
const harUtils = require('utils/codegenerator/har');
const harCall = harUtils.buildHarRequest.mock.calls[0][0];
expect(harCall.headers).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: 'Authorization',
value: 'token-without-prefix'
})
])
);
});
});

View File

@@ -1,6 +1,9 @@
import get from 'lodash/get';
import { find } from 'lodash';
import { interpolate } from '@usebruno/common';
import { getAllVariables } from 'utils/collections/index';
export const getAuthHeaders = (collectionRootAuth, requestAuth) => {
export const getAuthHeaders = (collectionRootAuth, requestAuth, collection = null, item = null) => {
// Discovered edge case where code generation fails when you create a collection which has not been saved yet:
// Collection auth therefore null, and request inherits from collection, therefore it is also null
// TypeError: Cannot read properties of undefined (reading 'mode')
@@ -48,6 +51,72 @@ export const getAuthHeaders = (collectionRootAuth, requestAuth) => {
];
}
return [];
case 'oauth2': {
const oauth2Config = get(auth, 'oauth2', {});
const tokenPlacement = get(oauth2Config, 'tokenPlacement', 'header');
const tokenHeaderPrefix = get(oauth2Config, 'tokenHeaderPrefix', 'Bearer');
// Only add header if token placement is 'header'
if (tokenPlacement === 'header') {
// Try to get access token from persisted credentials
let accessToken = '<access_token>';
if (collection && item) {
try {
const grantType = get(oauth2Config, 'grantType', '');
// For implicit grant type, use authorizationUrl; for others, use accessTokenUrl
const urlToLookup = grantType === 'implicit'
? get(oauth2Config, 'authorizationUrl', '')
: get(oauth2Config, 'accessTokenUrl', '');
const credentialsId = get(oauth2Config, 'credentialsId', 'credentials');
const collectionUid = get(collection, 'uid');
if (urlToLookup && collectionUid) {
// Interpolate the URL with variables
const variables = getAllVariables(collection, item);
const interpolatedUrl = interpolate(urlToLookup, variables);
// Look up stored credentials
const credentialsData = find(
collection?.oauth2Credentials || [],
(creds) =>
creds?.url === interpolatedUrl
&& creds?.collectionUid === collectionUid
&& creds?.credentialsId === credentialsId
);
if (credentialsData?.credentials?.access_token) {
accessToken = credentialsData.credentials.access_token;
}
}
} catch (error) {
console.error('Error retrieving OAuth2 access token:', error);
// Fall back to placeholder if lookup fails
}
}
// Build the authorization header value
// If tokenHeaderPrefix is empty, just use the token
// Otherwise, use the format: "prefix token"
// Always trim the final result for consistent formatting
const headerValue = (
tokenHeaderPrefix
? `${tokenHeaderPrefix} ${accessToken}`
: accessToken
).trim();
return [
{
enabled: true,
name: 'Authorization',
value: headerValue
}
];
}
// If tokenPlacement is 'url', this function does not add any auth headers;
// token placement in the URL/query params must be handled elsewhere.
return [];
}
default:
return [];
}