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:
lohit 2024-03-04 15:21:05 +05:30 committed by GitHub
parent 9d3df0a86a
commit 858536e13d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
84 changed files with 1707 additions and 570 deletions

View File

@ -43,6 +43,7 @@ if (!SERVER_RENDERED) {
'req.getUrl()',
'req.setUrl(url)',
'req.getMethod()',
'req.getAuthMode()',
'req.setMethod(method)',
'req.getHeader(name)',
'req.getHeaders()',

View File

@ -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={() => {

View File

@ -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;

View File

@ -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 };

View File

@ -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;

View File

@ -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;

View File

@ -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 };

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 };

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -5,7 +5,7 @@ const inputsConfig = [
},
{
key: 'authorizationUrl',
label: 'Auth URL'
label: 'Authorization URL'
},
{
key: 'accessTokenUrl',

View File

@ -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>
);
};

View File

@ -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 };

View File

@ -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"

View File

@ -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;

View File

@ -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;

View File

@ -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 };

View File

@ -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;

View File

@ -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} />;

View File

@ -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>
);
}

View File

@ -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);

View File

@ -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;
}
}
},

View File

@ -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': {

View File

@ -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;

View File

@ -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;
}

View File

@ -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')}`;
}
}

View File

@ -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);
}

View File

@ -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]) {

View File

@ -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:

View File

@ -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
};

View 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
};

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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 : ''
}
: {}
}

View File

@ -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) => {

View File

@ -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 || ''}`)}
}
`;

View File

@ -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) {

View File

@ -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

View File

@ -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"
}
},

View File

@ -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)

View File

@ -3,16 +3,7 @@ headers {
}
auth {
mode: bearer
}
auth:basic {
username: bruno
password: {{basicAuthPassword}}
}
auth:bearer {
token: {{bearer_auth_token}}
mode: none
}
docs {

View File

@ -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
]

View File

@ -0,0 +1 @@
!.env

View File

@ -0,0 +1 @@
v18

View 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"
}
}

View 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
}

View File

@ -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
]

View File

@ -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}}
}

View 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"
}
}
}
}

View File

@ -0,0 +1,7 @@
{
"name": "@usebruno/test-collection",
"version": "0.0.1",
"dependencies": {
"@faker-js/faker": "^8.4.0"
}
}

View File

@ -0,0 +1,3 @@
# bruno-tests collection
API Collection to run sanity tests on Bruno CLI.

View 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}}
}

View File

@ -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]);
}

View File

@ -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);
}

View File

@ -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}}"
}
}

View File

@ -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);
}

View File

@ -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]);
}

View File

@ -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);
}

View File

@ -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}}
}

View File

@ -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);
}

View File

@ -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}}
}

View File

@ -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);
}

View File

@ -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}}
}

View File

@ -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);
}

View File

@ -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}}
}

View File

@ -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);
}

View File

@ -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}}
}

View File

@ -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);
}

View File

@ -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"
}
}

View File

@ -6,15 +6,6 @@ auth {
mode: none
}
auth:basic {
username: bruno
password: {{basicAuthPassword}}
}
auth:bearer {
token: {{bearerAuthToken}}
}
docs {
# bruno-testbench 🐶

View File

@ -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
]

View File

@ -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);

View File

@ -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) {

View File

@ -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) => {

View File

@ -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' });