mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
fix(bru-1928): bruno-cli oauth2 updates (#5729)
This commit is contained in:
24
package-lock.json
generated
24
package-lock.json
generated
@@ -28497,6 +28497,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",
|
||||
@@ -29552,6 +29553,23 @@
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-cli/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/bruno-cli/node_modules/fs-extra": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||
@@ -29566,6 +29584,12 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/bruno-cli/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/bruno-common": {
|
||||
"name": "@usebruno/common",
|
||||
"version": "0.1.0",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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`));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
const { getOAuth2Token } = require('@usebruno/requests');
|
||||
const tokenStore = require('./tokenStore');
|
||||
|
||||
module.exports = {
|
||||
getOAuth2Token: (oauth2Config) => getOAuth2Token(oauth2Config, tokenStore)
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
50
packages/bruno-cli/src/store/tokenStore.js
Normal file
50
packages/bruno-cli/src/store/tokenStore.js
Normal 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;
|
||||
33
packages/bruno-cli/src/utils/oauth2.js
Normal file
33
packages/bruno-cli/src/utils/oauth2.js
Normal 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
|
||||
};
|
||||
@@ -1,10 +1,18 @@
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import axios, { AxiosRequestConfig, ResponseType } from 'axios';
|
||||
import qs from 'qs';
|
||||
import debug from 'debug';
|
||||
|
||||
export interface TokenStore {
|
||||
saveToken(serviceId: string, account: string, token: any): Promise<boolean>;
|
||||
getToken(serviceId: string, account: string): Promise<any>;
|
||||
deleteToken(serviceId: string, account: string): Promise<boolean>;
|
||||
saveCredential({ url, credentialsId, credentials }: { url: string; credentialsId: string; credentials: any }): Promise<boolean>;
|
||||
getCredential({ url, credentialsId }: { url: string; credentialsId: string }): Promise<any>;
|
||||
deleteCredential({ url, credentialsId }: { url: string; credentialsId: string }): Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface AdditionalParameter {
|
||||
name: string;
|
||||
value: string;
|
||||
enabled: boolean;
|
||||
sendIn: 'headers' | 'queryparams' | 'body';
|
||||
}
|
||||
|
||||
export interface OAuth2Config {
|
||||
@@ -15,14 +23,25 @@ export interface OAuth2Config {
|
||||
username?: string;
|
||||
password?: string;
|
||||
scope?: string;
|
||||
credentialsPlacement?: 'header' | 'body';
|
||||
credentialsPlacement?: 'basic_auth_header' | 'body';
|
||||
credentialsId?: string;
|
||||
autoRefreshToken?: boolean;
|
||||
autoFetchToken?: boolean;
|
||||
additionalParameters?: {
|
||||
token?: AdditionalParameter[];
|
||||
};
|
||||
}
|
||||
|
||||
interface RequestConfig {
|
||||
interface RequestConfig extends AxiosRequestConfig {
|
||||
method: string;
|
||||
url: string;
|
||||
headers: {
|
||||
'Content-Type': string;
|
||||
'Authorization'?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
data: string;
|
||||
responseType: ResponseType;
|
||||
}
|
||||
|
||||
interface ClientCredentialsData {
|
||||
@@ -30,6 +49,7 @@ interface ClientCredentialsData {
|
||||
scope?: string;
|
||||
client_id?: string;
|
||||
client_secret?: string;
|
||||
[key: string]: any; // For additional parameters
|
||||
}
|
||||
|
||||
interface PasswordGrantData {
|
||||
@@ -39,8 +59,51 @@ interface PasswordGrantData {
|
||||
scope?: string;
|
||||
client_id?: string;
|
||||
client_secret?: string;
|
||||
[key: string]: any; // For additional parameters
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply additional parameters to a request
|
||||
*/
|
||||
const applyAdditionalParameters = (requestConfig: RequestConfig, data: any, params: AdditionalParameter[] = []) => {
|
||||
params.forEach((param) => {
|
||||
if (!param.enabled || !param.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (param.sendIn) {
|
||||
case 'headers':
|
||||
requestConfig.headers[param.name] = param.value || '';
|
||||
break;
|
||||
case 'queryparams':
|
||||
// For query params, add to URL
|
||||
try {
|
||||
const url = new URL(requestConfig.url);
|
||||
url.searchParams.append(param.name, param.value || '');
|
||||
requestConfig.url = url.href;
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid token URL: ${requestConfig.url}`);
|
||||
}
|
||||
break;
|
||||
case 'body':
|
||||
// For body, add to data object
|
||||
data[param.name] = param.value || '';
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Safely parse JSON response data
|
||||
*/
|
||||
const safeParseJSONBuffer = (data: any) => {
|
||||
try {
|
||||
return JSON.parse(Buffer.isBuffer(data) ? data.toString() : data);
|
||||
} catch {
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches an OAuth2 token using client credentials grant
|
||||
*/
|
||||
@@ -50,13 +113,29 @@ const fetchTokenClientCredentials = async (oauth2Config: OAuth2Config) => {
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsPlacement = 'header'
|
||||
credentialsPlacement = 'basic_auth_header',
|
||||
additionalParameters
|
||||
} = oauth2Config;
|
||||
|
||||
if (!accessTokenUrl || !clientId) {
|
||||
throw new Error('Missing required OAuth2 parameters');
|
||||
if (!accessTokenUrl) {
|
||||
throw new Error('Access Token URL is required for OAuth2 client credentials flow');
|
||||
}
|
||||
|
||||
if (!clientId) {
|
||||
throw new Error('Client ID is required for OAuth2 client credentials flow');
|
||||
}
|
||||
|
||||
const requestConfig: RequestConfig = {
|
||||
method: 'POST',
|
||||
url: accessTokenUrl,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
data: '',
|
||||
responseType: 'arraybuffer'
|
||||
};
|
||||
|
||||
const data: ClientCredentialsData = {
|
||||
grant_type: 'client_credentials'
|
||||
};
|
||||
@@ -65,31 +144,52 @@ const fetchTokenClientCredentials = async (oauth2Config: OAuth2Config) => {
|
||||
data.scope = scope;
|
||||
}
|
||||
|
||||
const config: RequestConfig = {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
};
|
||||
|
||||
// Handle credentials placement
|
||||
if (credentialsPlacement === 'header') {
|
||||
config.headers['Authorization'] = `Basic ${Buffer.from(`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret || '')}`).toString('base64')}`;
|
||||
} else {
|
||||
// Credentials in body
|
||||
data.client_id = clientId;
|
||||
if (clientSecret) {
|
||||
data.client_secret = clientSecret;
|
||||
}
|
||||
if (credentialsPlacement === 'basic_auth_header') {
|
||||
requestConfig.headers['Authorization'] = `Basic ${Buffer.from(`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret!)}`).toString('base64')}`;
|
||||
}
|
||||
|
||||
if (credentialsPlacement !== 'basic_auth_header') {
|
||||
data.client_id = clientId;
|
||||
}
|
||||
|
||||
if (clientSecret && clientSecret.trim() !== '' && credentialsPlacement !== 'basic_auth_header') {
|
||||
data.client_secret = clientSecret;
|
||||
}
|
||||
|
||||
if (additionalParameters?.token?.length) {
|
||||
applyAdditionalParameters(requestConfig, data, additionalParameters.token);
|
||||
}
|
||||
|
||||
requestConfig.data = qs.stringify(data);
|
||||
|
||||
debug('oauth2')('> request');
|
||||
debug('oauth2')(JSON.stringify(requestConfig, null, 2));
|
||||
|
||||
try {
|
||||
const response = await axios.post(accessTokenUrl, qs.stringify(data), config);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error('CLIENT_CREDENTIALS: Error fetching OAuth2 token:', error.message);
|
||||
const response = await axios(requestConfig);
|
||||
const parsedData = safeParseJSONBuffer(response.data);
|
||||
|
||||
if (parsedData && typeof parsedData === 'object') {
|
||||
parsedData.created_at = Date.now();
|
||||
}
|
||||
throw error;
|
||||
|
||||
debug('oauth2')('> response');
|
||||
debug('oauth2')(JSON.stringify(parsedData, null, 2));
|
||||
return parsedData;
|
||||
} catch (err: any) {
|
||||
if (err?.response) {
|
||||
debug('oauth2')('< error');
|
||||
debug('oauth2')(JSON.stringify({
|
||||
status: err.response.status,
|
||||
statusText: err.response.statusText,
|
||||
data: err.response.data ? safeParseJSONBuffer(err.response.data) : null,
|
||||
headers: err.response.headers
|
||||
}, null, 2));
|
||||
} else {
|
||||
debug('oauth2')('< error');
|
||||
debug('oauth2')(err.message || err);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -104,13 +204,37 @@ const fetchTokenPassword = async (oauth2Config: OAuth2Config) => {
|
||||
username,
|
||||
password,
|
||||
scope,
|
||||
credentialsPlacement = 'header'
|
||||
credentialsPlacement = 'basic_auth_header',
|
||||
additionalParameters
|
||||
} = oauth2Config;
|
||||
|
||||
if (!accessTokenUrl || !username || !password) {
|
||||
throw new Error('Missing required OAuth2 parameters for password grant');
|
||||
if (!accessTokenUrl) {
|
||||
throw new Error('Access Token URL is required for OAuth2 password credentials flow');
|
||||
}
|
||||
|
||||
if (!username) {
|
||||
throw new Error('Username is required for OAuth2 password credentials flow');
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
throw new Error('Password is required for OAuth2 password credentials flow');
|
||||
}
|
||||
|
||||
if (!clientId) {
|
||||
throw new Error('Client ID is required for OAuth2 password credentials flow');
|
||||
}
|
||||
|
||||
const requestConfig: RequestConfig = {
|
||||
method: 'POST',
|
||||
url: accessTokenUrl,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
data: '',
|
||||
responseType: 'arraybuffer'
|
||||
};
|
||||
|
||||
const data: PasswordGrantData = {
|
||||
grant_type: 'password',
|
||||
username,
|
||||
@@ -121,85 +245,149 @@ const fetchTokenPassword = async (oauth2Config: OAuth2Config) => {
|
||||
data.scope = scope;
|
||||
}
|
||||
|
||||
const config: RequestConfig = {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
};
|
||||
|
||||
// Handle credentials placement
|
||||
if (credentialsPlacement === 'header' && clientId) {
|
||||
config.headers['Authorization'] = `Basic ${Buffer.from(`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret || '')}`).toString('base64')}`;
|
||||
} else if (clientId) {
|
||||
// Credentials in body
|
||||
data.client_id = clientId;
|
||||
if (clientSecret) {
|
||||
data.client_secret = clientSecret;
|
||||
}
|
||||
if (credentialsPlacement === 'basic_auth_header') {
|
||||
requestConfig.headers['Authorization'] = `Basic ${Buffer.from(`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret!)}`).toString('base64')}`;
|
||||
}
|
||||
|
||||
if (credentialsPlacement !== 'basic_auth_header') {
|
||||
data.client_id = clientId;
|
||||
}
|
||||
|
||||
if (clientSecret && clientSecret.trim() !== '' && credentialsPlacement !== 'basic_auth_header') {
|
||||
data.client_secret = clientSecret;
|
||||
}
|
||||
|
||||
if (additionalParameters?.token?.length) {
|
||||
applyAdditionalParameters(requestConfig, data, additionalParameters.token);
|
||||
}
|
||||
|
||||
requestConfig.data = qs.stringify(data);
|
||||
|
||||
debug('oauth2')('> request');
|
||||
debug('oauth2')(JSON.stringify(requestConfig, null, 2));
|
||||
|
||||
try {
|
||||
const response = await axios.post(accessTokenUrl, qs.stringify(data), config);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError && error.response) {
|
||||
console.error('PASSWORD_GRANT: Error fetching OAuth2 token:', error.message);
|
||||
console.error('Status:', error.response.status, 'Response:', error.response.data);
|
||||
} else if (error instanceof Error) {
|
||||
console.error('PASSWORD_GRANT: Error fetching OAuth2 token:', error.message);
|
||||
const response = await axios(requestConfig);
|
||||
const parsedData = safeParseJSONBuffer(response.data);
|
||||
|
||||
if (parsedData && typeof parsedData === 'object') {
|
||||
parsedData.created_at = Date.now();
|
||||
}
|
||||
throw error;
|
||||
|
||||
debug('oauth2')('< response');
|
||||
debug('oauth2')(JSON.stringify(parsedData, null, 2));
|
||||
return parsedData;
|
||||
} catch (err: any) {
|
||||
if (err?.response) {
|
||||
debug('oauth2')('< error');
|
||||
debug('oauth2')(JSON.stringify({
|
||||
status: err.response.status,
|
||||
statusText: err.response.statusText,
|
||||
data: err.response.data ? safeParseJSONBuffer(err.response.data) : null,
|
||||
headers: err.response.headers
|
||||
}, null, 2));
|
||||
} else {
|
||||
debug('oauth2')('< error');
|
||||
debug('oauth2')(err.message || err);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a token is expired
|
||||
*/
|
||||
const isTokenExpired = (credentials: any): boolean => {
|
||||
if (!credentials?.access_token) {
|
||||
return true;
|
||||
}
|
||||
if (!credentials?.expires_in || !credentials.created_at) {
|
||||
return false; // No expiration info, assume valid
|
||||
}
|
||||
const expiryTime = credentials.created_at + credentials.expires_in * 1000;
|
||||
return Date.now() > expiryTime;
|
||||
};
|
||||
|
||||
/**
|
||||
* Manages OAuth2 token retrieval and storage
|
||||
*/
|
||||
export const getOAuth2Token = async (oauth2Config: OAuth2Config, tokenStore: TokenStore): Promise<string | null> => {
|
||||
const { grantType, clientId, accessTokenUrl } = oauth2Config;
|
||||
|
||||
if (!grantType || !accessTokenUrl) {
|
||||
throw new Error('Missing required OAuth2 parameters: grantType or accessTokenUrl');
|
||||
export const getOAuth2Token = async (oauth2Config: OAuth2Config, tokenStore: TokenStore, verbose: string): Promise<string | null> => {
|
||||
const {
|
||||
grantType,
|
||||
accessTokenUrl,
|
||||
credentialsId = 'default',
|
||||
autoFetchToken = true
|
||||
} = oauth2Config;
|
||||
|
||||
if (verbose) {
|
||||
debug.enable('oauth2');
|
||||
}
|
||||
|
||||
const serviceId = accessTokenUrl;
|
||||
const account = clientId || oauth2Config.username || 'default';
|
||||
if (!grantType) {
|
||||
throw new Error('Grant type is required for OAuth2');
|
||||
}
|
||||
|
||||
// Check if we already have a token stored
|
||||
const existingToken = await tokenStore.getToken(serviceId, account);
|
||||
if (!accessTokenUrl) {
|
||||
throw new Error('Access token URL is required for OAuth2');
|
||||
}
|
||||
|
||||
if (!['client_credentials', 'password'].includes(grantType)) {
|
||||
throw new Error(`Unsupported grant type: ${grantType}. Supported types: client_credentials, password`);
|
||||
}
|
||||
|
||||
// Check if we already have credentials stored
|
||||
const existingToken = await tokenStore.getCredential({ url: accessTokenUrl, credentialsId });
|
||||
|
||||
if (existingToken) {
|
||||
// Check if token is expired
|
||||
if (existingToken.expires_at && existingToken.expires_at > Date.now()) {
|
||||
if (!isTokenExpired(existingToken)) {
|
||||
// Token is valid, use it
|
||||
return existingToken.access_token;
|
||||
}
|
||||
}
|
||||
|
||||
// No valid token found, fetch a new one
|
||||
try {
|
||||
let tokenResponse;
|
||||
|
||||
if (grantType === 'client_credentials') {
|
||||
tokenResponse = await fetchTokenClientCredentials(oauth2Config);
|
||||
} else if (grantType === 'password') {
|
||||
tokenResponse = await fetchTokenPassword(oauth2Config);
|
||||
} else {
|
||||
throw new Error(`Unsupported grant type: ${grantType}`);
|
||||
// Token is expired
|
||||
if (autoFetchToken) {
|
||||
// Clear expired token and proceed to fetch new token
|
||||
await tokenStore.deleteCredential({ url: accessTokenUrl, credentialsId });
|
||||
} else {
|
||||
// Return expired token if autoFetchToken is disabled
|
||||
return existingToken.access_token;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate expiry time if expires_in is provided
|
||||
if (tokenResponse.expires_in) {
|
||||
tokenResponse.expires_at = Date.now() + tokenResponse.expires_in * 1000;
|
||||
} else {
|
||||
// No stored credentials
|
||||
if (!autoFetchToken) {
|
||||
// Don't fetch token if autoFetchToken is disabled
|
||||
return null;
|
||||
}
|
||||
|
||||
// Store the token
|
||||
await tokenStore.saveToken(serviceId, account, tokenResponse);
|
||||
|
||||
return tokenResponse.access_token;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error('Failed to get OAuth2 token:', error.message);
|
||||
}
|
||||
return null;
|
||||
// Otherwise, proceed to fetch new token
|
||||
}
|
||||
};
|
||||
|
||||
let tokenResponse;
|
||||
|
||||
if (grantType === 'client_credentials') {
|
||||
tokenResponse = await fetchTokenClientCredentials(oauth2Config);
|
||||
} else if (grantType === 'password') {
|
||||
tokenResponse = await fetchTokenPassword(oauth2Config);
|
||||
} else {
|
||||
throw new Error(`Unsupported grant type: ${grantType}`);
|
||||
}
|
||||
|
||||
if (tokenResponse.error) {
|
||||
throw new Error(JSON.stringify(tokenResponse));
|
||||
}
|
||||
|
||||
if (!tokenResponse || !tokenResponse.access_token) {
|
||||
throw new Error('No access token received from server');
|
||||
}
|
||||
|
||||
if (tokenResponse.expires_in && tokenResponse.created_at) {
|
||||
tokenResponse.expires_at = tokenResponse.created_at + tokenResponse.expires_in * 1000;
|
||||
}
|
||||
|
||||
const saved = await tokenStore.saveCredential({ url: accessTokenUrl, credentialsId, credentials: tokenResponse });
|
||||
if (!saved) {
|
||||
console.warn('OAuth2: Failed to save token to store, but proceeding with token');
|
||||
}
|
||||
|
||||
return tokenResponse.access_token;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user