mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-15 20:01:28 +00:00
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:
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user