Compare commits

...

9 Commits

Author SHA1 Message Date
lohxt1
25b482d6f3 Merge branch 'pietrygamat/inherit-oauth' into HEAD 2025-01-11 12:24:06 +05:30
lohxt1
fefe71eaa4 Merge remote-tracking branch 'pietrygamat/feature/inherit-oauth' into pietrygamat/inherit-oauth 2025-01-11 12:21:45 +05:30
Mateusz Pietryga
3bd8f09c88 feat: OAuth2 - Supported at the collection level (#1704) 2024-09-23 21:59:16 +02:00
Mateusz Pietryga
dd9cb21f8c feat: OAuth2 - UI for OAuth2 Credentials independent of the Request Output pane
fix: typo - rename OAuth2PasswordCredentials component
fix: typo - Use the same name for AuthMode - OAuth 2.0 in collection and request level
2024-09-23 21:59:16 +02:00
Mateusz Pietryga
2064cc88ab feat: OAuth2 - automatically handle Bearer token type only
According to RFC6749 Section 7.1, The client MUST NOT use an access token
if it does not understand the token type.
At this point bruno only understands 'bearer' token_type.
2024-09-23 21:59:16 +02:00
Mateusz Pietryga
d982e35a17 feat: OAuth2 - Do not make axios request when executing collection level Get Access Token action
The actual the authorization request is now part of request preparation, and its response is returned for post-request script processing.
2024-09-23 21:59:16 +02:00
Mateusz Pietryga
4afcd44216 feat: OAuth2 - Include resolved authorization details in req object to be usable by scripts
The new variable 'credentials' is now available in 'req' object. It is added automatically during request preparation if oauth2 method is used and is value is either evaluated or retrieved from collection oauth2 cache.
2024-09-23 21:59:16 +02:00
Mateusz Pietryga
63252d3ee2 feat: OAuth2 - Store authorization information
Results of oauth2 authorization flow (i.e. access_token but also refresh_token, id_token, scope or any other information returned from token request) are stored in a collection specific cache. It is persisted in the file system, and will be automatically reused when executing requests until the cache is purged (using Clear Cache button available in all related views).
2024-09-23 20:50:41 +02:00
Mateusz Pietryga
22a9502976 fix: OAuth2 - auth is successful but token endpoint is returned instead of api endpoint (#1999)
Setting oauth2 authorization no longer equals overwriting user-specified data in a request. The pre-requests made to obtain oauth2 access_token are now separated from actual API request.
2024-09-23 20:50:37 +02:00
20 changed files with 265 additions and 176 deletions

View File

@@ -86,7 +86,7 @@ const AuthMode = ({ collection }) => {
onModeChange('oauth2');
}}
>
Oauth2
OAuth 2.0
</div>
<div
className="dropdown-item"

View File

@@ -7,8 +7,6 @@ import { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/Redux
import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
import { clearOauth2Cache } from 'utils/network/index';
import toast from 'react-hot-toast';
const OAuth2AuthorizationCode = ({ collection }) => {
const dispatch = useDispatch();
@@ -64,17 +62,6 @@ const OAuth2AuthorizationCode = ({ collection }) => {
})
);
};
const handleClearCache = (e) => {
clearOauth2Cache(collection?.uid)
.then(() => {
toast.success('cleared cache successfully');
})
.catch((err) => {
toast.error(err.message);
});
};
return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
{inputsConfig.map((input) => {
@@ -105,14 +92,6 @@ const OAuth2AuthorizationCode = ({ collection }) => {
onChange={handlePKCEToggle}
/>
</div>
<div className="flex flex-row gap-4">
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
Get Access Token
</button>
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
Clear Cache
</button>
</div>
</StyledWrapper>
);
};

View File

@@ -60,9 +60,6 @@ const OAuth2ClientCredentials = ({ collection }) => {
</div>
);
})}
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
Get Access Token
</button>
</StyledWrapper>
);
};

View File

@@ -6,9 +6,9 @@ import SingleLineEditor from 'components/SingleLineEditor';
import { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
const OAuth2AuthorizationCode = ({ item, collection }) => {
const OAuth2PasswordCredentials = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
@@ -62,11 +62,8 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
</div>
);
})}
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
Get Access Token
</button>
</StyledWrapper>
);
};
export default OAuth2AuthorizationCode;
export default OAuth2PasswordCredentials;

View File

@@ -5,6 +5,7 @@ import GrantTypeSelector from './GrantTypeSelector/index';
import OAuth2PasswordCredentials from './PasswordCredentials/index';
import OAuth2AuthorizationCode from './AuthorizationCode/index';
import OAuth2ClientCredentials from './ClientCredentials/index';
import CredentialsPreview from 'components/RequestPane/Auth/OAuth2/CredentialsPreview';
const grantTypeComponentMap = (grantType, collection) => {
switch (grantType) {
@@ -30,6 +31,7 @@ const OAuth2 = ({ collection }) => {
<StyledWrapper className="mt-2 w-full">
<GrantTypeSelector collection={collection} />
{grantTypeComponentMap(oAuth?.grantType, collection)}
<CredentialsPreview collection={collection} />
</StyledWrapper>
);
};

View File

@@ -7,8 +7,6 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
import { clearOauth2Cache } from 'utils/network/index';
import toast from 'react-hot-toast';
const OAuth2AuthorizationCode = ({ item, collection }) => {
const dispatch = useDispatch();
@@ -67,16 +65,6 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
);
};
const handleClearCache = (e) => {
clearOauth2Cache(collection?.uid)
.then(() => {
toast.success('cleared cache successfully');
})
.catch((err) => {
toast.error(err.message);
});
};
return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
{inputsConfig.map((input) => {
@@ -108,14 +96,6 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
onChange={handlePKCEToggle}
/>
</div>
<div className="flex flex-row gap-4">
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
Get Access Token
</button>
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
Clear Cache
</button>
</div>
</StyledWrapper>
);
};

View File

@@ -62,9 +62,6 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
</div>
);
})}
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
Get Access Token
</button>
</StyledWrapper>
);
};

View File

@@ -0,0 +1,17 @@
import styled from 'styled-components';
const Wrapper = styled.div`
label {
display: block;
font-size: 0.8125rem;
}
textarea {
height: fit-content;
max-width: 400px;
border: solid 1px ${(props) => props.theme.input.border};
background-color: ${(props) => props.theme.input.bg};
}
`;
export default Wrapper;

View File

@@ -0,0 +1,80 @@
import React, { useEffect, useState } from 'react';
import { clearOauth2Cache, readOauth2CachedCredentials } from 'utils/network';
import { sendCollectionOauth2Request, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast';
import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
const CredentialsPreview = ({ item, collection }) => {
const oauth2CredentialsAreaRef = React.createRef();
const [oauth2Credentials, setOauth2Credentials] = useState({});
const dispatch = useDispatch();
useEffect(() => {
oauth2CredentialsAreaRef.current.value = oauth2Credentials;
readOauth2CachedCredentials(collection.uid).then((credentials) => setOauth2Credentials(credentials));
}, [oauth2CredentialsAreaRef]);
const handleRun = async () => {
if (item) {
dispatch(sendRequest(item, collection.uid));
} else {
dispatch(sendCollectionOauth2Request(collection.uid));
}
};
const handleClearCache = (e) => {
clearOauth2Cache(collection?.uid)
.then(() => {
readOauth2CachedCredentials(collection.uid).then((credentials) => {
setOauth2Credentials(credentials);
toast.success('Cleared cache successfully');
});
})
.catch((err) => {
toast.error(err.message);
});
};
const sortedFields = () => {
const tokens = {};
const extras = {};
Object.entries(oauth2Credentials).forEach(([key, value]) => {
if (key.endsWith('_token')) {
tokens[key] = value;
} else {
extras[key] = value;
}
});
return { ...tokens, ...extras };
};
return (
<StyledWrapper className="flex flex-col w-full gap-1 mt-4">
<div className="credential-item-wrapper" ref={oauth2CredentialsAreaRef}>
{Object.entries(oauth2Credentials).length > 0 ? (
<>
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
Clear Access Token Cache
</button>
<details className="cursor-pointer flex flex-row w-full mt-2 gap-2">
<summary>Cached OAuth2 Credentials</summary>
{Object.entries(sortedFields()).map(([field, value]) => (
<div key={field}>
<label className="text-xs">{field}</label>
<textarea className="w-full h-24 p-2 text-xs border rounded" value={value} readOnly />
</div>
))}
</details>
</>
) : (
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
Get Access Token
</button>
)}
</div>
</StyledWrapper>
);
};
export default CredentialsPreview;

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
@@ -8,7 +8,7 @@ import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collection
import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
const OAuth2AuthorizationCode = ({ item, collection }) => {
const OAuth2PasswordCredentials = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
@@ -64,11 +64,8 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
</div>
);
})}
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
Get Access Token
</button>
</StyledWrapper>
);
};
export default OAuth2AuthorizationCode;
export default OAuth2PasswordCredentials;

View File

@@ -5,6 +5,7 @@ import GrantTypeSelector from './GrantTypeSelector/index';
import OAuth2PasswordCredentials from './PasswordCredentials/index';
import OAuth2AuthorizationCode from './AuthorizationCode/index';
import OAuth2ClientCredentials from './ClientCredentials/index';
import CredentialsPreview from './CredentialsPreview';
const grantTypeComponentMap = (grantType, item, collection) => {
switch (grantType) {
@@ -30,6 +31,7 @@ const OAuth2 = ({ item, collection }) => {
<StyledWrapper className="mt-2 w-full">
<GrantTypeSelector item={item} collection={collection} />
{grantTypeComponentMap(oAuth?.grantType, item, collection)}
<CredentialsPreview item={item} collection={collection} />
</StyledWrapper>
);
};

View File

@@ -8,8 +8,9 @@ import DigestAuth from './DigestAuth';
import WsseAuth from './WsseAuth';
import ApiKeyAuth from './ApiKeyAuth';
import StyledWrapper from './StyledWrapper';
import { humanizeRequestAuthMode } from 'utils/collections/index';
import { humanizeRequestAuthMode } from 'utils/collections';
import OAuth2 from './OAuth2/index';
import CredentialsPreview from './OAuth2/CredentialsPreview';
const Auth = ({ item, collection }) => {
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
@@ -42,24 +43,13 @@ const Auth = ({ item, collection }) => {
}
case 'inherit': {
return (
<div className="flex flex-row w-full mt-2 gap-2">
{collectionAuth?.mode === 'oauth2' ? (
<div className="flex flex-col gap-2">
<div className="flex flex-row gap-1">
<div>Collection level auth is: </div>
<div className="inherit-mode-text">{humanizeRequestAuthMode(collectionAuth?.mode)}</div>
</div>
<div className="text-sm opacity-50">
Note: You need to use scripting to set the access token in the request headers.
</div>
</div>
) : (
<>
<div>Auth inherited from the Collection: </div>
<div className="inherit-mode-text">{humanizeRequestAuthMode(collectionAuth?.mode)}</div>
</>
)}
</div>
<>
<div className="flex flex-row w-full mt-2 gap-2">
<div>Auth inherited from the Collection: </div>
<div className="inherit-mode-text">{humanizeRequestAuthMode(collectionAuth?.mode)}</div>
</div>
{collectionAuth?.mode === 'oauth2' && <CredentialsPreview item={item} collection={collection} />}
</>
);
}
}

View File

@@ -50,6 +50,13 @@ export const clearOauth2Cache = async (uid) => {
});
};
export const readOauth2CachedCredentials = async (uid) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('read-oauth2-cached-credentials', uid).then(resolve).catch(reject);
});
};
export const fetchGqlSchema = async (endpoint, environment, request, collection) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;

View File

@@ -12,7 +12,6 @@ const { ipcMain } = require('electron');
const { isUndefined, isNull, each, get, compact, cloneDeep, forOwn, extend } = require('lodash');
const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js');
const prepareRequest = require('./prepare-request');
const prepareCollectionRequest = require('./prepare-collection-request');
const prepareGqlIntrospectionRequest = require('./prepare-gql-introspection-request');
const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token');
const { uuid } = require('../../utils/common');
@@ -31,9 +30,9 @@ const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../../utils/proxy-ut
const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem');
const { getCookieStringForUrl, addCookieToJar, getDomainsWithCookies } = require('../../utils/cookies');
const {
resolveOAuth2AuthorizationCodeAccessToken,
transformClientCredentialsRequest,
transformPasswordCredentialsRequest
oauth2AuthorizeWithAuthorizationCode,
oauth2AuthorizeWithClientCredentials,
oauth2AuthorizeWithPasswordCredentials
} = require('./oauth2-helper');
const Oauth2Store = require('../../store/oauth2');
const iconv = require('iconv-lite');
@@ -276,35 +275,30 @@ const configureRequest = async (
if (request.oauth2) {
let requestCopy = cloneDeep(request);
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
let credentials, response;
switch (request?.oauth2?.grantType) {
case 'authorization_code':
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
const { data: authorizationCodeData, url: authorizationCodeAccessTokenUrl } =
await resolveOAuth2AuthorizationCodeAccessToken(requestCopy, collectionUid);
request.method = 'POST';
request.headers['content-type'] = 'application/x-www-form-urlencoded';
request.data = authorizationCodeData;
request.url = authorizationCodeAccessTokenUrl;
case 'authorization_code': {
({ credentials, response } = await oauth2AuthorizeWithAuthorizationCode(requestCopy, collectionUid));
break;
case 'client_credentials':
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
const { data: clientCredentialsData, url: clientCredentialsAccessTokenUrl } =
await transformClientCredentialsRequest(requestCopy);
request.method = 'POST';
request.headers['content-type'] = 'application/x-www-form-urlencoded';
request.data = clientCredentialsData;
request.url = clientCredentialsAccessTokenUrl;
}
case 'client_credentials': {
({ credentials, response } = await oauth2AuthorizeWithClientCredentials(requestCopy, collectionUid));
break;
case 'password':
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
const { data: passwordData, url: passwordAccessTokenUrl } = await transformPasswordCredentialsRequest(
requestCopy
);
request.method = 'POST';
request.headers['content-type'] = 'application/x-www-form-urlencoded';
request.data = passwordData;
request.url = passwordAccessTokenUrl;
}
case 'password': {
({ credentials, response } = await oauth2AuthorizeWithPasswordCredentials(requestCopy, collectionUid));
break;
}
}
request.credentials = credentials;
request.authRequestResponse = response;
// Bruno can handle bearer token type automatically.
// Other - more exotic token types are not touched
// Users are free to use pre-request script and operate on req.credentials.access_token variable
if (credentials?.token_type?.toLowerCase() === 'bearer') {
request.headers['Authorization'] = `Bearer ${credentials.access_token}`;
}
}
@@ -781,7 +775,7 @@ const registerNetworkIpc = (mainWindow) => {
);
interpolateVars(request, envVars, collection.runtimeVariables, processEnvVars);
const axiosInstance = await configureRequest(
await configureRequest(
collection.uid,
request,
envVars,
@@ -790,19 +784,13 @@ const registerNetworkIpc = (mainWindow) => {
collectionPath
);
try {
response = await axiosInstance(request);
} catch (error) {
if (error?.response) {
response = error.response;
} else {
return Promise.reject(error);
}
const response = request.authRequestResponse;
// When credentials are loaded from cache, authRequestResponse has no data
if (response.data) {
const { data } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
response.data = data;
}
const { data } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
response.data = data;
await runPostResponse(
request,
response,
@@ -820,7 +808,8 @@ const registerNetworkIpc = (mainWindow) => {
status: response.status,
statusText: response.statusText,
headers: response.headers,
data: response.data
data: response.data,
credentials: request.credentials
};
} catch (error) {
return Promise.reject(error);
@@ -839,6 +828,17 @@ const registerNetworkIpc = (mainWindow) => {
});
});
ipcMain.handle('read-oauth2-cached-credentials', async (event, uid) => {
return new Promise((resolve, reject) => {
try {
const oauth2Store = new Oauth2Store();
return resolve(oauth2Store.getOauth2DataOfCollection(uid).credentials ?? {});
} catch (err) {
reject(new Error('Could not read cached oauth2 credentials'));
}
});
});
ipcMain.handle('cancel-http-request', async (event, cancelTokenUid) => {
return new Promise((resolve, reject) => {
if (cancelTokenUid && cancelTokens[cancelTokenUid]) {

View File

@@ -170,14 +170,6 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
request.oauth2.clientId = clientId;
request.oauth2.clientSecret = clientSecret;
request.oauth2.scope = scope;
request.data = {
grant_type: 'password',
username,
password,
client_id: clientId,
client_secret: clientSecret,
scope
};
break;
case 'authorization_code':
request.oauth2.callbackUrl = _interpolate(request.oauth2.callbackUrl) || '';
@@ -197,12 +189,6 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
request.oauth2.clientId = clientId;
request.oauth2.clientSecret = clientSecret;
request.oauth2.scope = scope;
request.data = {
grant_type: 'client_credentials',
client_id: clientId,
client_secret: clientSecret,
scope
};
break;
default:
break;

View File

@@ -2,6 +2,9 @@ const { get, cloneDeep } = require('lodash');
const crypto = require('crypto');
const { authorizeUserInWindow } = require('./authorize-user-in-window');
const Oauth2Store = require('../../store/oauth2');
const { makeAxiosInstance } = require('./axios-instance');
const oauth2Store = new Oauth2Store();
const generateCodeVerifier = () => {
return crypto.randomBytes(22).toString('hex');
@@ -14,16 +17,34 @@ const generateCodeChallenge = (codeVerifier) => {
return base64Hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
};
const getPersistedOauth2Credentials = (collectionUid) => {
const collectionOauthStore = oauth2Store.getOauth2DataOfCollection(collectionUid);
const cachedCredentials = collectionOauthStore.credentials;
return { cachedCredentials };
};
const persistOauth2Credentials = (credentials, collectionUid) => {
const collectionOauthStore = oauth2Store.getOauth2DataOfCollection(collectionUid);
collectionOauthStore.credentials = credentials;
oauth2Store.updateOauth2DataOfCollection(collectionUid, collectionOauthStore);
};
// AUTHORIZATION CODE
const resolveOAuth2AuthorizationCodeAccessToken = async (request, collectionUid) => {
const oauth2AuthorizeWithAuthorizationCode = async (request, collectionUid) => {
const { cachedCredentials } = getPersistedOauth2Credentials(collectionUid);
if (cachedCredentials?.access_token) {
console.log('Reusing Stored access token');
return { credentials: cachedCredentials, response: {} };
}
let codeVerifier = generateCodeVerifier();
let codeChallenge = generateCodeChallenge(codeVerifier);
let requestCopy = cloneDeep(request);
const { authorizationCode } = await getOAuth2AuthorizationCode(requestCopy, codeChallenge, collectionUid);
const oAuth = get(requestCopy, 'oauth2', {});
const { clientId, clientSecret, callbackUrl, scope, pkce } = oAuth;
const { clientId, clientSecret, callbackUrl, pkce } = oAuth;
const data = {
grant_type: 'authorization_code',
code: authorizationCode,
@@ -35,11 +56,16 @@ const resolveOAuth2AuthorizationCodeAccessToken = async (request, collectionUid)
data['code_verifier'] = codeVerifier;
}
const url = requestCopy?.oauth2?.accessTokenUrl;
return {
data,
url
};
request.method = 'POST';
request.headers['content-type'] = 'application/x-www-form-urlencoded';
request.data = data;
request.url = request?.oauth2?.accessTokenUrl;
const axiosInstance = makeAxiosInstance();
const response = await axiosInstance(request);
const credentials = JSON.parse(response.data);
persistOauth2Credentials(credentials, collectionUid);
return { credentials, response };
};
const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => {
@@ -64,7 +90,6 @@ const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => {
authorizationUrlWithQueryParams.searchParams.append('state', state);
}
try {
const oauth2Store = new Oauth2Store();
const { authorizationCode } = await authorizeUserInWindow({
authorizeUrl: authorizationUrlWithQueryParams.toString(),
callbackUrl,
@@ -79,7 +104,13 @@ const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => {
// CLIENT CREDENTIALS
const transformClientCredentialsRequest = async (request) => {
const oauth2AuthorizeWithClientCredentials = async (request, collectionUid) => {
const { cachedCredentials } = getPersistedOauth2Credentials(collectionUid);
if (cachedCredentials?.access_token) {
console.log('Reusing Stored access token');
return { credentials: cachedCredentials, response: {} };
}
let requestCopy = cloneDeep(request);
const oAuth = get(requestCopy, 'oauth2', {});
const { clientId, clientSecret, scope } = oAuth;
@@ -91,18 +122,29 @@ const transformClientCredentialsRequest = async (request) => {
if (scope) {
data.scope = scope;
}
const url = requestCopy?.oauth2?.accessTokenUrl;
return {
data,
url
};
request.method = 'POST';
request.headers['content-type'] = 'application/x-www-form-urlencoded';
request.data = data;
request.url = request?.oauth2?.accessTokenUrl;
const axiosInstance = makeAxiosInstance();
let response = await axiosInstance(request);
let credentials = JSON.parse(response.data);
persistOauth2Credentials(credentials, collectionUid);
return { credentials, response };
};
// PASSWORD CREDENTIALS
const transformPasswordCredentialsRequest = async (request) => {
let requestCopy = cloneDeep(request);
const oAuth = get(requestCopy, 'oauth2', {});
const oauth2AuthorizeWithPasswordCredentials = async (request, collectionUid) => {
const { cachedCredentials } = getPersistedOauth2Credentials(collectionUid);
if (cachedCredentials?.access_token) {
console.log('Reusing Stored access token');
return { credentials: cachedCredentials, response: {} };
}
const oAuth = get(request, 'oauth2', {});
const { username, password, clientId, clientSecret, scope } = oAuth;
const data = {
grant_type: 'password',
@@ -114,16 +156,20 @@ const transformPasswordCredentialsRequest = async (request) => {
if (scope) {
data.scope = scope;
}
const url = requestCopy?.oauth2?.accessTokenUrl;
return {
data,
url
};
};
module.exports = {
resolveOAuth2AuthorizationCodeAccessToken,
getOAuth2AuthorizationCode,
transformClientCredentialsRequest,
transformPasswordCredentialsRequest
request.method = 'POST';
request.headers['content-type'] = 'application/x-www-form-urlencoded';
request.data = data;
request.url = request?.oauth2?.accessTokenUrl;
const axiosInstance = makeAxiosInstance();
let response = await axiosInstance(request);
let credentials = JSON.parse(response.data);
persistOauth2Credentials(credentials, collectionUid);
return { credentials, response };
};
module.exports = {
oauth2AuthorizeWithAuthorizationCode,
oauth2AuthorizeWithClientCredentials,
oauth2AuthorizeWithPasswordCredentials
};

View File

@@ -59,6 +59,9 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
axiosRequest.apiKeyAuthValueForQueryParams = apiKeyAuth;
}
break;
case 'oauth2':
request.auth = collectionAuth;
break;
}
}
@@ -194,7 +197,7 @@ const prepareRequest = (item, collection) => {
});
let axiosRequest = {
mode: request.body.mode,
mode: request?.body?.mode,
method: request.method,
url,
headers,
@@ -204,7 +207,7 @@ const prepareRequest = (item, collection) => {
axiosRequest = setAuthHeaders(axiosRequest, request, collectionRoot);
if (request.body.mode === 'json') {
if (request.body?.mode === 'json') {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'application/json';
}
@@ -215,28 +218,28 @@ const prepareRequest = (item, collection) => {
}
}
if (request.body.mode === 'text') {
if (request.body?.mode === 'text') {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'text/plain';
}
axiosRequest.data = request.body.text;
}
if (request.body.mode === 'xml') {
if (request.body?.mode === 'xml') {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'application/xml';
}
axiosRequest.data = request.body.xml;
}
if (request.body.mode === 'sparql') {
if (request.body?.mode === 'sparql') {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'application/sparql-query';
}
axiosRequest.data = request.body.sparql;
}
if (request.body.mode === 'formUrlEncoded') {
if (request.body?.mode === 'formUrlEncoded') {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded';
}
@@ -252,7 +255,7 @@ const prepareRequest = (item, collection) => {
axiosRequest.data = enabledParams;
}
if (request.body.mode === 'graphql') {
if (request.body?.mode === 'graphql') {
const graphqlQuery = {
query: get(request, 'body.graphql.query'),
// https://github.com/usebruno/bruno/issues/884 - we must only parse the variables after the variable interpolation

View File

@@ -85,6 +85,7 @@ class Oauth2Store {
let oauth2DataForCollection = this.getOauth2DataOfCollection(collectionUid);
delete oauth2DataForCollection.sessionId;
delete oauth2DataForCollection.credentials;
let updatedOauth2Data = oauth2Data.filter((d) => d.collectionUid !== collectionUid);
updatedOauth2Data.push({ ...oauth2DataForCollection });

View File

@@ -6,7 +6,8 @@ class BrunoRequest {
* - req.headers
* - req.timeout
* - req.body
*
* - req.credentials
*
* Above shorthands are useful for accessing the request properties directly in the scripts
* It must be noted that the user cannot set these properties directly.
* They should use the respective setter methods to set these properties.
@@ -17,13 +18,14 @@ class BrunoRequest {
this.method = req.method;
this.headers = req.headers;
this.timeout = req.timeout;
this.credentials = req.credentials;
/**
* We automatically parse the JSON body if the content type is JSON
* This is to make it easier for the user to access the body directly
*
*
* It must be noted that the request data is always a string and is what gets sent over the network
* If the user wants to access the raw data, they can use getBody({raw: true}) method
* If the user wants to access the raw data, they can use getBody({raw: true}) method
*/
const isJson = this.hasJSONContentType(this.req.headers);
if (isJson) {
@@ -84,6 +86,10 @@ class BrunoRequest {
this.req.headers[name] = value;
}
getCredentials() {
return this.credentials;
}
hasJSONContentType(headers) {
const contentType = headers?.['Content-Type'] || headers?.['content-type'] || '';
return contentType.includes('json');
@@ -91,7 +97,7 @@ class BrunoRequest {
/**
* Get the body of the request
*
*
* We automatically parse and return the JSON body if the content type is JSON
* If the user wants the raw body, they can pass the raw option as true
*/
@@ -115,7 +121,7 @@ class BrunoRequest {
* Otherwise
* - We set the request data as the data itself
* - We set the body property as the data itself
*
*
* If the user wants to override this behavior, they can pass the raw option as true
*/
setBody(data, options = {}) {
@@ -168,7 +174,7 @@ class BrunoRequest {
__isObject(obj) {
return obj !== null && typeof obj === 'object';
}
disableParsingResponseJson() {
this.req.__brunoDisableParsingResponseJson = true;

View File

@@ -8,12 +8,14 @@ const addBrunoRequestShimToContext = (vm, req) => {
const headers = marshallToVm(req.getHeaders(), vm);
const body = marshallToVm(req.getBody(), vm);
const timeout = marshallToVm(req.getTimeout(), vm);
const credentials = marshallToVm(req.getCredentials(), vm);
vm.setProp(reqObject, 'url', url);
vm.setProp(reqObject, 'method', method);
vm.setProp(reqObject, 'headers', headers);
vm.setProp(reqObject, 'body', body);
vm.setProp(reqObject, 'timeout', timeout);
vm.setProp(reqObject, 'credentials', credentials);
url.dispose();
method.dispose();