mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-24 05:05:39 +00:00
Merge branch 'pietrygamat/inherit-oauth' into HEAD
This commit is contained in:
@@ -86,7 +86,7 @@ const AuthMode = ({ collection }) => {
|
||||
onModeChange('oauth2');
|
||||
}}
|
||||
>
|
||||
Oauth2
|
||||
OAuth 2.0
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user