mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-29 07:34:07 +00:00
feat: add support for oauth2 in cli (#4578)
Co-authored-by: Pooja Belaramani <109731557+poojabela@users.noreply.github.com>
This commit is contained in:
8
package-lock.json
generated
8
package-lock.json
generated
@@ -8358,6 +8358,11 @@
|
||||
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
|
||||
"integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA=="
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
|
||||
@@ -30570,6 +30575,9 @@
|
||||
"name": "@usebruno/requests",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/qs": "^6.9.18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
|
||||
@@ -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) || '';
|
||||
|
||||
6
packages/bruno-cli/src/runner/oauth2.js
Normal file
6
packages/bruno-cli/src/runner/oauth2.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const { getOAuth2Token } = require('@usebruno/requests');
|
||||
const tokenStore = require('./tokenStore');
|
||||
|
||||
module.exports = {
|
||||
getOAuth2Token: (oauth2Config) => getOAuth2Token(oauth2Config, tokenStore)
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
22
packages/bruno-cli/src/runner/tokenStore.js
Normal file
22
packages/bruno-cli/src/runner/tokenStore.js
Normal 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;
|
||||
@@ -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
|
||||
}
|
||||
@@ -150,6 +150,72 @@ describe('prepare-request: prepareRequest', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('OAuth2 Authentication', () => {
|
||||
it('If collection auth is OAuth2 with client credentials grant type', () => {
|
||||
collection.root.request.auth = {
|
||||
mode: 'oauth2',
|
||||
oauth2: {
|
||||
grantType: 'client_credentials',
|
||||
accessTokenUrl: 'https://auth.example.com/token',
|
||||
clientId: 'test_client_id',
|
||||
clientSecret: 'test_client_secret',
|
||||
scope: 'read write',
|
||||
credentialsPlacement: 'header',
|
||||
tokenPlacement: 'header',
|
||||
tokenHeaderPrefix: 'Bearer',
|
||||
tokenQueryKey: 'access_token'
|
||||
}
|
||||
};
|
||||
|
||||
const result = prepareRequest(item, collection);
|
||||
|
||||
expect(result.oauth2).toBeDefined();
|
||||
expect(result.oauth2.grantType).toBe('client_credentials');
|
||||
expect(result.oauth2.accessTokenUrl).toBe('https://auth.example.com/token');
|
||||
expect(result.oauth2.clientId).toBe('test_client_id');
|
||||
expect(result.oauth2.clientSecret).toBe('test_client_secret');
|
||||
expect(result.oauth2.scope).toBe('read write');
|
||||
expect(result.oauth2.credentialsPlacement).toBe('header');
|
||||
expect(result.oauth2.tokenPlacement).toBe('header');
|
||||
expect(result.oauth2.tokenHeaderPrefix).toBe('Bearer');
|
||||
expect(result.oauth2.tokenQueryKey).toBe('access_token');
|
||||
});
|
||||
|
||||
it('If collection auth is OAuth2 with password grant type', () => {
|
||||
collection.root.request.auth = {
|
||||
mode: 'oauth2',
|
||||
oauth2: {
|
||||
grantType: 'password',
|
||||
accessTokenUrl: 'https://auth.example.com/token',
|
||||
username: 'test_user',
|
||||
password: 'test_password',
|
||||
clientId: 'test_client_id',
|
||||
clientSecret: 'test_client_secret',
|
||||
scope: 'read write',
|
||||
credentialsPlacement: 'body',
|
||||
tokenPlacement: 'url',
|
||||
tokenHeaderPrefix: 'Bearer',
|
||||
tokenQueryKey: 'access_token'
|
||||
}
|
||||
};
|
||||
|
||||
const result = prepareRequest(item, collection);
|
||||
|
||||
expect(result.oauth2).toBeDefined();
|
||||
expect(result.oauth2.grantType).toBe('password');
|
||||
expect(result.oauth2.accessTokenUrl).toBe('https://auth.example.com/token');
|
||||
expect(result.oauth2.username).toBe('test_user');
|
||||
expect(result.oauth2.password).toBe('test_password');
|
||||
expect(result.oauth2.clientId).toBe('test_client_id');
|
||||
expect(result.oauth2.clientSecret).toBe('test_client_secret');
|
||||
expect(result.oauth2.scope).toBe('read write');
|
||||
expect(result.oauth2.credentialsPlacement).toBe('body');
|
||||
expect(result.oauth2.tokenPlacement).toBe('url');
|
||||
expect(result.oauth2.tokenHeaderPrefix).toBe('Bearer');
|
||||
expect(result.oauth2.tokenQueryKey).toBe('access_token');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AWS v4 Authentication', () => {
|
||||
it('If collection auth is AWS v4', () => {
|
||||
collection.root.request.auth = {
|
||||
@@ -228,6 +294,7 @@ describe('prepare-request: prepareRequest', () => {
|
||||
};
|
||||
|
||||
const result = prepareRequest(item, collection);
|
||||
|
||||
const expected = {
|
||||
username: 'testUser',
|
||||
password: 'testPass123'
|
||||
|
||||
@@ -28,5 +28,8 @@
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.29.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/qs": "^6.9.18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ module.exports = [
|
||||
}),
|
||||
commonjs(),
|
||||
typescript({ tsconfig: './tsconfig.json' }),
|
||||
terser()
|
||||
]
|
||||
terser(),
|
||||
],
|
||||
external: ['axios', 'qs']
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { addDigestInterceptor } from './digestauth-helper';
|
||||
export { addDigestInterceptor } from './digestauth-helper';
|
||||
export { getOAuth2Token } from './oauth2-helper';
|
||||
199
packages/bruno-requests/src/auth/oauth2-helper.ts
Normal file
199
packages/bruno-requests/src/auth/oauth2-helper.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import qs from 'qs';
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
export interface OAuth2Config {
|
||||
grantType: 'client_credentials' | 'password';
|
||||
accessTokenUrl: string;
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
scope?: string;
|
||||
credentialsPlacement?: 'header' | 'body';
|
||||
}
|
||||
|
||||
interface RequestConfig {
|
||||
headers: {
|
||||
'Content-Type': string;
|
||||
'Authorization'?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ClientCredentialsData {
|
||||
grant_type: string;
|
||||
scope: string;
|
||||
client_id?: string;
|
||||
client_secret?: string;
|
||||
}
|
||||
|
||||
interface PasswordGrantData {
|
||||
grant_type: string;
|
||||
username: string;
|
||||
password: string;
|
||||
scope: string;
|
||||
client_id?: string;
|
||||
client_secret?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches an OAuth2 token using client credentials grant
|
||||
*/
|
||||
const fetchTokenClientCredentials = async (oauth2Config: OAuth2Config) => {
|
||||
const {
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsPlacement = 'header'
|
||||
} = oauth2Config;
|
||||
|
||||
if (!accessTokenUrl || !clientId) {
|
||||
throw new Error('Missing required OAuth2 parameters');
|
||||
}
|
||||
|
||||
const data: ClientCredentialsData = {
|
||||
grant_type: 'client_credentials',
|
||||
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(`${clientId}:${clientSecret || ''}`).toString('base64')}`;
|
||||
} else {
|
||||
// Credentials in body
|
||||
data.client_id = clientId;
|
||||
if (clientSecret) {
|
||||
data.client_secret = clientSecret;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches an OAuth2 token using password grant
|
||||
*/
|
||||
const fetchTokenPassword = async (oauth2Config: OAuth2Config) => {
|
||||
const {
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
username,
|
||||
password,
|
||||
scope,
|
||||
credentialsPlacement = 'header'
|
||||
} = oauth2Config;
|
||||
|
||||
if (!accessTokenUrl || !username || !password) {
|
||||
throw new Error('Missing required OAuth2 parameters for password grant');
|
||||
}
|
||||
|
||||
const data: PasswordGrantData = {
|
||||
grant_type: 'password',
|
||||
username,
|
||||
password,
|
||||
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(`${clientId}:${clientSecret || ''}`).toString('base64')}`;
|
||||
} else if (clientId) {
|
||||
// Credentials in body
|
||||
data.client_id = clientId;
|
||||
if (clientSecret) {
|
||||
data.client_secret = clientSecret;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
|
||||
const serviceId = accessTokenUrl;
|
||||
const account = clientId || oauth2Config.username || 'default';
|
||||
|
||||
// Check if we already have a token stored
|
||||
const existingToken = await tokenStore.getToken(serviceId, account);
|
||||
|
||||
if (existingToken) {
|
||||
// Check if token is expired
|
||||
if (existingToken.expires_at && existingToken.expires_at > Date.now()) {
|
||||
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}`);
|
||||
}
|
||||
|
||||
// Calculate expiry time if expires_in is provided
|
||||
if (tokenResponse.expires_in) {
|
||||
tokenResponse.expires_at = Date.now() + tokenResponse.expires_in * 1000;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
};
|
||||
@@ -1 +1 @@
|
||||
export { addDigestInterceptor } from './auth';
|
||||
export { addDigestInterceptor, getOAuth2Token } from './auth';
|
||||
|
||||
Reference in New Issue
Block a user