mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-21 15:33:11 +01:00
feat(#1003): collection level oauth2, access_token_url & scope for 'Client Credentials' and 'Password Credentials' grant types (#1691)
* feat(#1003): authorization_code grant type PKCE support, code cleanup.. --------- Co-authored-by: lohit-1 <lohit@usebruno.com>
This commit is contained in:
parent
9d3df0a86a
commit
858536e13d
@ -43,6 +43,7 @@ if (!SERVER_RENDERED) {
|
||||
'req.getUrl()',
|
||||
'req.setUrl(url)',
|
||||
'req.getMethod()',
|
||||
'req.getAuthMode()',
|
||||
'req.setMethod(method)',
|
||||
'req.getHeader(name)',
|
||||
'req.getHeaders()',
|
||||
|
@ -70,6 +70,15 @@ const AuthMode = ({ collection }) => {
|
||||
>
|
||||
Digest Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('oauth2');
|
||||
}}
|
||||
>
|
||||
Oauth2
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
|
@ -0,0 +1,100 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
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';
|
||||
|
||||
const OAuth2AuthorizationCode = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const oAuth = get(collection, 'root.request.auth.oauth2', {});
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendCollectionOauth2Request(collection.uid));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, pkce } = oAuth;
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType: 'authorization_code',
|
||||
callbackUrl,
|
||||
authorizationUrl,
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
pkce,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handlePKCEToggle = (e) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType: 'authorization_code',
|
||||
callbackUrl,
|
||||
authorizationUrl,
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
pkce: !Boolean(oAuth?.['pkce'])
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex flex-row w-full gap-4" key="pkce">
|
||||
<label className="block font-medium">Use PKCE</label>
|
||||
<input
|
||||
className="cursor-pointer"
|
||||
type="checkbox"
|
||||
checked={Boolean(oAuth?.['pkce'])}
|
||||
onChange={handlePKCEToggle}
|
||||
/>
|
||||
</div>
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2AuthorizationCode;
|
@ -0,0 +1,28 @@
|
||||
const inputsConfig = [
|
||||
{
|
||||
key: 'callbackUrl',
|
||||
label: 'Callback URL'
|
||||
},
|
||||
{
|
||||
key: 'authorizationUrl',
|
||||
label: 'Authorization URL'
|
||||
},
|
||||
{
|
||||
key: 'accessTokenUrl',
|
||||
label: 'Access Token URL'
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
label: 'Client ID'
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret'
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
label: 'Scope'
|
||||
}
|
||||
];
|
||||
|
||||
export { inputsConfig };
|
@ -0,0 +1,16 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.single-line-editor-wrapper {
|
||||
max-width: 400px;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
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';
|
||||
|
||||
const OAuth2ClientCredentials = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const oAuth = get(collection, 'root.request.auth.oauth2', {});
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendCollectionOauth2Request(collection.uid));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
const { accessTokenUrl, clientId, clientSecret, scope } = oAuth;
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType: 'client_credentials',
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2ClientCredentials;
|
@ -0,0 +1,20 @@
|
||||
const inputsConfig = [
|
||||
{
|
||||
key: 'accessTokenUrl',
|
||||
label: 'Access Token URL'
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
label: 'Client ID'
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret'
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
label: 'Scope'
|
||||
}
|
||||
];
|
||||
|
||||
export { inputsConfig };
|
@ -0,0 +1,54 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
font-size: 0.8125rem;
|
||||
|
||||
.grant-type-mode-selector {
|
||||
padding: 0.5rem 0px;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
|
||||
.dropdown {
|
||||
width: fit-content;
|
||||
|
||||
div[data-tippy-root] {
|
||||
width: fit-content;
|
||||
}
|
||||
.tippy-box {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
|
||||
.tippy-content: {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grant-type-label {
|
||||
width: fit-content;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
justify-content: space-between;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
|
||||
.label-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140 140 140);
|
||||
}
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
@ -0,0 +1,98 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { humanizeGrantType } from 'utils/collections';
|
||||
import { useEffect } from 'react';
|
||||
import { updateCollectionAuth, updateCollectionAuthMode } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const GrantTypeSelector = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const oAuth = get(collection, 'root.request.auth.oauth2', {});
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end grant-type-label select-none">
|
||||
{humanizeGrantType(oAuth?.grantType)} <IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const onGrantTypeChange = (grantType) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// initalize redux state with a default oauth2 grant type
|
||||
// authorization_code - default option
|
||||
!oAuth?.grantType &&
|
||||
dispatch(
|
||||
updateCollectionAuthMode({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
!oAuth?.grantType &&
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType: 'authorization_code'
|
||||
}
|
||||
})
|
||||
);
|
||||
}, [oAuth]);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<label className="block font-medium mb-2">Grant Type</label>
|
||||
<div className="inline-flex items-center cursor-pointer grant-type-mode-selector w-fit">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onGrantTypeChange('password');
|
||||
}}
|
||||
>
|
||||
Password Credentials
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onGrantTypeChange('authorization_code');
|
||||
}}
|
||||
>
|
||||
Authorization Code
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onGrantTypeChange('client_credentials');
|
||||
}}
|
||||
>
|
||||
Client Credentials
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default GrantTypeSelector;
|
@ -0,0 +1,16 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.single-line-editor-wrapper {
|
||||
max-width: 400px;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
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';
|
||||
|
||||
const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const oAuth = get(collection, 'root.request.auth.oauth2', {});
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendCollectionOauth2Request(collection.uid));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
const { accessTokenUrl, username, password, scope } = oAuth;
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType: 'password',
|
||||
accessTokenUrl,
|
||||
username,
|
||||
password,
|
||||
scope,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2AuthorizationCode;
|
@ -0,0 +1,20 @@
|
||||
const inputsConfig = [
|
||||
{
|
||||
key: 'accessTokenUrl',
|
||||
label: 'Access Token URL'
|
||||
},
|
||||
{
|
||||
key: 'username',
|
||||
label: 'Username'
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
label: 'Password'
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
label: 'Scope'
|
||||
}
|
||||
];
|
||||
|
||||
export { inputsConfig };
|
@ -0,0 +1,16 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.single-line-editor-wrapper {
|
||||
max-width: 400px;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import GrantTypeSelector from './GrantTypeSelector/index';
|
||||
import OAuth2PasswordCredentials from './PasswordCredentials/index';
|
||||
import OAuth2AuthorizationCode from './AuthorizationCode/index';
|
||||
import OAuth2ClientCredentials from './ClientCredentials/index';
|
||||
|
||||
const grantTypeComponentMap = (grantType, collection) => {
|
||||
switch (grantType) {
|
||||
case 'password':
|
||||
return <OAuth2PasswordCredentials collection={collection} />;
|
||||
break;
|
||||
case 'authorization_code':
|
||||
return <OAuth2AuthorizationCode collection={collection} />;
|
||||
break;
|
||||
case 'client_credentials':
|
||||
return <OAuth2ClientCredentials collection={collection} />;
|
||||
break;
|
||||
default:
|
||||
return <div>TBD</div>;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const OAuth2 = ({ collection }) => {
|
||||
const oAuth = get(collection, 'root.request.auth.oauth2', {});
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<GrantTypeSelector collection={collection} />
|
||||
{grantTypeComponentMap(oAuth?.grantType, collection)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2;
|
@ -8,6 +8,7 @@ import BasicAuth from './BasicAuth';
|
||||
import DigestAuth from './DigestAuth';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import OAuth2 from './OAuth2';
|
||||
|
||||
const Auth = ({ collection }) => {
|
||||
const authMode = get(collection, 'root.request.auth.mode');
|
||||
@ -29,6 +30,9 @@ const Auth = ({ collection }) => {
|
||||
case 'digest': {
|
||||
return <DigestAuth collection={collection} />;
|
||||
}
|
||||
case 'oauth2': {
|
||||
return <OAuth2 collection={collection} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -38,7 +42,6 @@ const Auth = ({ collection }) => {
|
||||
<AuthMode collection={collection} />
|
||||
</div>
|
||||
{getAuthView()}
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
|
@ -81,71 +81,72 @@ const EnvironmentVariables = ({ environment, collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full mt-6 mb-6">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Enabled</td>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td>Secret</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{formik.values.map((variable, index) => (
|
||||
<tr key={variable.uid}>
|
||||
<td className="text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-3 mousetrap"
|
||||
name={`${index}.enabled`}
|
||||
checked={variable.enabled}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
className="mousetrap"
|
||||
id={`${index}.name`}
|
||||
name={`${index}.name`}
|
||||
value={variable.name}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<ErrorMessage name={`${index}.name`} />
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
theme={storedTheme}
|
||||
collection={collection}
|
||||
name={`${index}.value`}
|
||||
value={variable.value}
|
||||
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-3 mousetrap"
|
||||
name={`${index}.secret`}
|
||||
checked={variable.secret}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<button onClick={() => handleRemoveVar(variable.uid)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</td>
|
||||
<div className="h-[50vh] overflow-y-auto w-full">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Enabled</td>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td>Secret</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</thead>
|
||||
<tbody>
|
||||
{formik.values.map((variable, index) => (
|
||||
<tr key={variable.uid}>
|
||||
<td className="text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-3 mousetrap"
|
||||
name={`${index}.enabled`}
|
||||
checked={variable.enabled}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
className="mousetrap"
|
||||
id={`${index}.name`}
|
||||
name={`${index}.name`}
|
||||
value={variable.name}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<ErrorMessage name={`${index}.name`} />
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
theme={storedTheme}
|
||||
collection={collection}
|
||||
name={`${index}.value`}
|
||||
value={variable.value}
|
||||
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-3 mousetrap"
|
||||
name={`${index}.secret`}
|
||||
checked={variable.secret}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<button onClick={() => handleRemoveVar(variable.uid)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={addVariable}>
|
||||
+ Add Variable
|
||||
|
@ -20,6 +20,8 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, pkce } = oAuth;
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
@ -28,13 +30,39 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
grantType: 'authorization_code',
|
||||
...oAuth,
|
||||
callbackUrl,
|
||||
authorizationUrl,
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
pkce,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handlePKCEToggle = (e) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
grantType: 'authorization_code',
|
||||
callbackUrl,
|
||||
authorizationUrl,
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
pkce: !Boolean(oAuth?.['pkce'])
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
@ -48,13 +76,22 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={() => {}}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex flex-row w-full gap-4" key="pkce">
|
||||
<label className="block font-medium">Use PKCE</label>
|
||||
<input
|
||||
className="cursor-pointer"
|
||||
type="checkbox"
|
||||
checked={Boolean(oAuth?.['pkce'])}
|
||||
onChange={handlePKCEToggle}
|
||||
/>
|
||||
</div>
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
|
@ -5,7 +5,7 @@ const inputsConfig = [
|
||||
},
|
||||
{
|
||||
key: 'authorizationUrl',
|
||||
label: 'Auth URL'
|
||||
label: 'Authorization URL'
|
||||
},
|
||||
{
|
||||
key: 'accessTokenUrl',
|
||||
|
@ -4,8 +4,9 @@ import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { inputsConfig } from './inputsConfig';
|
||||
|
||||
const OAuth2ClientCredentials = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@ -13,25 +14,15 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
|
||||
|
||||
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleClientIdChange = (clientId) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
grantType: 'client_credentials',
|
||||
clientId: clientId,
|
||||
clientSecret: oAuth.clientSecret
|
||||
}
|
||||
})
|
||||
);
|
||||
const handleRun = async () => {
|
||||
dispatch(sendRequest(item, collection.uid));
|
||||
};
|
||||
|
||||
const handleClientSecretChange = (clientSecret) => {
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const { accessTokenUrl, clientId, clientSecret, scope } = oAuth;
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'oauth2',
|
||||
@ -39,38 +30,39 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
grantType: 'client_credentials',
|
||||
clientId: oAuth.clientId,
|
||||
clientSecret: clientSecret
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Client Id</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={oAuth.clientId || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleClientIdChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Client Secret</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={oAuth.clientSecret || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleClientSecretChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,20 @@
|
||||
const inputsConfig = [
|
||||
{
|
||||
key: 'accessTokenUrl',
|
||||
label: 'Access Token URL'
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
label: 'Client ID'
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret'
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
label: 'Scope'
|
||||
}
|
||||
];
|
||||
|
||||
export { inputsConfig };
|
@ -37,7 +37,7 @@ const GrantTypeSelector = ({ item, collection }) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// initalize redux state with a default oauth2 auth type
|
||||
// initalize redux state with a default oauth2 grant type
|
||||
// authorization_code - default option
|
||||
!oAuth?.grantType &&
|
||||
dispatch(
|
||||
@ -64,7 +64,7 @@ const GrantTypeSelector = ({ item, collection }) => {
|
||||
onGrantTypeChange('password');
|
||||
}}
|
||||
>
|
||||
Resource Owner Password Credentials
|
||||
Password Credentials
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
|
@ -0,0 +1,16 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.single-line-editor-wrapper {
|
||||
max-width: 400px;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { inputsConfig } from './inputsConfig';
|
||||
|
||||
const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendRequest(item, collection.uid));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const { accessTokenUrl, username, password, scope } = oAuth;
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
grantType: 'password',
|
||||
accessTokenUrl,
|
||||
username,
|
||||
password,
|
||||
scope,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2AuthorizationCode;
|
@ -0,0 +1,20 @@
|
||||
const inputsConfig = [
|
||||
{
|
||||
key: 'accessTokenUrl',
|
||||
label: 'Access Token URL'
|
||||
},
|
||||
{
|
||||
key: 'username',
|
||||
label: 'Username'
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
label: 'Password'
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
label: 'Scope'
|
||||
}
|
||||
];
|
||||
|
||||
export { inputsConfig };
|
@ -1,78 +0,0 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const OAuth2Ropc = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleUsernameChange = (username) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
grantType: 'password',
|
||||
username: username,
|
||||
password: oAuth.password
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handlePasswordChange = (password) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
grantType: 'password',
|
||||
username: oAuth.username,
|
||||
password: password
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={oAuth.username || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleUsernameChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={oAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2Ropc;
|
@ -2,14 +2,14 @@ import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import GrantTypeSelector from './GrantTypeSelector/index';
|
||||
import OAuth2Ropc from './Ropc/index';
|
||||
import OAuth2PasswordCredentials from './PasswordCredentials/index';
|
||||
import OAuth2AuthorizationCode from './AuthorizationCode/index';
|
||||
import OAuth2ClientCredentials from './ClientCredentials/index';
|
||||
|
||||
const grantTypeComponentMap = (grantType, item, collection) => {
|
||||
switch (grantType) {
|
||||
case 'password':
|
||||
return <OAuth2Ropc item={item} collection={collection} />;
|
||||
return <OAuth2PasswordCredentials item={item} collection={collection} />;
|
||||
break;
|
||||
case 'authorization_code':
|
||||
return <OAuth2AuthorizationCode item={item} collection={collection} />;
|
||||
|
@ -35,8 +35,20 @@ const Auth = ({ item, collection }) => {
|
||||
case 'inherit': {
|
||||
return (
|
||||
<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>
|
||||
{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">Cannot inherit Oauth2 from collection.</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div>Auth inherited from the Collection: </div>
|
||||
<div className="inherit-mode-text">{humanizeRequestAuthMode(collectionAuth?.mode)}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ import { each } from 'lodash';
|
||||
import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { resolveRequestFilename } from 'utils/common/platform';
|
||||
import { parseQueryParams, splitOnFirst } from 'utils/url/index';
|
||||
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
|
||||
|
||||
export const renameCollection = (newName, collectionUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
@ -138,6 +139,35 @@ export const saveCollectionRoot = (collectionUid) => (dispatch, getState) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const sendCollectionOauth2Request = (collectionUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!collection) {
|
||||
return reject(new Error('Collection not found'));
|
||||
}
|
||||
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
|
||||
const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid);
|
||||
|
||||
_sendCollectionOauth2Request(collection, environment, collectionCopy.collectionVariables)
|
||||
.then((response) => {
|
||||
if (response?.data?.error) {
|
||||
toast.error(response?.data?.error);
|
||||
} else {
|
||||
toast.success('Request made successfully');
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
@ -147,7 +177,7 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
|
||||
return reject(new Error('Collection not found'));
|
||||
}
|
||||
|
||||
const itemCopy = cloneDeep(item);
|
||||
const itemCopy = cloneDeep(item || {});
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
|
||||
const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid);
|
||||
|
@ -678,6 +678,7 @@ export const collectionsSlice = createSlice({
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
item.draft.request.auth = {};
|
||||
item.draft.request.auth.mode = action.payload.mode;
|
||||
}
|
||||
}
|
||||
@ -978,6 +979,7 @@ export const collectionsSlice = createSlice({
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
set(collection, 'root.request.auth', {});
|
||||
set(collection, 'root.request.auth.mode', action.payload.mode);
|
||||
}
|
||||
},
|
||||
@ -985,6 +987,8 @@ export const collectionsSlice = createSlice({
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
set(collection, 'root.request.auth', {});
|
||||
set(collection, 'root.request.auth.mode', action.payload.mode);
|
||||
switch (action.payload.mode) {
|
||||
case 'awsv4':
|
||||
set(collection, 'root.request.auth.awsv4', action.payload.content);
|
||||
@ -998,6 +1002,9 @@ export const collectionsSlice = createSlice({
|
||||
case 'digest':
|
||||
set(collection, 'root.request.auth.digest', action.payload.content);
|
||||
break;
|
||||
case 'oauth2':
|
||||
set(collection, 'root.request.auth.oauth2', action.payload.content);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -522,7 +522,7 @@ export const humanizeGrantType = (mode) => {
|
||||
let label = 'No Auth';
|
||||
switch (mode) {
|
||||
case 'password': {
|
||||
label = 'Resource Owner Password Credentials';
|
||||
label = 'Password Credentials';
|
||||
break;
|
||||
}
|
||||
case 'authorization_code': {
|
||||
|
@ -33,6 +33,16 @@ const sendHttpRequest = async (item, collection, environment, collectionVariable
|
||||
});
|
||||
};
|
||||
|
||||
export const sendCollectionOauth2Request = async (collection, environment, collectionVariables) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer
|
||||
.invoke('send-collection-oauth2-request', collection, environment, collectionVariables)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchGqlSchema = async (endpoint, environment, request, collection) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
|
@ -105,7 +105,7 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
|
||||
const password = _interpolate(request.auth.password) || '';
|
||||
|
||||
// use auth header based approach and delete the request.auth object
|
||||
request.headers['authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
||||
request.headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
||||
delete request.auth;
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@ const prepareRequest = (request, collectionRoot) => {
|
||||
}
|
||||
|
||||
if (collectionAuth.mode === 'bearer') {
|
||||
axiosRequest.headers['authorization'] = `Bearer ${get(collectionAuth, 'bearer.token')}`;
|
||||
axiosRequest.headers['Authorization'] = `Bearer ${get(collectionAuth, 'bearer.token')}`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,7 +76,7 @@ const prepareRequest = (request, collectionRoot) => {
|
||||
}
|
||||
|
||||
if (request.auth.mode === 'bearer') {
|
||||
axiosRequest.headers['authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`;
|
||||
axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl }) => {
|
||||
const callbackUrlWithCode = new URL(finalUrl);
|
||||
const authorizationCode = callbackUrlWithCode.searchParams.get('code');
|
||||
|
||||
return resolve(authorizationCode);
|
||||
return resolve({ authorizationCode });
|
||||
} catch (error) {
|
||||
return reject(error);
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ const { ipcMain } = require('electron');
|
||||
const { isUndefined, isNull, each, get, compact, cloneDeep } = 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');
|
||||
@ -29,7 +30,11 @@ const { addDigestInterceptor } = require('./digestauth-helper');
|
||||
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../../utils/proxy-util');
|
||||
const { chooseFileToSave, writeBinaryFile } = require('../../utils/filesystem');
|
||||
const { getCookieStringForUrl, addCookieToJar, getDomainsWithCookies } = require('../../utils/cookies');
|
||||
const { resolveOAuth2AuthorizationCodecessToken } = require('./oauth2-authorization-code-helper');
|
||||
const {
|
||||
resolveOAuth2AuthorizationCodeAccessToken,
|
||||
transformClientCredentialsRequest,
|
||||
transformPasswordCredentialsRequest
|
||||
} = require('./oauth2-helper');
|
||||
|
||||
// override the default escape function to prevent escaping
|
||||
Mustache.escape = function (value) {
|
||||
@ -191,12 +196,30 @@ const configureRequest = async (
|
||||
const axiosInstance = makeAxiosInstance();
|
||||
|
||||
if (request.oauth2) {
|
||||
if (request?.oauth2?.grantType == 'authorization_code') {
|
||||
let requestCopy = cloneDeep(request);
|
||||
interpolateVars(requestCopy, envVars, collectionVariables, processEnvVars);
|
||||
const { data, url } = await resolveOAuth2AuthorizationCodecessToken(requestCopy);
|
||||
request.data = data;
|
||||
request.url = url;
|
||||
let requestCopy = cloneDeep(request);
|
||||
switch (request?.oauth2?.grantType) {
|
||||
case 'authorization_code':
|
||||
interpolateVars(requestCopy, envVars, collectionVariables, processEnvVars);
|
||||
const { data: authorizationCodeData, url: authorizationCodeAccessTokenUrl } =
|
||||
await resolveOAuth2AuthorizationCodeAccessToken(requestCopy);
|
||||
request.data = authorizationCodeData;
|
||||
request.url = authorizationCodeAccessTokenUrl;
|
||||
break;
|
||||
case 'client_credentials':
|
||||
interpolateVars(requestCopy, envVars, collectionVariables, processEnvVars);
|
||||
const { data: clientCredentialsData, url: clientCredentialsAccessTokenUrl } =
|
||||
await transformClientCredentialsRequest(requestCopy);
|
||||
request.data = clientCredentialsData;
|
||||
request.url = clientCredentialsAccessTokenUrl;
|
||||
break;
|
||||
case 'password':
|
||||
interpolateVars(requestCopy, envVars, collectionVariables, processEnvVars);
|
||||
const { data: passwordData, url: passwordAccessTokenUrl } = await transformPasswordCredentialsRequest(
|
||||
requestCopy
|
||||
);
|
||||
request.data = passwordData;
|
||||
request.url = passwordAccessTokenUrl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -219,7 +242,6 @@ const configureRequest = async (
|
||||
request.headers['cookie'] = cookieString;
|
||||
}
|
||||
}
|
||||
|
||||
return axiosInstance;
|
||||
};
|
||||
|
||||
@ -594,6 +616,79 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('send-collection-oauth2-request', async (event, collection, environment, collectionVariables) => {
|
||||
try {
|
||||
const collectionUid = collection.uid;
|
||||
const collectionPath = collection.pathname;
|
||||
const requestUid = uuid();
|
||||
|
||||
const collectionRoot = get(collection, 'root', {});
|
||||
const _request = collectionRoot?.request;
|
||||
const request = prepareCollectionRequest(_request, collectionRoot, collectionPath);
|
||||
const envVars = getEnvVars(environment);
|
||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||
const brunoConfig = getBrunoConfig(collectionUid);
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
|
||||
await runPreRequest(
|
||||
request,
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collectionRoot,
|
||||
collectionUid,
|
||||
collectionVariables,
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
);
|
||||
|
||||
interpolateVars(request, envVars, collection.collectionVariables, processEnvVars);
|
||||
const axiosInstance = await configureRequest(
|
||||
collection.uid,
|
||||
request,
|
||||
envVars,
|
||||
collection.collectionVariables,
|
||||
processEnvVars,
|
||||
collectionPath
|
||||
);
|
||||
|
||||
try {
|
||||
response = await axiosInstance(request);
|
||||
} catch (error) {
|
||||
if (error?.response) {
|
||||
response = error.response;
|
||||
} else {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
const { data } = parseDataFromResponse(response);
|
||||
response.data = data;
|
||||
|
||||
await runPostResponse(
|
||||
request,
|
||||
response,
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collectionRoot,
|
||||
collectionUid,
|
||||
collectionVariables,
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
);
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
data: response.data
|
||||
};
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('cancel-http-request', async (event, cancelTokenUid) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (cancelTokenUid && cancelTokens[cancelTokenUid]) {
|
||||
|
@ -104,21 +104,26 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
|
||||
const username = _interpolate(request.auth.username) || '';
|
||||
const password = _interpolate(request.auth.password) || '';
|
||||
// use auth header based approach and delete the request.auth object
|
||||
request.headers['authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
||||
request.headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
||||
delete request.auth;
|
||||
}
|
||||
|
||||
if (request?.oauth2?.grantType) {
|
||||
let username, password, scope, clientId, clientSecret;
|
||||
switch (request.oauth2.grantType) {
|
||||
case 'password':
|
||||
let username = _interpolate(request.oauth2.username) || '';
|
||||
let password = _interpolate(request.oauth2.password) || '';
|
||||
username = _interpolate(request.oauth2.username) || '';
|
||||
password = _interpolate(request.oauth2.password) || '';
|
||||
scope = _interpolate(request.oauth2.scope) || '';
|
||||
request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || '';
|
||||
request.oauth2.username = username;
|
||||
request.oauth2.password = password;
|
||||
request.oauth2.scope = scope;
|
||||
request.data = {
|
||||
grant_type: 'password',
|
||||
username,
|
||||
password
|
||||
password,
|
||||
scope
|
||||
};
|
||||
break;
|
||||
case 'authorization_code':
|
||||
@ -128,16 +133,21 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
|
||||
request.oauth2.clientId = _interpolate(request.oauth2.clientId) || '';
|
||||
request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || '';
|
||||
request.oauth2.scope = _interpolate(request.oauth2.scope) || '';
|
||||
request.oauth2.pkce = _interpolate(request.oauth2.pkce) || false;
|
||||
break;
|
||||
case 'client_credentials':
|
||||
let clientId = _interpolate(request.oauth2.clientId) || '';
|
||||
let clientSecret = _interpolate(request.oauth2.clientSecret) || '';
|
||||
clientId = _interpolate(request.oauth2.clientId) || '';
|
||||
clientSecret = _interpolate(request.oauth2.clientSecret) || '';
|
||||
scope = _interpolate(request.oauth2.scope) || '';
|
||||
request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || '';
|
||||
request.oauth2.clientId = clientId;
|
||||
request.oauth2.clientSecret = clientSecret;
|
||||
request.oauth2.scope = scope;
|
||||
request.data = {
|
||||
grant_type: 'client_credentials',
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret
|
||||
client_secret: clientSecret,
|
||||
scope
|
||||
};
|
||||
break;
|
||||
default:
|
||||
|
@ -1,41 +0,0 @@
|
||||
const { get, cloneDeep } = require('lodash');
|
||||
const { authorizeUserInWindow } = require('./authorize-user-in-window');
|
||||
|
||||
const resolveOAuth2AuthorizationCodecessToken = async (request) => {
|
||||
let requestCopy = cloneDeep(request);
|
||||
const authorization_code = await getOAuth2AuthorizationCode(requestCopy);
|
||||
const oAuth = get(requestCopy, 'oauth2', {});
|
||||
const { clientId, clientSecret, callbackUrl, scope } = oAuth;
|
||||
const data = {
|
||||
grant_type: 'authorization_code',
|
||||
code: authorization_code,
|
||||
redirect_uri: callbackUrl,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
scope: scope
|
||||
};
|
||||
const url = requestCopy?.oauth2?.accessTokenUrl;
|
||||
return {
|
||||
data,
|
||||
url
|
||||
};
|
||||
};
|
||||
|
||||
const getOAuth2AuthorizationCode = (request) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const { oauth2 } = request;
|
||||
const { callbackUrl, clientId, authorizationUrl, scope } = oauth2;
|
||||
const authorizationUrlWithQueryParams = `${authorizationUrl}?client_id=${clientId}&redirect_uri=${callbackUrl}&response_type=code&scope=${scope}`;
|
||||
try {
|
||||
const code = await authorizeUserInWindow({ authorizeUrl: authorizationUrlWithQueryParams, callbackUrl });
|
||||
resolve(code);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
resolveOAuth2AuthorizationCodecessToken,
|
||||
getOAuth2AuthorizationCode
|
||||
};
|
109
packages/bruno-electron/src/ipc/network/oauth2-helper.js
Normal file
109
packages/bruno-electron/src/ipc/network/oauth2-helper.js
Normal file
@ -0,0 +1,109 @@
|
||||
const { get, cloneDeep } = require('lodash');
|
||||
const crypto = require('crypto');
|
||||
const { authorizeUserInWindow } = require('./authorize-user-in-window');
|
||||
|
||||
const generateCodeVerifier = () => {
|
||||
return crypto.randomBytes(16).toString('hex');
|
||||
};
|
||||
|
||||
const generateCodeChallenge = (codeVerifier) => {
|
||||
const hash = crypto.createHash('sha256');
|
||||
hash.update(codeVerifier);
|
||||
const base64Hash = hash.digest('base64');
|
||||
return base64Hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
||||
};
|
||||
|
||||
// AUTHORIZATION CODE
|
||||
|
||||
const resolveOAuth2AuthorizationCodeAccessToken = async (request) => {
|
||||
let codeVerifier = generateCodeVerifier();
|
||||
let codeChallenge = generateCodeChallenge(codeVerifier);
|
||||
|
||||
let requestCopy = cloneDeep(request);
|
||||
const { authorizationCode } = await getOAuth2AuthorizationCode(requestCopy, codeChallenge);
|
||||
const oAuth = get(requestCopy, 'oauth2', {});
|
||||
const { clientId, clientSecret, callbackUrl, scope, pkce } = oAuth;
|
||||
const data = {
|
||||
grant_type: 'authorization_code',
|
||||
code: authorizationCode,
|
||||
redirect_uri: callbackUrl,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
scope: scope
|
||||
};
|
||||
if (pkce) {
|
||||
data['code_verifier'] = codeVerifier;
|
||||
}
|
||||
|
||||
const url = requestCopy?.oauth2?.accessTokenUrl;
|
||||
return {
|
||||
data,
|
||||
url
|
||||
};
|
||||
};
|
||||
|
||||
const getOAuth2AuthorizationCode = (request, codeChallenge) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const { oauth2 } = request;
|
||||
const { callbackUrl, clientId, authorizationUrl, scope, pkce } = oauth2;
|
||||
|
||||
let authorizationUrlWithQueryParams = `${authorizationUrl}?client_id=${clientId}&redirect_uri=${callbackUrl}&response_type=code&scope=${scope}`;
|
||||
if (pkce) {
|
||||
authorizationUrlWithQueryParams += `&code_challenge=${codeChallenge}&code_challenge_method=S256`;
|
||||
}
|
||||
try {
|
||||
const { authorizationCode } = await authorizeUserInWindow({
|
||||
authorizeUrl: authorizationUrlWithQueryParams,
|
||||
callbackUrl
|
||||
});
|
||||
resolve({ authorizationCode });
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// CLIENT CREDENTIALS
|
||||
|
||||
const transformClientCredentialsRequest = async (request) => {
|
||||
let requestCopy = cloneDeep(request);
|
||||
const oAuth = get(requestCopy, 'oauth2', {});
|
||||
const { clientId, clientSecret, scope } = oAuth;
|
||||
const data = {
|
||||
grant_type: 'client_credentials',
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
scope
|
||||
};
|
||||
const url = requestCopy?.oauth2?.accessTokenUrl;
|
||||
return {
|
||||
data,
|
||||
url
|
||||
};
|
||||
};
|
||||
|
||||
// PASSWORD CREDENTIALS
|
||||
|
||||
const transformPasswordCredentialsRequest = async (request) => {
|
||||
let requestCopy = cloneDeep(request);
|
||||
const oAuth = get(requestCopy, 'oauth2', {});
|
||||
const { username, password, scope } = oAuth;
|
||||
const data = {
|
||||
grant_type: 'password',
|
||||
username,
|
||||
password,
|
||||
scope
|
||||
};
|
||||
const url = requestCopy?.oauth2?.accessTokenUrl;
|
||||
return {
|
||||
data,
|
||||
url
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
resolveOAuth2AuthorizationCodeAccessToken,
|
||||
getOAuth2AuthorizationCode,
|
||||
transformClientCredentialsRequest,
|
||||
transformPasswordCredentialsRequest
|
||||
};
|
@ -0,0 +1,49 @@
|
||||
const { get, each } = require('lodash');
|
||||
const { setAuthHeaders } = require('./prepare-request');
|
||||
|
||||
const prepareCollectionRequest = (request, collectionRoot) => {
|
||||
const headers = {};
|
||||
let contentTypeDefined = false;
|
||||
let url = request.url;
|
||||
|
||||
// collection headers
|
||||
each(get(collectionRoot, 'request.headers', []), (h) => {
|
||||
if (h.enabled) {
|
||||
headers[h.name] = h.value;
|
||||
if (h.name.toLowerCase() === 'content-type') {
|
||||
contentTypeDefined = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
each(request.headers, (h) => {
|
||||
if (h.enabled) {
|
||||
headers[h.name] = h.value;
|
||||
if (h.name.toLowerCase() === 'content-type') {
|
||||
contentTypeDefined = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let axiosRequest = {
|
||||
mode: request?.body?.mode,
|
||||
method: request.method,
|
||||
url,
|
||||
headers,
|
||||
responseType: 'arraybuffer'
|
||||
};
|
||||
|
||||
axiosRequest = setAuthHeaders(axiosRequest, request, collectionRoot);
|
||||
|
||||
if (request.script) {
|
||||
axiosRequest.script = request.script;
|
||||
}
|
||||
|
||||
axiosRequest.vars = request.vars;
|
||||
|
||||
axiosRequest.method = 'POST';
|
||||
|
||||
return axiosRequest;
|
||||
};
|
||||
|
||||
module.exports = prepareCollectionRequest;
|
@ -61,7 +61,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
};
|
||||
break;
|
||||
case 'bearer':
|
||||
axiosRequest.headers['authorization'] = `Bearer ${get(collectionAuth, 'bearer.token')}`;
|
||||
axiosRequest.headers['Authorization'] = `Bearer ${get(collectionAuth, 'bearer.token')}`;
|
||||
break;
|
||||
case 'digest':
|
||||
axiosRequest.digestConfig = {
|
||||
@ -69,36 +69,6 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
password: get(collectionAuth, 'digest.password')
|
||||
};
|
||||
break;
|
||||
case 'oauth2':
|
||||
const grantType = get(collectionAuth, 'auth.oauth2.grantType');
|
||||
switch (grantType) {
|
||||
case 'password':
|
||||
axiosRequest.oauth2 = {
|
||||
grantType: grantType,
|
||||
username: get(collectionAuth, 'auth.oauth2.username'),
|
||||
password: get(collectionAuth, 'auth.oauth2.password')
|
||||
};
|
||||
break;
|
||||
case 'authorization_code':
|
||||
axiosRequest.oauth2 = {
|
||||
grantType: grantType,
|
||||
callbackUrl: get(collectionAuth, 'auth.oauth2.callbackUrl'),
|
||||
authorizationUrl: get(collectionAuth, 'auth.oauth2.authorizationUrl'),
|
||||
accessTokenUrl: get(collectionAuth, 'auth.oauth2.accessTokenUrl'),
|
||||
clientId: get(collectionAuth, 'auth.oauth2.clientId'),
|
||||
clientSecret: get(collectionAuth, 'auth.oauth2.clientSecret'),
|
||||
scope: get(collectionAuth, 'auth.oauth2.scope')
|
||||
};
|
||||
break;
|
||||
case 'client_credentials':
|
||||
axiosRequest.oauth2 = {
|
||||
grantType: grantType,
|
||||
clientId: get(collectionAuth, 'auth.oauth2.clientId'),
|
||||
clientSecret: get(collectionAuth, 'auth.oauth2.clientSecret')
|
||||
};
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,7 +91,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
};
|
||||
break;
|
||||
case 'bearer':
|
||||
axiosRequest.headers['authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`;
|
||||
axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`;
|
||||
break;
|
||||
case 'digest':
|
||||
axiosRequest.digestConfig = {
|
||||
@ -135,8 +105,10 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
case 'password':
|
||||
axiosRequest.oauth2 = {
|
||||
grantType: grantType,
|
||||
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
|
||||
username: get(request, 'auth.oauth2.username'),
|
||||
password: get(request, 'auth.oauth2.password')
|
||||
password: get(request, 'auth.oauth2.password'),
|
||||
scope: get(request, 'auth.oauth2.scope')
|
||||
};
|
||||
break;
|
||||
case 'authorization_code':
|
||||
@ -147,14 +119,17 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
|
||||
clientId: get(request, 'auth.oauth2.clientId'),
|
||||
clientSecret: get(request, 'auth.oauth2.clientSecret'),
|
||||
scope: get(request, 'auth.oauth2.scope')
|
||||
scope: get(request, 'auth.oauth2.scope'),
|
||||
pkce: get(request, 'auth.oauth2.pkce')
|
||||
};
|
||||
break;
|
||||
case 'client_credentials':
|
||||
axiosRequest.oauth2 = {
|
||||
grantType: grantType,
|
||||
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
|
||||
clientId: get(request, 'auth.oauth2.clientId'),
|
||||
clientSecret: get(request, 'auth.oauth2.clientSecret')
|
||||
clientSecret: get(request, 'auth.oauth2.clientSecret'),
|
||||
scope: get(request, 'auth.oauth2.scope')
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
@ -20,6 +20,22 @@ class BrunoRequest {
|
||||
return this.req.method;
|
||||
}
|
||||
|
||||
getAuthMode() {
|
||||
if (this.req?.oauth2) {
|
||||
return 'oauth2';
|
||||
} else if (this.headers?.['Authorization']?.startsWith('Bearer')) {
|
||||
return 'bearer';
|
||||
} else if (this.headers?.['Authorization']?.startsWith('Basic') || this.req?.auth?.username) {
|
||||
return 'basic';
|
||||
} else if (this.req?.awsv4) {
|
||||
return 'awsv4';
|
||||
} else if (this.req?.digestConfig) {
|
||||
return 'digest';
|
||||
} else {
|
||||
return 'none';
|
||||
}
|
||||
}
|
||||
|
||||
setMethod(method) {
|
||||
this.req.method = method;
|
||||
}
|
||||
|
@ -392,14 +392,17 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
const clientIdKey = _.find(auth, { name: 'client_id' });
|
||||
const clientSecretKey = _.find(auth, { name: 'client_secret' });
|
||||
const scopeKey = _.find(auth, { name: 'scope' });
|
||||
const pkceKey = _.find(auth, { name: 'pkce' });
|
||||
return {
|
||||
auth: {
|
||||
oauth2:
|
||||
grantTypeKey?.value && grantTypeKey?.value == 'password'
|
||||
? {
|
||||
grantType: grantTypeKey ? grantTypeKey.value : '',
|
||||
accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',
|
||||
username: usernameKey ? usernameKey.value : '',
|
||||
password: passwordKey ? passwordKey.value : ''
|
||||
password: passwordKey ? passwordKey.value : '',
|
||||
scope: scopeKey ? scopeKey.value : ''
|
||||
}
|
||||
: grantTypeKey?.value && grantTypeKey?.value == 'authorization_code'
|
||||
? {
|
||||
@ -409,13 +412,16 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',
|
||||
clientId: clientIdKey ? clientIdKey.value : '',
|
||||
clientSecret: clientSecretKey ? clientSecretKey.value : '',
|
||||
scope: scopeKey ? scopeKey.value : ''
|
||||
scope: scopeKey ? scopeKey.value : '',
|
||||
pkce: pkceKey ? JSON.parse(pkceKey?.value || false) : false
|
||||
}
|
||||
: grantTypeKey?.value && grantTypeKey?.value == 'client_credentials'
|
||||
? {
|
||||
grantType: grantTypeKey ? grantTypeKey.value : '',
|
||||
accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',
|
||||
clientId: clientIdKey ? clientIdKey.value : '',
|
||||
clientSecret: clientSecretKey ? clientSecretKey.value : ''
|
||||
clientSecret: clientSecretKey ? clientSecretKey.value : '',
|
||||
scope: scopeKey ? scopeKey.value : ''
|
||||
}
|
||||
: {}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ const { outdentString } = require('../../v1/src/utils');
|
||||
|
||||
const grammar = ohm.grammar(`Bru {
|
||||
BruFile = (meta | query | headers | auth | auths | vars | script | tests | docs)*
|
||||
auths = authawsv4 | authbasic | authbearer | authdigest
|
||||
auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2
|
||||
|
||||
nl = "\\r"? "\\n"
|
||||
st = " " | "\\t"
|
||||
@ -42,6 +42,7 @@ const grammar = ohm.grammar(`Bru {
|
||||
authbasic = "auth:basic" dictionary
|
||||
authbearer = "auth:bearer" dictionary
|
||||
authdigest = "auth:digest" dictionary
|
||||
authOAuth2 = "auth:oauth2" dictionary
|
||||
|
||||
script = scriptreq | scriptres
|
||||
scriptreq = "script:pre-request" st* "{" nl* textblock tagend
|
||||
@ -242,6 +243,52 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
}
|
||||
};
|
||||
},
|
||||
authOAuth2(_1, dictionary) {
|
||||
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
|
||||
const grantTypeKey = _.find(auth, { name: 'grant_type' });
|
||||
const usernameKey = _.find(auth, { name: 'username' });
|
||||
const passwordKey = _.find(auth, { name: 'password' });
|
||||
const callbackUrlKey = _.find(auth, { name: 'callback_url' });
|
||||
const authorizationUrlKey = _.find(auth, { name: 'authorization_url' });
|
||||
const accessTokenUrlKey = _.find(auth, { name: 'access_token_url' });
|
||||
const clientIdKey = _.find(auth, { name: 'client_id' });
|
||||
const clientSecretKey = _.find(auth, { name: 'client_secret' });
|
||||
const scopeKey = _.find(auth, { name: 'scope' });
|
||||
const pkceKey = _.find(auth, { name: 'pkce' });
|
||||
return {
|
||||
auth: {
|
||||
oauth2:
|
||||
grantTypeKey?.value && grantTypeKey?.value == 'password'
|
||||
? {
|
||||
grantType: grantTypeKey ? grantTypeKey.value : '',
|
||||
accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',
|
||||
username: usernameKey ? usernameKey.value : '',
|
||||
password: passwordKey ? passwordKey.value : '',
|
||||
scope: scopeKey ? scopeKey.value : ''
|
||||
}
|
||||
: grantTypeKey?.value && grantTypeKey?.value == 'authorization_code'
|
||||
? {
|
||||
grantType: grantTypeKey ? grantTypeKey.value : '',
|
||||
callbackUrl: callbackUrlKey ? callbackUrlKey.value : '',
|
||||
authorizationUrl: authorizationUrlKey ? authorizationUrlKey.value : '',
|
||||
accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',
|
||||
clientId: clientIdKey ? clientIdKey.value : '',
|
||||
clientSecret: clientSecretKey ? clientSecretKey.value : '',
|
||||
scope: scopeKey ? scopeKey.value : '',
|
||||
pkce: pkceKey ? JSON.parse(pkceKey?.value || false) : false
|
||||
}
|
||||
: grantTypeKey?.value && grantTypeKey?.value == 'client_credentials'
|
||||
? {
|
||||
grantType: grantTypeKey ? grantTypeKey.value : '',
|
||||
accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',
|
||||
clientId: clientIdKey ? clientIdKey.value : '',
|
||||
clientSecret: clientSecretKey ? clientSecretKey.value : '',
|
||||
scope: scopeKey ? scopeKey.value : ''
|
||||
}
|
||||
: {}
|
||||
}
|
||||
};
|
||||
},
|
||||
varsreq(_1, dictionary) {
|
||||
const vars = mapPairListToKeyValPairs(dictionary.ast);
|
||||
_.each(vars, (v) => {
|
||||
|
@ -131,8 +131,10 @@ ${indentString(`password: ${auth?.digest?.password || ''}`)}
|
||||
case 'password':
|
||||
bru += `auth:oauth2 {
|
||||
${indentString(`grant_type: password`)}
|
||||
${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}
|
||||
${indentString(`username: ${auth?.oauth2?.username || ''}`)}
|
||||
${indentString(`password: ${auth?.oauth2?.password || ''}`)}
|
||||
${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
@ -146,6 +148,7 @@ ${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}
|
||||
${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}
|
||||
${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)}
|
||||
${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
|
||||
${indentString(`pkce: ${(auth?.oauth2?.pkce || false).toString()}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
@ -153,8 +156,10 @@ ${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
|
||||
case 'client_credentials':
|
||||
bru += `auth:oauth2 {
|
||||
${indentString(`grant_type: client_credentials`)}
|
||||
${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}
|
||||
${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}
|
||||
${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)}
|
||||
${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
|
@ -114,6 +114,47 @@ ${indentString(`password: ${auth.digest.password}`)}
|
||||
`;
|
||||
}
|
||||
|
||||
if (auth && auth.oauth2) {
|
||||
switch (auth?.oauth2?.grantType) {
|
||||
case 'password':
|
||||
bru += `auth:oauth2 {
|
||||
${indentString(`grant_type: password`)}
|
||||
${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}
|
||||
${indentString(`username: ${auth?.oauth2?.username || ''}`)}
|
||||
${indentString(`password: ${auth?.oauth2?.password || ''}`)}
|
||||
${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
break;
|
||||
case 'authorization_code':
|
||||
bru += `auth:oauth2 {
|
||||
${indentString(`grant_type: authorization_code`)}
|
||||
${indentString(`callback_url: ${auth?.oauth2?.callbackUrl || ''}`)}
|
||||
${indentString(`authorization_url: ${auth?.oauth2?.authorizationUrl || ''}`)}
|
||||
${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}
|
||||
${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}
|
||||
${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)}
|
||||
${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
|
||||
${indentString(`pkce: ${(auth?.oauth2?.pkce || false).toString()}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
break;
|
||||
case 'client_credentials':
|
||||
bru += `auth:oauth2 {
|
||||
${indentString(`grant_type: client_credentials`)}
|
||||
${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}
|
||||
${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}
|
||||
${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)}
|
||||
${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let reqvars = _.get(vars, 'req');
|
||||
let resvars = _.get(vars, 'res');
|
||||
if (reqvars && reqvars.length) {
|
||||
|
@ -47,9 +47,9 @@ auth:digest {
|
||||
|
||||
auth:oauth2 {
|
||||
grant_type: authorization_code
|
||||
callback_url: http://localhost:8080/api/auth/oauth2/ac/callback
|
||||
authorization_url: http://localhost:8080/api/auth/oauth2/ac/authorize
|
||||
access_token_url: http://localhost:8080/api/auth/oauth2/ac/token
|
||||
callback_url: http://localhost:8080/api/auth/oauth2/authorization_code/callback
|
||||
authorization_url: http://localhost:8080/api/auth/oauth2/authorization_code/authorize
|
||||
access_token_url: http://localhost:8080/api/auth/oauth2/authorization_code/token
|
||||
client_id: client_id_1
|
||||
client_secret: client_secret_1
|
||||
scope: read write
|
||||
|
@ -68,9 +68,9 @@
|
||||
"grantType": "authorization_code",
|
||||
"clientId": "client_id_1",
|
||||
"clientSecret": "client_secret_1",
|
||||
"authorizationUrl": "http://localhost:8080/api/auth/oauth2/ac/authorize",
|
||||
"callbackUrl": "http://localhost:8080/api/auth/oauth2/ac/callback",
|
||||
"accessTokenUrl": "http://localhost:8080/api/auth/oauth2/ac/token",
|
||||
"authorizationUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/authorize",
|
||||
"callbackUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/callback",
|
||||
"accessTokenUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/token",
|
||||
"scope": "read write"
|
||||
}
|
||||
},
|
||||
|
@ -144,7 +144,7 @@ const oauth2Schema = Yup.object({
|
||||
otherwise: Yup.string().nullable().strip()
|
||||
}),
|
||||
accessTokenUrl: Yup.string().when('grantType', {
|
||||
is: (val) => ['authorization_code'].includes(val),
|
||||
is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val),
|
||||
then: Yup.string().nullable(),
|
||||
otherwise: Yup.string().nullable().strip()
|
||||
}),
|
||||
@ -159,9 +159,14 @@ const oauth2Schema = Yup.object({
|
||||
otherwise: Yup.string().nullable().strip()
|
||||
}),
|
||||
scope: Yup.string().when('grantType', {
|
||||
is: (val) => ['authorization_code'].includes(val),
|
||||
is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val),
|
||||
then: Yup.string().nullable(),
|
||||
otherwise: Yup.string().nullable().strip()
|
||||
}),
|
||||
pkce: Yup.boolean().when('grantType', {
|
||||
is: (val) => ['authorization_code'].includes(val),
|
||||
then: Yup.boolean().defined(),
|
||||
otherwise: Yup.boolean()
|
||||
})
|
||||
})
|
||||
.noUnknown(true)
|
||||
|
@ -3,16 +3,7 @@ headers {
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: bearer
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: bruno
|
||||
password: {{basicAuthPassword}}
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{bearer_auth_token}}
|
||||
mode: none
|
||||
}
|
||||
|
||||
docs {
|
||||
|
@ -4,11 +4,11 @@ vars {
|
||||
basic_auth_password: della
|
||||
client_id: client_id_1
|
||||
client_secret: client_secret_1
|
||||
auth_url: http://localhost:8080/api/auth/oauth2/ac/authorize
|
||||
callback_url: http://localhost:8080/api/auth/oauth2/ac/callback
|
||||
access_token_url: http://localhost:8080/api/auth/oauth2/ac/token
|
||||
ropc_username: foo
|
||||
ropc_password: bar
|
||||
auth_url: http://localhost:8080/api/auth/oauth2/authorization_code/authorize
|
||||
callback_url: http://localhost:8080/api/auth/oauth2/authorization_code/callback
|
||||
access_token_url: http://localhost:8080/api/auth/oauth2/authorization_code/token
|
||||
passwordCredentials_username: foo
|
||||
passwordCredentials_password: bar
|
||||
github_authorize_url: https://github.com/login/oauth/authorize
|
||||
github_access_token_url: https://github.com/login/oauth/access_token
|
||||
google_auth_url: https://accounts.google.com/o/oauth2/auth
|
||||
@ -21,8 +21,8 @@ vars:secret [
|
||||
google_client_id,
|
||||
google_client_secret,
|
||||
github_authorization_code,
|
||||
ropc_access_token,
|
||||
cc_access_token,
|
||||
ac_access_token,
|
||||
passwordCredentials_access_token,
|
||||
client_credentials_access_token,
|
||||
authorization_code_access_token,
|
||||
github_access_token
|
||||
]
|
||||
|
1
packages/bruno-tests/collection_level_oauth2/.gitignore
vendored
Normal file
1
packages/bruno-tests/collection_level_oauth2/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
!.env
|
1
packages/bruno-tests/collection_level_oauth2/.nvmrc
Normal file
1
packages/bruno-tests/collection_level_oauth2/.nvmrc
Normal file
@ -0,0 +1 @@
|
||||
v18
|
18
packages/bruno-tests/collection_level_oauth2/bruno.json
Normal file
18
packages/bruno-tests/collection_level_oauth2/bruno.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "collection_level_oauth2",
|
||||
"type": "collection",
|
||||
"scripts": {
|
||||
"moduleWhitelist": ["crypto"],
|
||||
"filesystemAccess": {
|
||||
"allow": true
|
||||
}
|
||||
},
|
||||
"clientCertificates": {
|
||||
"enabled": true,
|
||||
"certs": []
|
||||
},
|
||||
"presets": {
|
||||
"requestType": "http"
|
||||
}
|
||||
}
|
30
packages/bruno-tests/collection_level_oauth2/collection.bru
Normal file
30
packages/bruno-tests/collection_level_oauth2/collection.bru
Normal file
@ -0,0 +1,30 @@
|
||||
headers {
|
||||
check: again
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: oauth2
|
||||
}
|
||||
|
||||
auth:oauth2 {
|
||||
grant_type: authorization_code
|
||||
callback_url: {{authorization_code_callback_url}}
|
||||
authorization_url: {{authorization_code_authorize_url}}
|
||||
access_token_url: {{authorization_code_access_token_url}}
|
||||
client_id: {{client_id}}
|
||||
client_secret: {{client_secret}}
|
||||
scope:
|
||||
pkce: true
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
if(req.getAuthMode() == 'oauth2' && res.body.access_token) {
|
||||
bru.setEnvVar('access_token_set_by_collection',res.body.access_token)
|
||||
}
|
||||
}
|
||||
|
||||
docs {
|
||||
# bruno-testbench 🐶
|
||||
|
||||
This is a test collection that I am using to test various functionalities around bruno
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
vars {
|
||||
host: http://localhost:8080
|
||||
bearer_auth_token: your_secret_token
|
||||
basic_auth_password: della
|
||||
client_id: client_id_1
|
||||
client_secret: client_secret_1
|
||||
password_credentials_access_token_url: http://localhost:8080/api/auth/oauth2/password_credentials/token
|
||||
password_credentials_username: foo
|
||||
password_credentials_password: bar
|
||||
password_credentials_scope:
|
||||
authorization_code_authorize_url: http://localhost:8080/api/auth/oauth2/authorization_code/authorize
|
||||
authorization_code_callback_url: http://localhost:8080/api/auth/oauth2/authorization_code/callback
|
||||
authorization_code_access_token_url: http://localhost:8080/api/auth/oauth2/authorization_code/token
|
||||
authorization_code_google_auth_url: https://accounts.google.com/o/oauth2/auth
|
||||
authorization_code_google_access_token_url: https://accounts.google.com/o/oauth2/token
|
||||
authorization_code_google_scope: https://www.googleapis.com/auth/userinfo.email
|
||||
authorization_code_github_authorize_url: https://github.com/login/oauth/authorize
|
||||
authorization_code_github_access_token_url: https://github.com/login/oauth/access_token
|
||||
authorization_code_access_token: null
|
||||
client_credentials_access_token_url: http://localhost:8080/api/auth/oauth2/client_credentials/token
|
||||
client_credentials_client_id: client_id_1
|
||||
client_credentials_client_secret: client_secret_1
|
||||
client_credentials_scope: admin
|
||||
client_credentials_access_token: 9f1b1874f1e79b48a46d65569d830bbb
|
||||
common_access_token: 9f1b1874f1e79b48a46d65569d830bbb
|
||||
}
|
||||
vars:secret [
|
||||
authorization_code_google_client_id,
|
||||
authorization_code_google_client_secret,
|
||||
authorization_code_github_client_secret,
|
||||
authorization_code_github_client_id,
|
||||
authorization_code_github_authorization_code,
|
||||
authorization_code_github_access_token
|
||||
]
|
@ -0,0 +1,8 @@
|
||||
vars {
|
||||
host: https://testbench-sanity.usebruno.com
|
||||
bearer_auth_token: your_secret_token
|
||||
basic_auth_password: della
|
||||
env.var1: envVar1
|
||||
env-var2: envVar2
|
||||
bark: {{process.env.PROC_ENV_VAR}}
|
||||
}
|
30
packages/bruno-tests/collection_level_oauth2/package-lock.json
generated
Normal file
30
packages/bruno-tests/collection_level_oauth2/package-lock.json
generated
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@usebruno/test-collection",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@usebruno/test-collection",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "^8.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@faker-js/faker": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.0.tgz",
|
||||
"integrity": "sha512-htW87352wzUCdX1jyUQocUcmAaFqcR/w082EC8iP/gtkF0K+aKcBp0hR5Arb7dzR8tQ1TrhE9DNa5EbJELm84w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fakerjs"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
|
||||
"npm": ">=6.14.13"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "@usebruno/test-collection",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "^8.4.0"
|
||||
}
|
||||
}
|
3
packages/bruno-tests/collection_level_oauth2/readme.md
Normal file
3
packages/bruno-tests/collection_level_oauth2/readme.md
Normal file
@ -0,0 +1,3 @@
|
||||
# bruno-tests collection
|
||||
|
||||
API Collection to run sanity tests on Bruno CLI.
|
15
packages/bruno-tests/collection_level_oauth2/resource.bru
Normal file
15
packages/bruno-tests/collection_level_oauth2/resource.bru
Normal file
@ -0,0 +1,15 @@
|
||||
meta {
|
||||
name: resource
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{host}}/api/auth/oauth2/authorization_code/resource?token={{access_token_set_by_collection}}
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
query {
|
||||
token: {{access_token_set_by_collection}}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
meta {
|
||||
name: github token with authorize
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: github.com
|
||||
body: none
|
||||
auth: oauth2
|
||||
}
|
||||
|
||||
auth:oauth2 {
|
||||
grant_type: authorization_code
|
||||
callback_url: {{callback_url}}
|
||||
authorization_url: {{github_authorize_url}}
|
||||
access_token_url: {{github_access_token_url}}
|
||||
client_id: {{github_client_id}}
|
||||
client_secret: {{github_client_secret}}
|
||||
scope: repo,gist
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
bru.setEnvVar('github_access_token',res.body.split('access_token=')[1]?.split('&scope')[0]);
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
meta {
|
||||
name: google token with authorize
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
post {
|
||||
url:
|
||||
body: none
|
||||
auth: oauth2
|
||||
}
|
||||
|
||||
auth:oauth2 {
|
||||
grant_type: authorization_code
|
||||
callback_url: {{callback_url}}
|
||||
authorization_url: {{google_auth_url}}
|
||||
access_token_url: {{google_access_token_url}}
|
||||
client_id: {{google_client_id}}
|
||||
client_secret: {{google_client_secret}}
|
||||
scope: {{google_scope}}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
bru.setEnvVar('ac_access_token', res.body.access_token);
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
meta {
|
||||
name: resource
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{host}}/api/auth/oauth2/ac/resource?token={{ac_access_token}}
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
query {
|
||||
token: {{ac_access_token}}
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token:
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"code": "eb30dbf783b65bec4539ee1dcb068606",
|
||||
"client_id": "{{client_id}}",
|
||||
"client_secret": "{{client_secret}}"
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
meta {
|
||||
name: token with authorize
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
post {
|
||||
url:
|
||||
body: none
|
||||
auth: oauth2
|
||||
}
|
||||
|
||||
auth:oauth2 {
|
||||
grant_type: authorization_code
|
||||
callback_url: {{callback_url}}
|
||||
authorization_url: {{auth_url}}
|
||||
access_token_url: {{access_token_url}}
|
||||
client_id: {{client_id}}
|
||||
client_secret: {{client_secret}}
|
||||
scope:
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
bru.setEnvVar('ac_access_token', res.body.access_token);
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
meta {
|
||||
name: github token with authorize
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url:
|
||||
body: none
|
||||
auth: oauth2
|
||||
}
|
||||
|
||||
auth:oauth2 {
|
||||
grant_type: authorization_code
|
||||
callback_url: {{authorization_code_callback_url}}
|
||||
authorization_url: {{authorization_code_github_authorize_url}}
|
||||
access_token_url: {{authorization_code_github_access_token_url}}
|
||||
client_id: {{authorization_code_github_client_id}}
|
||||
client_secret: {{authorization_code_github_client_secret}}
|
||||
scope: repo,gist
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
bru.setEnvVar('github_access_token',res.body.split('access_token=')[1]?.split('&scope')[0]);
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
meta {
|
||||
name: google token with authorize
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
post {
|
||||
url:
|
||||
body: none
|
||||
auth: oauth2
|
||||
}
|
||||
|
||||
auth:oauth2 {
|
||||
grant_type: authorization_code
|
||||
callback_url: {{authorization_code_callback_url}}
|
||||
authorization_url: {{authorization_code_google_auth_url}}
|
||||
access_token_url: {{authorization_code_google_access_token_url}}
|
||||
client_id: {{authorization_code_google_client_id}}
|
||||
client_secret: {{authorization_code_google_client_secret}}
|
||||
scope: {{authorization_code_google_scope}}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
bru.setEnvVar('authorization_code_access_token', res.body.access_token);
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
meta {
|
||||
name: resource
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{host}}/api/auth/oauth2/authorization_code/resource?token={{authorization_code_access_token}}
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
query {
|
||||
token: {{authorization_code_access_token}}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: token with authorize
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
post {
|
||||
url:
|
||||
body: none
|
||||
auth: oauth2
|
||||
}
|
||||
|
||||
auth:oauth2 {
|
||||
grant_type: authorization_code
|
||||
callback_url: {{authorization_code_callback_url}}
|
||||
authorization_url: {{authorization_code_authorize_url}}
|
||||
access_token_url: {{authorization_code_access_token_url}}
|
||||
client_id: {{client_id}}
|
||||
client_secret: {{client_secret}}
|
||||
scope:
|
||||
pkce: true
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
bru.setEnvVar('authorization_code_access_token', res.body.access_token);
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
meta {
|
||||
name: resource
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/api/auth/oauth2/cc/resource?token={{cc_access_token}}
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
query {
|
||||
token: {{cc_access_token}}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
meta {
|
||||
name: token
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{host}}/api/auth/oauth2/cc/token
|
||||
body: none
|
||||
auth: oauth2
|
||||
}
|
||||
|
||||
auth:oauth2 {
|
||||
grant_type: client_credentials
|
||||
client_id: {{client_id}}
|
||||
client_secret: {{client_secret}}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
bru.setEnvVar('cc_access_token', res.body.access_token);
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
meta {
|
||||
name: resource
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/api/auth/oauth2/client_credentials/resource?token={{client_credentials_access_token}}
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
query {
|
||||
token: {{client_credentials_access_token}}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
meta {
|
||||
name: token
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url:
|
||||
body: none
|
||||
auth: oauth2
|
||||
}
|
||||
|
||||
auth:oauth2 {
|
||||
grant_type: client_credentials
|
||||
access_token_url: {{client_credentials_access_token_url}}
|
||||
client_id: {{client_credentials_client_id}}
|
||||
client_secret: {{client_credentials_client_secret}}
|
||||
scope: {{client_credentials_scope}}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
bru.setEnvVar('client_credentials_access_token', res.body.access_token);
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
meta {
|
||||
name: resource
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{host}}/api/auth/oauth2/password_credentials/resource
|
||||
body: none
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{passwordCredentials_access_token}}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
meta {
|
||||
name: token
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url:
|
||||
body: none
|
||||
auth: oauth2
|
||||
}
|
||||
|
||||
auth:oauth2 {
|
||||
grant_type: password
|
||||
access_token_url: {{password_credentials_access_token_url}}
|
||||
username: {{password_credentials_username}}
|
||||
password: {{password_credentials_password}}
|
||||
scope:
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
bru.setEnvVar('passwordCredentials_access_token', res.body.access_token);
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
meta {
|
||||
name: resource
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{host}}/api/auth/oauth2/ropc/resource
|
||||
body: none
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{ropc_access_token}}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
meta {
|
||||
name: token
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{host}}/api/auth/oauth2/ropc/token
|
||||
body: none
|
||||
auth: oauth2
|
||||
}
|
||||
|
||||
auth:oauth2 {
|
||||
grant_type: password
|
||||
username: {{ropc_username}}
|
||||
password: {{ropc_password}}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
bru.setEnvVar('ropc_access_token', res.body.access_token);
|
||||
}
|
@ -1,19 +1,7 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "bruno-testbench",
|
||||
"name": "collection_oauth2",
|
||||
"type": "collection",
|
||||
"proxy": {
|
||||
"enabled": false,
|
||||
"protocol": "http",
|
||||
"hostname": "{{proxyHostname}}",
|
||||
"port": 4000,
|
||||
"auth": {
|
||||
"enabled": false,
|
||||
"username": "anoop",
|
||||
"password": "password"
|
||||
},
|
||||
"bypassProxy": ""
|
||||
},
|
||||
"scripts": {
|
||||
"moduleWhitelist": ["crypto"],
|
||||
"filesystemAccess": {
|
||||
@ -25,7 +13,6 @@
|
||||
"certs": []
|
||||
},
|
||||
"presets": {
|
||||
"requestType": "http",
|
||||
"requestUrl": "http://localhost:6000"
|
||||
"requestType": "http"
|
||||
}
|
||||
}
|
||||
|
@ -6,15 +6,6 @@ auth {
|
||||
mode: none
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: bruno
|
||||
password: {{basicAuthPassword}}
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{bearerAuthToken}}
|
||||
}
|
||||
|
||||
docs {
|
||||
# bruno-testbench 🐶
|
||||
|
||||
|
@ -4,23 +4,29 @@ vars {
|
||||
basic_auth_password: della
|
||||
client_id: client_id_1
|
||||
client_secret: client_secret_1
|
||||
auth_url: http://localhost:8080/api/auth/oauth2/ac/authorize
|
||||
callback_url: http://localhost:8080/api/auth/oauth2/ac/callback
|
||||
access_token_url: http://localhost:8080/api/auth/oauth2/ac/token
|
||||
ropc_username: foo
|
||||
ropc_password: bar
|
||||
github_authorize_url: https://github.com/login/oauth/authorize
|
||||
github_access_token_url: https://github.com/login/oauth/access_token
|
||||
google_auth_url: https://accounts.google.com/o/oauth2/auth
|
||||
google_access_token_url: https://accounts.google.com/o/oauth2/token
|
||||
google_scope: https://www.googleapis.com/auth/userinfo.email
|
||||
password_credentials_access_token_url: http://localhost:8080/api/auth/oauth2/password_credentials/token
|
||||
password_credentials_username: foo
|
||||
password_credentials_password: bar
|
||||
password_credentials_scope:
|
||||
authorization_code_authorize_url: http://localhost:8080/api/auth/oauth2/authorization_code/authorize
|
||||
authorization_code_callback_url: http://localhost:8080/api/auth/oauth2/authorization_code/callback
|
||||
authorization_code_access_token_url: http://localhost:8080/api/auth/oauth2/authorization_code/token
|
||||
authorization_code_google_auth_url: https://accounts.google.com/o/oauth2/auth
|
||||
authorization_code_google_access_token_url: https://accounts.google.com/o/oauth2/token
|
||||
authorization_code_google_scope: https://www.googleapis.com/auth/userinfo.email
|
||||
authorization_code_github_authorize_url: https://github.com/login/oauth/authorize
|
||||
authorization_code_github_access_token_url: https://github.com/login/oauth/access_token
|
||||
authorization_code_access_token: null
|
||||
client_credentials_access_token_url: http://localhost:8080/api/auth/oauth2/client_credentials/token
|
||||
client_credentials_client_id: client_id_1
|
||||
client_credentials_client_secret: client_secret_1
|
||||
client_credentials_scope: admin
|
||||
}
|
||||
vars:secret [
|
||||
github_client_secret,
|
||||
github_client_id,
|
||||
google_client_id,
|
||||
google_client_secret,
|
||||
github_authorization_code,
|
||||
github_access_token,
|
||||
ac_access_token
|
||||
authorization_code_google_client_id,
|
||||
authorization_code_google_client_secret,
|
||||
authorization_code_github_client_secret,
|
||||
authorization_code_github_client_id,
|
||||
authorization_code_github_authorization_code,
|
||||
authorization_code_github_access_token
|
||||
]
|
||||
|
@ -4,13 +4,13 @@ const router = express.Router();
|
||||
const authBearer = require('./bearer');
|
||||
const authBasic = require('./basic');
|
||||
const authCookie = require('./cookie');
|
||||
const authOAuth2Ropc = require('./oauth2/ropc');
|
||||
const authOAuth2AuthorizationCode = require('./oauth2/ac');
|
||||
const authOAuth2Cc = require('./oauth2/cc');
|
||||
const authOAuth2PasswordCredentials = require('./oauth2/passwordCredentials');
|
||||
const authOAuth2AuthorizationCode = require('./oauth2/authorizationCode');
|
||||
const authOAuth2ClientCredentials = require('./oauth2/clientCredentials');
|
||||
|
||||
router.use('/oauth2/ropc', authOAuth2Ropc);
|
||||
router.use('/oauth2/ac', authOAuth2AuthorizationCode);
|
||||
router.use('/oauth2/cc', authOAuth2Cc);
|
||||
router.use('/oauth2/password_credentials', authOAuth2PasswordCredentials);
|
||||
router.use('/oauth2/authorization_code', authOAuth2AuthorizationCode);
|
||||
router.use('/oauth2/client_credentials', authOAuth2ClientCredentials);
|
||||
router.use('/bearer', authBearer);
|
||||
router.use('/basic', authBasic);
|
||||
router.use('/cookie', authCookie);
|
||||
|
@ -17,8 +17,16 @@ function generateUniqueString() {
|
||||
return crypto.randomBytes(16).toString('hex');
|
||||
}
|
||||
|
||||
const generateCodeChallenge = (codeVerifier) => {
|
||||
const hash = crypto.createHash('sha256');
|
||||
hash.update(codeVerifier);
|
||||
const base64Hash = hash.digest('base64');
|
||||
return base64Hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
||||
};
|
||||
|
||||
router.get('/authorize', (req, res) => {
|
||||
const { response_type, client_id, redirect_uri } = req.query;
|
||||
const { response_type, client_id, redirect_uri, code_challenge } = req.query;
|
||||
console.log('authorization code authorize', req.query);
|
||||
if (response_type !== 'code') {
|
||||
return res.status(401).json({ error: 'Invalid Response type, expected "code"' });
|
||||
}
|
||||
@ -37,7 +45,8 @@ router.get('/authorize', (req, res) => {
|
||||
authCodes.push({
|
||||
authCode: authorization_code,
|
||||
client_id,
|
||||
redirect_uri
|
||||
redirect_uri,
|
||||
code_challenge
|
||||
});
|
||||
|
||||
const redirectUrl = `${redirect_uri}?code=${authorization_code}`;
|
||||
@ -72,6 +81,7 @@ router.get('/authorize', (req, res) => {
|
||||
|
||||
// Handle the authorization callback
|
||||
router.get('/callback', (req, res) => {
|
||||
console.log('authorization code callback', req.query);
|
||||
const { code } = req.query;
|
||||
|
||||
// Check if the authCode is valid.
|
||||
@ -85,13 +95,15 @@ router.get('/callback', (req, res) => {
|
||||
});
|
||||
|
||||
router.post('/token', (req, res) => {
|
||||
let grant_type, code, redirect_uri, client_id, client_secret;
|
||||
console.log('authorization code token', req.body, req.headers);
|
||||
let grant_type, code, redirect_uri, client_id, client_secret, code_verifier;
|
||||
if (req?.body?.grant_type) {
|
||||
grant_type = req?.body?.grant_type;
|
||||
code = req?.body?.code;
|
||||
redirect_uri = req?.body?.redirect_uri;
|
||||
client_id = req?.body?.client_id;
|
||||
client_secret = req?.body?.client_secret;
|
||||
code_verifier = req?.body?.code_verifier;
|
||||
}
|
||||
if (req?.headers?.grant_type) {
|
||||
grant_type = req?.headers?.grant_type;
|
||||
@ -99,6 +111,7 @@ router.post('/token', (req, res) => {
|
||||
redirect_uri = req?.headers?.redirect_uri;
|
||||
client_id = req?.headers?.client_id;
|
||||
client_secret = req?.headers?.client_secret;
|
||||
code_verifier = req?.headers?.code_verifier;
|
||||
}
|
||||
|
||||
if (grant_type !== 'authorization_code') {
|
||||
@ -110,7 +123,13 @@ router.post('/token', (req, res) => {
|
||||
// return res.status(401).json({ error: 'Invalid client credentials' });
|
||||
// }
|
||||
|
||||
const storedAuthCode = authCodes.find((t) => t.authCode === code);
|
||||
const storedAuthCode = authCodes.find((t) => {
|
||||
if (!t?.code_challenge) {
|
||||
return t.authCode === code;
|
||||
} else {
|
||||
return t.authCode === code && t.code_challenge === generateCodeChallenge(code_verifier);
|
||||
}
|
||||
});
|
||||
|
||||
if (!storedAuthCode) {
|
||||
return res.status(401).json({ error: 'Invalid Authorization Code' });
|
||||
@ -127,6 +146,7 @@ router.post('/token', (req, res) => {
|
||||
|
||||
router.post('/resource', (req, res) => {
|
||||
try {
|
||||
console.log('authorization code resource', req.query, tokens);
|
||||
const { token } = req.query;
|
||||
const storedToken = tokens.find((t) => t.accessToken === token);
|
||||
if (!storedToken) {
|
@ -4,7 +4,8 @@ const crypto = require('crypto');
|
||||
const clients = [
|
||||
{
|
||||
client_id: 'client_id_1',
|
||||
client_secret: 'client_secret_1'
|
||||
client_secret: 'client_secret_1',
|
||||
scope: 'admin'
|
||||
}
|
||||
];
|
||||
|
||||
@ -15,35 +16,39 @@ function generateUniqueString() {
|
||||
}
|
||||
|
||||
router.post('/token', (req, res) => {
|
||||
let grant_type, client_id, client_secret;
|
||||
let grant_type, client_id, client_secret, scope;
|
||||
if (req?.body?.grant_type) {
|
||||
grant_type = req?.body?.grant_type;
|
||||
client_id = req?.body?.client_id;
|
||||
client_secret = req?.body?.client_secret;
|
||||
scope = req?.body?.scope;
|
||||
} else if (req?.headers?.grant_type) {
|
||||
grant_type = req?.headers?.grant_type;
|
||||
client_id = req?.headers?.client_id;
|
||||
client_secret = req?.headers?.client_secret;
|
||||
scope = req?.headers?.scope;
|
||||
}
|
||||
|
||||
console.log('client_cred', client_id, client_secret, scope);
|
||||
if (grant_type !== 'client_credentials') {
|
||||
return res.status(401).json({ error: 'Invalid Grant Type, expected "client_credentials"' });
|
||||
}
|
||||
|
||||
const client = clients.find((c) => c.client_id == client_id && c.client_secret == client_secret);
|
||||
const client = clients.find((c) => c.client_id == client_id && c.client_secret == client_secret && c.scope == scope);
|
||||
|
||||
if (!client) {
|
||||
return res.status(401).json({ error: 'Invalid client' });
|
||||
return res.status(401).json({ error: 'Invalid client details or scope' });
|
||||
}
|
||||
|
||||
const token = generateUniqueString();
|
||||
tokens.push({
|
||||
token,
|
||||
client_id,
|
||||
client_secret
|
||||
client_secret,
|
||||
scope
|
||||
});
|
||||
|
||||
return res.json({ message: 'Authenticated successfully', access_token: token });
|
||||
return res.json({ message: 'Authenticated successfully', access_token: token, scope });
|
||||
});
|
||||
|
||||
router.get('/resource', (req, res) => {
|
@ -9,23 +9,10 @@ const users = [
|
||||
}
|
||||
];
|
||||
|
||||
// P
|
||||
// {
|
||||
// grant_type: 'password',
|
||||
// username: 'foo',
|
||||
// password: 'bar'
|
||||
// }
|
||||
|
||||
// I
|
||||
// {
|
||||
// grant_type: 'password',
|
||||
// username: 'foo',
|
||||
// password: 'bar',
|
||||
// client_id: 'client_id_1',
|
||||
// client_secret: 'client_secret_1'
|
||||
// }
|
||||
router.post('/token', (req, res) => {
|
||||
const { grant_type, username, password, client_id, client_secret } = req.body;
|
||||
const { grant_type, username, password, scope } = req.body;
|
||||
|
||||
console.log('password_credentials', username, password, scope);
|
||||
|
||||
if (grant_type !== 'password') {
|
||||
return res.status(401).json({ error: 'Invalid Grant Type' });
|
Loading…
Reference in New Issue
Block a user