fix(bru-1928): bruno-cli oauth2 updates (#5729)

This commit is contained in:
lohit
2025-10-07 22:38:52 +05:30
committed by GitHub
parent c1853e613b
commit 10739c32c4
11 changed files with 469 additions and 134 deletions

View File

@@ -59,6 +59,7 @@
"axios-ntlm": "^1.4.2",
"chai": "^4.3.7",
"chalk": "^3.0.0",
"debug": "^4.4.3",
"decomment": "^0.9.5",
"form-data": "^4.0.0",
"fs-extra": "^10.1.0",

View File

@@ -210,6 +210,10 @@ const builder = async (yargs) => {
type: 'string',
description: 'Tags to exclude from the run'
})
.option('verbose', {
type: 'boolean',
description: 'Allow verbose output for debugging purposes'
})
.example('$0 run request.bru', 'Run a request')
.example('$0 run request.bru --env local', 'Run a request with the environment set to local')
.example('$0 run request.bru --env-file env.bru', 'Run a request with the environment from env.bru file')
@@ -285,7 +289,8 @@ const handler = async function (argv) {
noproxy,
delay,
tags: includeTags,
excludeTags
excludeTags,
verbose
} = argv;
const collectionPath = process.cwd();
@@ -411,6 +416,9 @@ const handler = async function (argv) {
if (noproxy) {
options['noproxy'] = true;
}
if (verbose) {
options['verbose'] = true;
}
if (cacert && cacert.length) {
if (insecure) {
console.error(chalk.red(`Ignoring the cacert option since insecure connections are enabled`));

View File

@@ -17,6 +17,7 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
const collectionVariables = request?.collectionVariables || {};
const folderVariables = request?.folderVariables || {};
const requestVariables = request?.requestVariables || {};
const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};
// we clone envVars because we don't want to modify the original object
envVariables = cloneDeep(envVariables);
@@ -43,6 +44,7 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
...envVariables,
...folderVariables,
...requestVariables,
...oauth2CredentialVariables,
...runtimeVariables,
process: {
env: {
@@ -172,6 +174,7 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || '';
request.oauth2.scope = _interpolate(request.oauth2.scope) || '';
request.oauth2.credentialsPlacement = _interpolate(request.oauth2.credentialsPlacement) || '';
request.oauth2.credentialsId = _interpolate(request.oauth2.credentialsId) || '';
request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || '';
request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || '';
request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || '';
@@ -183,6 +186,7 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || '';
request.oauth2.scope = _interpolate(request.oauth2.scope) || '';
request.oauth2.credentialsPlacement = _interpolate(request.oauth2.credentialsPlacement) || '';
request.oauth2.credentialsId = _interpolate(request.oauth2.credentialsId) || '';
request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || '';
request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || '';
request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || '';
@@ -190,6 +194,39 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
default:
break;
}
// Interpolate additional parameters for all OAuth2 grant types
if (request.oauth2.additionalParameters) {
// Interpolate authorization parameters
if (Array.isArray(request.oauth2.additionalParameters.authorization)) {
request.oauth2.additionalParameters.authorization.forEach((param) => {
if (param && param.enabled !== false) {
param.name = _interpolate(param.name) || '';
param.value = _interpolate(param.value) || '';
}
});
}
// Interpolate token parameters
if (Array.isArray(request.oauth2.additionalParameters.token)) {
request.oauth2.additionalParameters.token.forEach((param) => {
if (param && param.enabled !== false) {
param.name = _interpolate(param.name) || '';
param.value = _interpolate(param.value) || '';
}
});
}
// Interpolate refresh parameters
if (Array.isArray(request.oauth2.additionalParameters.refresh)) {
request.oauth2.additionalParameters.refresh.forEach((param) => {
if (param && param.enabled !== false) {
param.name = _interpolate(param.name) || '';
param.value = _interpolate(param.value) || '';
}
});
}
}
}
if (request.awsv4config) {

View File

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

View File

@@ -9,6 +9,7 @@ const { mergeHeaders, mergeScripts, mergeVars, mergeAuth, getTreePathFromCollect
const { buildFormUrlEncodedPayload } = require('../utils/form-data');
const path = require('node:path');
const { isLargeFile } = require('../utils/filesystem');
const { getFormattedOauth2Credentials } = require('../utils/oauth2');
const STREAMING_FILE_SIZE_THRESHOLD = 20 * 1024 * 1024; // 20MB
@@ -93,27 +94,37 @@ const prepareRequest = async (item = {}, collection = {}) => {
axiosRequest.oauth2 = {
grantType,
accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),
refreshTokenUrl: get(collectionAuth, 'oauth2.refreshTokenUrl'),
clientId: get(collectionAuth, 'oauth2.clientId'),
clientSecret: get(collectionAuth, 'oauth2.clientSecret'),
scope: get(collectionAuth, 'oauth2.scope'),
credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),
credentialsId: get(collectionAuth, 'oauth2.credentialsId'),
tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey')
tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'),
autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'),
autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken'),
additionalParameters: get(collectionAuth, 'oauth2.additionalParameters', { authorization: [], token: [], refresh: [] })
};
} else if (grantType === 'password') {
axiosRequest.oauth2 = {
grantType,
accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),
refreshTokenUrl: get(collectionAuth, 'oauth2.refreshTokenUrl'),
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'),
credentialsId: get(collectionAuth, 'oauth2.credentialsId'),
tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey')
tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'),
autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'),
autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken'),
additionalParameters: get(collectionAuth, 'oauth2.additionalParameters', { authorization: [], token: [], refresh: [] })
};
}
}
@@ -218,29 +229,39 @@ const prepareRequest = async (item = {}, collection = {}) => {
if (grantType === 'client_credentials') {
axiosRequest.oauth2 = {
grantType,
grantType: grantType,
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
refreshTokenUrl: get(request, 'auth.oauth2.refreshTokenUrl'),
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'),
credentialsId: get(request, 'auth.oauth2.credentialsId'),
tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey')
tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'),
autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'),
autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken'),
additionalParameters: get(request, 'auth.oauth2.additionalParameters', { authorization: [], token: [], refresh: [] })
};
} else if (grantType === 'password') {
axiosRequest.oauth2 = {
grantType,
grantType: grantType,
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
refreshTokenUrl: get(request, 'auth.oauth2.refreshTokenUrl'),
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'),
credentialsId: get(request, 'auth.oauth2.credentialsId'),
tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey')
tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'),
autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'),
autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken'),
additionalParameters: get(request, 'auth.oauth2.additionalParameters', { authorization: [], token: [], refresh: [] })
};
}
}
@@ -367,6 +388,7 @@ const prepareRequest = async (item = {}, collection = {}) => {
axiosRequest.collectionVariables = request.collectionVariables;
axiosRequest.folderVariables = request.folderVariables;
axiosRequest.requestVariables = request.requestVariables;
axiosRequest.oauth2CredentialVariables = getFormattedOauth2Credentials();
return axiosRequest;
};

View File

@@ -20,11 +20,11 @@ const path = require('path');
const { parseDataFromResponse } = require('../utils/common');
const { getCookieStringForUrl, saveCookies } = 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');
const { getCACertificates } = require('@usebruno/requests');
const { getOAuth2Token } = require('../utils/oauth2');
const { encodeUrl } = require('@usebruno/common').utils;
const onConsoleLog = (type, args) => {

View File

@@ -1,22 +0,0 @@
// 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

@@ -0,0 +1,50 @@
// In-memory credential store implementation for OAuth2 credentials
const tokenStore = {
credentials: {},
// Save credentials
async saveCredential({ url, credentialsId, credentials }) {
if (!this.credentials[credentialsId]) {
this.credentials[credentialsId] = {};
}
this.credentials[credentialsId][url] = credentials;
return true;
},
// Get credentials
async getCredential({ url, credentialsId }) {
return this.credentials[credentialsId]?.[url];
},
// Delete credentials
async deleteCredential({ url, credentialsId }) {
if (this.credentials[credentialsId]?.[url]) {
delete this.credentials[credentialsId][url];
// Clean up empty credentialsId objects
if (Object.keys(this.credentials[credentialsId]).length === 0) {
delete this.credentials[credentialsId];
}
return true;
}
return false;
},
// Get all stored OAuth2 credentials
getAllCredentials() {
const result = [];
for (const [credentialsId, urlMap] of Object.entries(this.credentials)) {
for (const [url, credentials] of Object.entries(urlMap)) {
if (credentials) {
result.push({
url,
credentialsId,
credentials
});
}
}
}
return result;
}
};
module.exports = tokenStore;

View File

@@ -0,0 +1,33 @@
const { getOAuth2Token: _getOAuth2Token } = require('@usebruno/requests');
const tokenStore = require('../store/tokenStore');
const { getOptions } = require('./bru');
/**
* Formats OAuth2 credentials into variables that can be accessed via bru.getOauth2CredentialVar()
* @returns {Object} Formatted OAuth2 credential variables
*/
const getFormattedOauth2Credentials = () => {
const oauth2Credentials = tokenStore.getAllCredentials();
let credentialsVariables = {};
oauth2Credentials.forEach(({ credentialsId, credentials }) => {
if (credentials) {
Object.entries(credentials).forEach(([key, value]) => {
credentialsVariables[`$oauth2.${credentialsId}.${key}`] = value;
});
}
});
return credentialsVariables;
};
const getOAuth2Token = (oauth2Config) => {
let options = getOptions();
let verbose = options?.verbose;
return _getOAuth2Token(oauth2Config, tokenStore, verbose);
};
module.exports = {
getFormattedOauth2Credentials,
getOAuth2Token
};