feat: add support for oauth2 in cli (#4578)

Co-authored-by: Pooja Belaramani <109731557+poojabela@users.noreply.github.com>
This commit is contained in:
Pooja
2025-05-12 21:37:42 +05:30
committed by GitHub
parent 2171d491a6
commit f58477931f
13 changed files with 456 additions and 9 deletions

View File

@@ -156,6 +156,37 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
delete request.basicAuth;
}
if (request?.oauth2?.grantType) {
switch (request.oauth2.grantType) {
case 'password':
request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || '';
request.oauth2.refreshTokenUrl = _interpolate(request.oauth2.refreshTokenUrl) || '';
request.oauth2.username = _interpolate(request.oauth2.username) || '';
request.oauth2.password = _interpolate(request.oauth2.password) || '';
request.oauth2.clientId = _interpolate(request.oauth2.clientId) || '';
request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || '';
request.oauth2.scope = _interpolate(request.oauth2.scope) || '';
request.oauth2.credentialsPlacement = _interpolate(request.oauth2.credentialsPlacement) || '';
request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || '';
request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || '';
request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || '';
break;
case 'client_credentials':
request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || '';
request.oauth2.refreshTokenUrl = _interpolate(request.oauth2.refreshTokenUrl) || '';
request.oauth2.clientId = _interpolate(request.oauth2.clientId) || '';
request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || '';
request.oauth2.scope = _interpolate(request.oauth2.scope) || '';
request.oauth2.credentialsPlacement = _interpolate(request.oauth2.credentialsPlacement) || '';
request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || '';
request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || '';
request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || '';
break;
default:
break;
}
}
if (request.awsv4config) {
request.awsv4config.accessKeyId = _interpolate(request.awsv4config.accessKeyId) || '';
request.awsv4config.secretAccessKey = _interpolate(request.awsv4config.secretAccessKey) || '';

View File

@@ -0,0 +1,6 @@
const { getOAuth2Token } = require('@usebruno/requests');
const tokenStore = require('./tokenStore');
module.exports = {
getOAuth2Token: (oauth2Config) => getOAuth2Token(oauth2Config, tokenStore)
};

View File

@@ -1,7 +1,7 @@
const { get, each, filter } = require('lodash');
const decomment = require('decomment');
const crypto = require('node:crypto');
const { mergeHeaders, mergeScripts, mergeVars, getTreePathFromCollectionToItem } = require('../utils/collection');
const { mergeHeaders, mergeScripts, mergeVars, mergeAuth, getTreePathFromCollectionToItem } = require('../utils/collection');
const { createFormData } = require('../utils/form-data');
const prepareRequest = (item = {}, collection = {}) => {
@@ -16,6 +16,7 @@ const prepareRequest = (item = {}, collection = {}) => {
mergeHeaders(collection, request, requestTreePath);
mergeScripts(collection, request, requestTreePath, scriptFlow);
mergeVars(collection, request, requestTreePath);
mergeAuth(collection, request, requestTreePath);
}
each(get(request, 'headers', []), (h) => {
@@ -73,6 +74,37 @@ const prepareRequest = (item = {}, collection = {}) => {
};
}
if (collectionAuth.mode === 'oauth2') {
const grantType = get(collectionAuth, 'oauth2.grantType');
if (grantType === 'client_credentials') {
axiosRequest.oauth2 = {
grantType,
accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),
clientId: get(collectionAuth, 'oauth2.clientId'),
clientSecret: get(collectionAuth, 'oauth2.clientSecret'),
scope: get(collectionAuth, 'oauth2.scope'),
credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),
tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey')
};
} else if (grantType === 'password') {
axiosRequest.oauth2 = {
grantType,
accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),
username: get(collectionAuth, 'oauth2.username'),
password: get(collectionAuth, 'oauth2.password'),
clientId: get(collectionAuth, 'oauth2.clientId'),
clientSecret: get(collectionAuth, 'oauth2.clientSecret'),
scope: get(collectionAuth, 'oauth2.scope'),
credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),
tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey')
};
}
}
if (collectionAuth.mode === 'awsv4') {
axiosRequest.awsv4config = {
accessKeyId: get(collectionAuth, 'awsv4.accessKeyId'),
@@ -169,6 +201,38 @@ const prepareRequest = (item = {}, collection = {}) => {
};
}
if (request.auth.mode === 'oauth2') {
const grantType = get(request, 'auth.oauth2.grantType');
if (grantType === 'client_credentials') {
axiosRequest.oauth2 = {
grantType,
clientId: get(request, 'auth.oauth2.clientId'),
clientSecret: get(request, 'auth.oauth2.clientSecret'),
scope: get(request, 'auth.oauth2.scope'),
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'),
tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey')
};
} else if (grantType === 'password') {
axiosRequest.oauth2 = {
grantType,
username: get(request, 'auth.oauth2.username'),
password: get(request, 'auth.oauth2.password'),
clientId: get(request, 'auth.oauth2.clientId'),
clientSecret: get(request, 'auth.oauth2.clientSecret'),
scope: get(request, 'auth.oauth2.scope'),
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'),
tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey')
};
}
}
if (request.auth.mode === 'apikey') {
if (request.auth.apikey?.placement === 'header') {
axiosRequest.headers[request.auth.apikey?.key] = request.auth.apikey?.value;

View File

@@ -22,6 +22,7 @@ const path = require('path');
const { parseDataFromResponse } = require('../utils/common');
const { getCookieStringForUrl, saveCookies, shouldUseCookies } = require('../utils/cookies');
const { createFormData } = require('../utils/form-data');
const { getOAuth2Token } = require('./oauth2');
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
const { NtlmClient } = require('axios-ntlm');
const { addDigestInterceptor } = require('@usebruno/requests');
@@ -303,6 +304,33 @@ const runSingleRequest = async function (
}
}
// Handle OAuth2 authentication
if (request.oauth2) {
try {
const token = await getOAuth2Token(request.oauth2);
if (token) {
const { tokenPlacement = 'header', tokenHeaderPrefix = 'Bearer', tokenQueryKey = 'access_token' } = request.oauth2;
if (tokenPlacement === 'header') {
request.headers['Authorization'] = `${tokenHeaderPrefix} ${token}`;
} else if (tokenPlacement === 'url') {
try {
const url = new URL(request.url);
url.searchParams.set(tokenQueryKey, token);
request.url = url.toString();
} catch (error) {
console.error('Error applying OAuth2 token to URL:', error.message);
}
}
}
} catch (error) {
console.error('OAuth2 token fetch error:', error.message);
}
// Remove oauth2 config from request to prevent it from being sent
delete request.oauth2;
}
let response, responseTime;
try {

View File

@@ -0,0 +1,22 @@
// In-memory token store implementation for OAuth2 tokens
const tokenStore = {
tokens: new Map(),
// Save a token with optional expiry information
async saveToken(serviceId, account, token) {
this.tokens.set(`${serviceId}:${account}`, token);
return true;
},
// Get a token
async getToken(serviceId, account) {
return this.tokens.get(`${serviceId}:${account}`);
},
// Delete a token
async deleteToken(serviceId, account) {
return this.tokens.delete(`${serviceId}:${account}`);
}
};
module.exports = tokenStore;

View File

@@ -310,6 +310,24 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
return path;
};
const mergeAuth = (collection, request, requestTreePath) => {
let collectionAuth = collection?.root?.request?.auth || { mode: 'none' };
let effectiveAuth = collectionAuth;
for (let i of requestTreePath) {
if (i.type === 'folder') {
const folderAuth = i?.root?.request?.auth;
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
effectiveAuth = folderAuth;
}
}
}
if (request.auth && request.auth.mode === 'inherit') {
request.auth = effectiveAuth;
}
}
const getAllRequestsInFolder = (folderItems = [], recursive = true) => {
let requests = [];
@@ -461,8 +479,6 @@ const processCollectionItems = async (items = [], currentPath) => {
}
};
module.exports = {
createCollectionJsonFromPathname,
mergeHeaders,
@@ -470,7 +486,8 @@ module.exports = {
mergeScripts,
findItemInCollection,
getTreePathFromCollectionToItem,
createCollectionFromBrunoObject,
mergeAuth,
getAllRequestsInFolder,
getAllRequestsAtFolderRoot,
createCollectionFromBrunoObject
getAllRequestsAtFolderRoot
}