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.getUrl()',
'req.setUrl(url)', 'req.setUrl(url)',
'req.getMethod()', 'req.getMethod()',
'req.getAuthMode()',
'req.setMethod(method)', 'req.setMethod(method)',
'req.getHeader(name)', 'req.getHeader(name)',
'req.getHeaders()', 'req.getHeaders()',

View File

@ -70,6 +70,15 @@ const AuthMode = ({ collection }) => {
> >
Digest Auth Digest Auth
</div> </div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('oauth2');
}}
>
Oauth2
</div>
<div <div
className="dropdown-item" className="dropdown-item"
onClick={() => { 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 DigestAuth from './DigestAuth';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions'; import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import OAuth2 from './OAuth2';
const Auth = ({ collection }) => { const Auth = ({ collection }) => {
const authMode = get(collection, 'root.request.auth.mode'); const authMode = get(collection, 'root.request.auth.mode');
@ -29,6 +30,9 @@ const Auth = ({ collection }) => {
case 'digest': { case 'digest': {
return <DigestAuth collection={collection} />; return <DigestAuth collection={collection} />;
} }
case 'oauth2': {
return <OAuth2 collection={collection} />;
}
} }
}; };
@ -38,7 +42,6 @@ const Auth = ({ collection }) => {
<AuthMode collection={collection} /> <AuthMode collection={collection} />
</div> </div>
{getAuthView()} {getAuthView()}
<div className="mt-6"> <div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}> <button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save Save

View File

@ -81,71 +81,72 @@ const EnvironmentVariables = ({ environment, collection }) => {
return ( return (
<StyledWrapper className="w-full mt-6 mb-6"> <StyledWrapper className="w-full mt-6 mb-6">
<table> <div className="h-[50vh] overflow-y-auto w-full">
<thead> <table>
<tr> <thead>
<td>Enabled</td> <tr>
<td>Name</td> <td>Enabled</td>
<td>Value</td> <td>Name</td>
<td>Secret</td> <td>Value</td>
<td></td> <td>Secret</td>
</tr> <td></td>
</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> </tr>
))} </thead>
</tbody> <tbody>
</table> {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> <div>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={addVariable}> <button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={addVariable}>
+ Add Variable + Add Variable

View File

@ -20,6 +20,8 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid)); const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, pkce } = oAuth;
const handleChange = (key, value) => { const handleChange = (key, value) => {
dispatch( dispatch(
updateAuth({ updateAuth({
@ -28,13 +30,39 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
itemUid: item.uid, itemUid: item.uid,
content: { content: {
grantType: 'authorization_code', grantType: 'authorization_code',
...oAuth, callbackUrl,
authorizationUrl,
accessTokenUrl,
clientId,
clientSecret,
scope,
pkce,
[key]: value [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 ( return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col"> <StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
{inputsConfig.map((input) => { {inputsConfig.map((input) => {
@ -48,13 +76,22 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
theme={storedTheme} theme={storedTheme}
onSave={handleSave} onSave={handleSave}
onChange={(val) => handleChange(key, val)} onChange={(val) => handleChange(key, val)}
onRun={() => {}} onRun={handleRun}
collection={collection} collection={collection}
/> />
</div> </div>
</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"> <button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
Get Access Token Get Access Token
</button> </button>

View File

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

View File

@ -4,8 +4,9 @@ import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor'; import SingleLineEditor from 'components/SingleLineEditor';
import { updateAuth } from 'providers/ReduxStore/slices/collections'; 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 StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
const OAuth2ClientCredentials = ({ item, collection }) => { const OAuth2ClientCredentials = ({ item, collection }) => {
const dispatch = useDispatch(); 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 oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
const handleRun = () => dispatch(sendRequest(item, collection.uid)); const handleRun = async () => {
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid)); dispatch(sendRequest(item, 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 handleClientSecretChange = (clientSecret) => { const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const { accessTokenUrl, clientId, clientSecret, scope } = oAuth;
const handleChange = (key, value) => {
dispatch( dispatch(
updateAuth({ updateAuth({
mode: 'oauth2', mode: 'oauth2',
@ -39,38 +30,39 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
itemUid: item.uid, itemUid: item.uid,
content: { content: {
grantType: 'client_credentials', grantType: 'client_credentials',
clientId: oAuth.clientId, accessTokenUrl,
clientSecret: clientSecret clientId,
clientSecret,
scope,
[key]: value
} }
}) })
); );
}; };
return ( return (
<StyledWrapper className="mt-2 w-full"> <StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
<label className="block font-medium mb-2">Client Id</label> {inputsConfig.map((input) => {
<div className="single-line-editor-wrapper mb-2"> const { key, label } = input;
<SingleLineEditor return (
value={oAuth.clientId || ''} <div className="flex flex-col w-full gap-1" key={`input-${key}`}>
theme={storedTheme} <label className="block font-medium">{label}</label>
onSave={handleSave} <div className="single-line-editor-wrapper">
onChange={(val) => handleClientIdChange(val)} <SingleLineEditor
onRun={handleRun} value={oAuth[key] || ''}
collection={collection} theme={storedTheme}
/> onSave={handleSave}
</div> onChange={(val) => handleChange(key, val)}
onRun={handleRun}
<label className="block font-medium mb-2">Client Secret</label> collection={collection}
<div className="single-line-editor-wrapper"> />
<SingleLineEditor </div>
value={oAuth.clientSecret || ''} </div>
theme={storedTheme} );
onSave={handleSave} })}
onChange={(val) => handleClientSecretChange(val)} <button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
onRun={handleRun} Get Access Token
collection={collection} </button>
/>
</div>
</StyledWrapper> </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(() => { useEffect(() => {
// initalize redux state with a default oauth2 auth type // initalize redux state with a default oauth2 grant type
// authorization_code - default option // authorization_code - default option
!oAuth?.grantType && !oAuth?.grantType &&
dispatch( dispatch(
@ -64,7 +64,7 @@ const GrantTypeSelector = ({ item, collection }) => {
onGrantTypeChange('password'); onGrantTypeChange('password');
}} }}
> >
Resource Owner Password Credentials Password Credentials
</div> </div>
<div <div
className="dropdown-item" 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 get from 'lodash/get';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import GrantTypeSelector from './GrantTypeSelector/index'; import GrantTypeSelector from './GrantTypeSelector/index';
import OAuth2Ropc from './Ropc/index'; import OAuth2PasswordCredentials from './PasswordCredentials/index';
import OAuth2AuthorizationCode from './AuthorizationCode/index'; import OAuth2AuthorizationCode from './AuthorizationCode/index';
import OAuth2ClientCredentials from './ClientCredentials/index'; import OAuth2ClientCredentials from './ClientCredentials/index';
const grantTypeComponentMap = (grantType, item, collection) => { const grantTypeComponentMap = (grantType, item, collection) => {
switch (grantType) { switch (grantType) {
case 'password': case 'password':
return <OAuth2Ropc item={item} collection={collection} />; return <OAuth2PasswordCredentials item={item} collection={collection} />;
break; break;
case 'authorization_code': case 'authorization_code':
return <OAuth2AuthorizationCode item={item} collection={collection} />; return <OAuth2AuthorizationCode item={item} collection={collection} />;

View File

@ -35,8 +35,20 @@ const Auth = ({ item, collection }) => {
case 'inherit': { case 'inherit': {
return ( return (
<div className="flex flex-row w-full mt-2 gap-2"> <div className="flex flex-row w-full mt-2 gap-2">
<div>Auth inherited from the Collection: </div> {collectionAuth?.mode === 'oauth2' ? (
<div className="inherit-mode-text">{humanizeRequestAuthMode(collectionAuth?.mode)}</div> <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> </div>
); );
} }

View File

@ -40,6 +40,7 @@ import { each } from 'lodash';
import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs'; import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
import { resolveRequestFilename } from 'utils/common/platform'; import { resolveRequestFilename } from 'utils/common/platform';
import { parseQueryParams, splitOnFirst } from 'utils/url/index'; import { parseQueryParams, splitOnFirst } from 'utils/url/index';
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
export const renameCollection = (newName, collectionUid) => (dispatch, getState) => { export const renameCollection = (newName, collectionUid) => (dispatch, getState) => {
const state = 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) => { export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
const state = getState(); const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid); 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')); return reject(new Error('Collection not found'));
} }
const itemCopy = cloneDeep(item); const itemCopy = cloneDeep(item || {});
const collectionCopy = cloneDeep(collection); const collectionCopy = cloneDeep(collection);
const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid); const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid);

View File

@ -678,6 +678,7 @@ export const collectionsSlice = createSlice({
if (!item.draft) { if (!item.draft) {
item.draft = cloneDeep(item); item.draft = cloneDeep(item);
} }
item.draft.request.auth = {};
item.draft.request.auth.mode = action.payload.mode; item.draft.request.auth.mode = action.payload.mode;
} }
} }
@ -978,6 +979,7 @@ export const collectionsSlice = createSlice({
const collection = findCollectionByUid(state.collections, action.payload.collectionUid); const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) { if (collection) {
set(collection, 'root.request.auth', {});
set(collection, 'root.request.auth.mode', action.payload.mode); 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); const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) { if (collection) {
set(collection, 'root.request.auth', {});
set(collection, 'root.request.auth.mode', action.payload.mode);
switch (action.payload.mode) { switch (action.payload.mode) {
case 'awsv4': case 'awsv4':
set(collection, 'root.request.auth.awsv4', action.payload.content); set(collection, 'root.request.auth.awsv4', action.payload.content);
@ -998,6 +1002,9 @@ export const collectionsSlice = createSlice({
case 'digest': case 'digest':
set(collection, 'root.request.auth.digest', action.payload.content); set(collection, 'root.request.auth.digest', action.payload.content);
break; 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'; let label = 'No Auth';
switch (mode) { switch (mode) {
case 'password': { case 'password': {
label = 'Resource Owner Password Credentials'; label = 'Password Credentials';
break; break;
} }
case 'authorization_code': { 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) => { export const fetchGqlSchema = async (endpoint, environment, request, collection) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const { ipcRenderer } = window; const { ipcRenderer } = window;

View File

@ -105,7 +105,7 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
const password = _interpolate(request.auth.password) || ''; const password = _interpolate(request.auth.password) || '';
// use auth header based approach and delete the request.auth object // 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; delete request.auth;
} }

View File

@ -52,7 +52,7 @@ const prepareRequest = (request, collectionRoot) => {
} }
if (collectionAuth.mode === 'bearer') { 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') { 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 callbackUrlWithCode = new URL(finalUrl);
const authorizationCode = callbackUrlWithCode.searchParams.get('code'); const authorizationCode = callbackUrlWithCode.searchParams.get('code');
return resolve(authorizationCode); return resolve({ authorizationCode });
} catch (error) { } catch (error) {
return reject(error); return reject(error);
} }

View File

@ -12,6 +12,7 @@ const { ipcMain } = require('electron');
const { isUndefined, isNull, each, get, compact, cloneDeep } = require('lodash'); const { isUndefined, isNull, each, get, compact, cloneDeep } = require('lodash');
const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js'); const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js');
const prepareRequest = require('./prepare-request'); const prepareRequest = require('./prepare-request');
const prepareCollectionRequest = require('./prepare-collection-request');
const prepareGqlIntrospectionRequest = require('./prepare-gql-introspection-request'); const prepareGqlIntrospectionRequest = require('./prepare-gql-introspection-request');
const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token'); const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token');
const { uuid } = require('../../utils/common'); const { uuid } = require('../../utils/common');
@ -29,7 +30,11 @@ const { addDigestInterceptor } = require('./digestauth-helper');
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../../utils/proxy-util'); const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../../utils/proxy-util');
const { chooseFileToSave, writeBinaryFile } = require('../../utils/filesystem'); const { chooseFileToSave, writeBinaryFile } = require('../../utils/filesystem');
const { getCookieStringForUrl, addCookieToJar, getDomainsWithCookies } = require('../../utils/cookies'); 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 // override the default escape function to prevent escaping
Mustache.escape = function (value) { Mustache.escape = function (value) {
@ -191,12 +196,30 @@ const configureRequest = async (
const axiosInstance = makeAxiosInstance(); const axiosInstance = makeAxiosInstance();
if (request.oauth2) { if (request.oauth2) {
if (request?.oauth2?.grantType == 'authorization_code') { let requestCopy = cloneDeep(request);
let requestCopy = cloneDeep(request); switch (request?.oauth2?.grantType) {
interpolateVars(requestCopy, envVars, collectionVariables, processEnvVars); case 'authorization_code':
const { data, url } = await resolveOAuth2AuthorizationCodecessToken(requestCopy); interpolateVars(requestCopy, envVars, collectionVariables, processEnvVars);
request.data = data; const { data: authorizationCodeData, url: authorizationCodeAccessTokenUrl } =
request.url = url; 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; request.headers['cookie'] = cookieString;
} }
} }
return axiosInstance; 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) => { ipcMain.handle('cancel-http-request', async (event, cancelTokenUid) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (cancelTokenUid && cancelTokens[cancelTokenUid]) { if (cancelTokenUid && cancelTokens[cancelTokenUid]) {

View File

@ -104,21 +104,26 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
const username = _interpolate(request.auth.username) || ''; const username = _interpolate(request.auth.username) || '';
const password = _interpolate(request.auth.password) || ''; const password = _interpolate(request.auth.password) || '';
// use auth header based approach and delete the request.auth object // 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; delete request.auth;
} }
if (request?.oauth2?.grantType) { if (request?.oauth2?.grantType) {
let username, password, scope, clientId, clientSecret;
switch (request.oauth2.grantType) { switch (request.oauth2.grantType) {
case 'password': case 'password':
let username = _interpolate(request.oauth2.username) || ''; username = _interpolate(request.oauth2.username) || '';
let password = _interpolate(request.oauth2.password) || ''; password = _interpolate(request.oauth2.password) || '';
scope = _interpolate(request.oauth2.scope) || '';
request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || '';
request.oauth2.username = username; request.oauth2.username = username;
request.oauth2.password = password; request.oauth2.password = password;
request.oauth2.scope = scope;
request.data = { request.data = {
grant_type: 'password', grant_type: 'password',
username, username,
password password,
scope
}; };
break; break;
case 'authorization_code': case 'authorization_code':
@ -128,16 +133,21 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
request.oauth2.clientId = _interpolate(request.oauth2.clientId) || ''; request.oauth2.clientId = _interpolate(request.oauth2.clientId) || '';
request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || ''; request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || '';
request.oauth2.scope = _interpolate(request.oauth2.scope) || ''; request.oauth2.scope = _interpolate(request.oauth2.scope) || '';
request.oauth2.pkce = _interpolate(request.oauth2.pkce) || false;
break; break;
case 'client_credentials': case 'client_credentials':
let clientId = _interpolate(request.oauth2.clientId) || ''; clientId = _interpolate(request.oauth2.clientId) || '';
let clientSecret = _interpolate(request.oauth2.clientSecret) || ''; clientSecret = _interpolate(request.oauth2.clientSecret) || '';
scope = _interpolate(request.oauth2.scope) || '';
request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || '';
request.oauth2.clientId = clientId; request.oauth2.clientId = clientId;
request.oauth2.clientSecret = clientSecret; request.oauth2.clientSecret = clientSecret;
request.oauth2.scope = scope;
request.data = { request.data = {
grant_type: 'client_credentials', grant_type: 'client_credentials',
client_id: clientId, client_id: clientId,
client_secret: clientSecret client_secret: clientSecret,
scope
}; };
break; break;
default: 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; break;
case 'bearer': case 'bearer':
axiosRequest.headers['authorization'] = `Bearer ${get(collectionAuth, 'bearer.token')}`; axiosRequest.headers['Authorization'] = `Bearer ${get(collectionAuth, 'bearer.token')}`;
break; break;
case 'digest': case 'digest':
axiosRequest.digestConfig = { axiosRequest.digestConfig = {
@ -69,36 +69,6 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
password: get(collectionAuth, 'digest.password') password: get(collectionAuth, 'digest.password')
}; };
break; 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; break;
case 'bearer': case 'bearer':
axiosRequest.headers['authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`; axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`;
break; break;
case 'digest': case 'digest':
axiosRequest.digestConfig = { axiosRequest.digestConfig = {
@ -135,8 +105,10 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
case 'password': case 'password':
axiosRequest.oauth2 = { axiosRequest.oauth2 = {
grantType: grantType, grantType: grantType,
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
username: get(request, 'auth.oauth2.username'), 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; break;
case 'authorization_code': case 'authorization_code':
@ -147,14 +119,17 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'), accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
clientId: get(request, 'auth.oauth2.clientId'), clientId: get(request, 'auth.oauth2.clientId'),
clientSecret: get(request, 'auth.oauth2.clientSecret'), 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; break;
case 'client_credentials': case 'client_credentials':
axiosRequest.oauth2 = { axiosRequest.oauth2 = {
grantType: grantType, grantType: grantType,
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
clientId: get(request, 'auth.oauth2.clientId'), 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; break;
} }

View File

@ -20,6 +20,22 @@ class BrunoRequest {
return this.req.method; 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) { setMethod(method) {
this.req.method = 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 clientIdKey = _.find(auth, { name: 'client_id' });
const clientSecretKey = _.find(auth, { name: 'client_secret' }); const clientSecretKey = _.find(auth, { name: 'client_secret' });
const scopeKey = _.find(auth, { name: 'scope' }); const scopeKey = _.find(auth, { name: 'scope' });
const pkceKey = _.find(auth, { name: 'pkce' });
return { return {
auth: { auth: {
oauth2: oauth2:
grantTypeKey?.value && grantTypeKey?.value == 'password' grantTypeKey?.value && grantTypeKey?.value == 'password'
? { ? {
grantType: grantTypeKey ? grantTypeKey.value : '', grantType: grantTypeKey ? grantTypeKey.value : '',
accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',
username: usernameKey ? usernameKey.value : '', username: usernameKey ? usernameKey.value : '',
password: passwordKey ? passwordKey.value : '' password: passwordKey ? passwordKey.value : '',
scope: scopeKey ? scopeKey.value : ''
} }
: grantTypeKey?.value && grantTypeKey?.value == 'authorization_code' : grantTypeKey?.value && grantTypeKey?.value == 'authorization_code'
? { ? {
@ -409,13 +412,16 @@ const sem = grammar.createSemantics().addAttribute('ast', {
accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '', accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',
clientId: clientIdKey ? clientIdKey.value : '', clientId: clientIdKey ? clientIdKey.value : '',
clientSecret: clientSecretKey ? clientSecretKey.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' : grantTypeKey?.value && grantTypeKey?.value == 'client_credentials'
? { ? {
grantType: grantTypeKey ? grantTypeKey.value : '', grantType: grantTypeKey ? grantTypeKey.value : '',
accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',
clientId: clientIdKey ? clientIdKey.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 { const grammar = ohm.grammar(`Bru {
BruFile = (meta | query | headers | auth | auths | vars | script | tests | docs)* BruFile = (meta | query | headers | auth | auths | vars | script | tests | docs)*
auths = authawsv4 | authbasic | authbearer | authdigest auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2
nl = "\\r"? "\\n" nl = "\\r"? "\\n"
st = " " | "\\t" st = " " | "\\t"
@ -42,6 +42,7 @@ const grammar = ohm.grammar(`Bru {
authbasic = "auth:basic" dictionary authbasic = "auth:basic" dictionary
authbearer = "auth:bearer" dictionary authbearer = "auth:bearer" dictionary
authdigest = "auth:digest" dictionary authdigest = "auth:digest" dictionary
authOAuth2 = "auth:oauth2" dictionary
script = scriptreq | scriptres script = scriptreq | scriptres
scriptreq = "script:pre-request" st* "{" nl* textblock tagend 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) { varsreq(_1, dictionary) {
const vars = mapPairListToKeyValPairs(dictionary.ast); const vars = mapPairListToKeyValPairs(dictionary.ast);
_.each(vars, (v) => { _.each(vars, (v) => {

View File

@ -131,8 +131,10 @@ ${indentString(`password: ${auth?.digest?.password || ''}`)}
case 'password': case 'password':
bru += `auth:oauth2 { bru += `auth:oauth2 {
${indentString(`grant_type: password`)} ${indentString(`grant_type: password`)}
${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}
${indentString(`username: ${auth?.oauth2?.username || ''}`)} ${indentString(`username: ${auth?.oauth2?.username || ''}`)}
${indentString(`password: ${auth?.oauth2?.password || ''}`)} ${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_id: ${auth?.oauth2?.clientId || ''}`)}
${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)} ${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)}
${indentString(`scope: ${auth?.oauth2?.scope || ''}`)} ${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
${indentString(`pkce: ${(auth?.oauth2?.pkce || false).toString()}`)}
} }
`; `;
@ -153,8 +156,10 @@ ${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
case 'client_credentials': case 'client_credentials':
bru += `auth:oauth2 { bru += `auth:oauth2 {
${indentString(`grant_type: client_credentials`)} ${indentString(`grant_type: client_credentials`)}
${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}
${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)} ${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}
${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)} ${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 reqvars = _.get(vars, 'req');
let resvars = _.get(vars, 'res'); let resvars = _.get(vars, 'res');
if (reqvars && reqvars.length) { if (reqvars && reqvars.length) {

View File

@ -47,9 +47,9 @@ auth:digest {
auth:oauth2 { auth:oauth2 {
grant_type: authorization_code grant_type: authorization_code
callback_url: http://localhost:8080/api/auth/oauth2/ac/callback callback_url: http://localhost:8080/api/auth/oauth2/authorization_code/callback
authorization_url: http://localhost:8080/api/auth/oauth2/ac/authorize authorization_url: http://localhost:8080/api/auth/oauth2/authorization_code/authorize
access_token_url: http://localhost:8080/api/auth/oauth2/ac/token access_token_url: http://localhost:8080/api/auth/oauth2/authorization_code/token
client_id: client_id_1 client_id: client_id_1
client_secret: client_secret_1 client_secret: client_secret_1
scope: read write scope: read write

View File

@ -68,9 +68,9 @@
"grantType": "authorization_code", "grantType": "authorization_code",
"clientId": "client_id_1", "clientId": "client_id_1",
"clientSecret": "client_secret_1", "clientSecret": "client_secret_1",
"authorizationUrl": "http://localhost:8080/api/auth/oauth2/ac/authorize", "authorizationUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/authorize",
"callbackUrl": "http://localhost:8080/api/auth/oauth2/ac/callback", "callbackUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/callback",
"accessTokenUrl": "http://localhost:8080/api/auth/oauth2/ac/token", "accessTokenUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/token",
"scope": "read write" "scope": "read write"
} }
}, },

View File

@ -144,7 +144,7 @@ const oauth2Schema = Yup.object({
otherwise: Yup.string().nullable().strip() otherwise: Yup.string().nullable().strip()
}), }),
accessTokenUrl: Yup.string().when('grantType', { accessTokenUrl: Yup.string().when('grantType', {
is: (val) => ['authorization_code'].includes(val), is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val),
then: Yup.string().nullable(), then: Yup.string().nullable(),
otherwise: Yup.string().nullable().strip() otherwise: Yup.string().nullable().strip()
}), }),
@ -159,9 +159,14 @@ const oauth2Schema = Yup.object({
otherwise: Yup.string().nullable().strip() otherwise: Yup.string().nullable().strip()
}), }),
scope: Yup.string().when('grantType', { scope: Yup.string().when('grantType', {
is: (val) => ['authorization_code'].includes(val), is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val),
then: Yup.string().nullable(), then: Yup.string().nullable(),
otherwise: Yup.string().nullable().strip() 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) .noUnknown(true)

View File

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

View File

@ -4,11 +4,11 @@ vars {
basic_auth_password: della basic_auth_password: della
client_id: client_id_1 client_id: client_id_1
client_secret: client_secret_1 client_secret: client_secret_1
auth_url: http://localhost:8080/api/auth/oauth2/ac/authorize auth_url: http://localhost:8080/api/auth/oauth2/authorization_code/authorize
callback_url: http://localhost:8080/api/auth/oauth2/ac/callback callback_url: http://localhost:8080/api/auth/oauth2/authorization_code/callback
access_token_url: http://localhost:8080/api/auth/oauth2/ac/token access_token_url: http://localhost:8080/api/auth/oauth2/authorization_code/token
ropc_username: foo passwordCredentials_username: foo
ropc_password: bar passwordCredentials_password: bar
github_authorize_url: https://github.com/login/oauth/authorize github_authorize_url: https://github.com/login/oauth/authorize
github_access_token_url: https://github.com/login/oauth/access_token github_access_token_url: https://github.com/login/oauth/access_token
google_auth_url: https://accounts.google.com/o/oauth2/auth google_auth_url: https://accounts.google.com/o/oauth2/auth
@ -21,8 +21,8 @@ vars:secret [
google_client_id, google_client_id,
google_client_secret, google_client_secret,
github_authorization_code, github_authorization_code,
ropc_access_token, passwordCredentials_access_token,
cc_access_token, client_credentials_access_token,
ac_access_token, authorization_code_access_token,
github_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", "version": "1",
"name": "bruno-testbench", "name": "collection_oauth2",
"type": "collection", "type": "collection",
"proxy": {
"enabled": false,
"protocol": "http",
"hostname": "{{proxyHostname}}",
"port": 4000,
"auth": {
"enabled": false,
"username": "anoop",
"password": "password"
},
"bypassProxy": ""
},
"scripts": { "scripts": {
"moduleWhitelist": ["crypto"], "moduleWhitelist": ["crypto"],
"filesystemAccess": { "filesystemAccess": {
@ -25,7 +13,6 @@
"certs": [] "certs": []
}, },
"presets": { "presets": {
"requestType": "http", "requestType": "http"
"requestUrl": "http://localhost:6000"
} }
} }

View File

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

View File

@ -4,23 +4,29 @@ vars {
basic_auth_password: della basic_auth_password: della
client_id: client_id_1 client_id: client_id_1
client_secret: client_secret_1 client_secret: client_secret_1
auth_url: http://localhost:8080/api/auth/oauth2/ac/authorize password_credentials_access_token_url: http://localhost:8080/api/auth/oauth2/password_credentials/token
callback_url: http://localhost:8080/api/auth/oauth2/ac/callback password_credentials_username: foo
access_token_url: http://localhost:8080/api/auth/oauth2/ac/token password_credentials_password: bar
ropc_username: foo password_credentials_scope:
ropc_password: bar authorization_code_authorize_url: http://localhost:8080/api/auth/oauth2/authorization_code/authorize
github_authorize_url: https://github.com/login/oauth/authorize authorization_code_callback_url: http://localhost:8080/api/auth/oauth2/authorization_code/callback
github_access_token_url: https://github.com/login/oauth/access_token authorization_code_access_token_url: http://localhost:8080/api/auth/oauth2/authorization_code/token
google_auth_url: https://accounts.google.com/o/oauth2/auth authorization_code_google_auth_url: https://accounts.google.com/o/oauth2/auth
google_access_token_url: https://accounts.google.com/o/oauth2/token authorization_code_google_access_token_url: https://accounts.google.com/o/oauth2/token
google_scope: https://www.googleapis.com/auth/userinfo.email 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 [ vars:secret [
github_client_secret, authorization_code_google_client_id,
github_client_id, authorization_code_google_client_secret,
google_client_id, authorization_code_github_client_secret,
google_client_secret, authorization_code_github_client_id,
github_authorization_code, authorization_code_github_authorization_code,
github_access_token, authorization_code_github_access_token
ac_access_token
] ]

View File

@ -4,13 +4,13 @@ const router = express.Router();
const authBearer = require('./bearer'); const authBearer = require('./bearer');
const authBasic = require('./basic'); const authBasic = require('./basic');
const authCookie = require('./cookie'); const authCookie = require('./cookie');
const authOAuth2Ropc = require('./oauth2/ropc'); const authOAuth2PasswordCredentials = require('./oauth2/passwordCredentials');
const authOAuth2AuthorizationCode = require('./oauth2/ac'); const authOAuth2AuthorizationCode = require('./oauth2/authorizationCode');
const authOAuth2Cc = require('./oauth2/cc'); const authOAuth2ClientCredentials = require('./oauth2/clientCredentials');
router.use('/oauth2/ropc', authOAuth2Ropc); router.use('/oauth2/password_credentials', authOAuth2PasswordCredentials);
router.use('/oauth2/ac', authOAuth2AuthorizationCode); router.use('/oauth2/authorization_code', authOAuth2AuthorizationCode);
router.use('/oauth2/cc', authOAuth2Cc); router.use('/oauth2/client_credentials', authOAuth2ClientCredentials);
router.use('/bearer', authBearer); router.use('/bearer', authBearer);
router.use('/basic', authBasic); router.use('/basic', authBasic);
router.use('/cookie', authCookie); router.use('/cookie', authCookie);

View File

@ -17,8 +17,16 @@ function generateUniqueString() {
return crypto.randomBytes(16).toString('hex'); 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) => { 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') { if (response_type !== 'code') {
return res.status(401).json({ error: 'Invalid Response type, expected "code"' }); return res.status(401).json({ error: 'Invalid Response type, expected "code"' });
} }
@ -37,7 +45,8 @@ router.get('/authorize', (req, res) => {
authCodes.push({ authCodes.push({
authCode: authorization_code, authCode: authorization_code,
client_id, client_id,
redirect_uri redirect_uri,
code_challenge
}); });
const redirectUrl = `${redirect_uri}?code=${authorization_code}`; const redirectUrl = `${redirect_uri}?code=${authorization_code}`;
@ -72,6 +81,7 @@ router.get('/authorize', (req, res) => {
// Handle the authorization callback // Handle the authorization callback
router.get('/callback', (req, res) => { router.get('/callback', (req, res) => {
console.log('authorization code callback', req.query);
const { code } = req.query; const { code } = req.query;
// Check if the authCode is valid. // Check if the authCode is valid.
@ -85,13 +95,15 @@ router.get('/callback', (req, res) => {
}); });
router.post('/token', (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) { if (req?.body?.grant_type) {
grant_type = req?.body?.grant_type; grant_type = req?.body?.grant_type;
code = req?.body?.code; code = req?.body?.code;
redirect_uri = req?.body?.redirect_uri; redirect_uri = req?.body?.redirect_uri;
client_id = req?.body?.client_id; client_id = req?.body?.client_id;
client_secret = req?.body?.client_secret; client_secret = req?.body?.client_secret;
code_verifier = req?.body?.code_verifier;
} }
if (req?.headers?.grant_type) { if (req?.headers?.grant_type) {
grant_type = 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; redirect_uri = req?.headers?.redirect_uri;
client_id = req?.headers?.client_id; client_id = req?.headers?.client_id;
client_secret = req?.headers?.client_secret; client_secret = req?.headers?.client_secret;
code_verifier = req?.headers?.code_verifier;
} }
if (grant_type !== 'authorization_code') { if (grant_type !== 'authorization_code') {
@ -110,7 +123,13 @@ router.post('/token', (req, res) => {
// return res.status(401).json({ error: 'Invalid client credentials' }); // 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) { if (!storedAuthCode) {
return res.status(401).json({ error: 'Invalid Authorization Code' }); return res.status(401).json({ error: 'Invalid Authorization Code' });
@ -127,6 +146,7 @@ router.post('/token', (req, res) => {
router.post('/resource', (req, res) => { router.post('/resource', (req, res) => {
try { try {
console.log('authorization code resource', req.query, tokens);
const { token } = req.query; const { token } = req.query;
const storedToken = tokens.find((t) => t.accessToken === token); const storedToken = tokens.find((t) => t.accessToken === token);
if (!storedToken) { if (!storedToken) {

View File

@ -4,7 +4,8 @@ const crypto = require('crypto');
const clients = [ const clients = [
{ {
client_id: 'client_id_1', 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) => { 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) { if (req?.body?.grant_type) {
grant_type = req?.body?.grant_type; grant_type = req?.body?.grant_type;
client_id = req?.body?.client_id; client_id = req?.body?.client_id;
client_secret = req?.body?.client_secret; client_secret = req?.body?.client_secret;
scope = req?.body?.scope;
} else if (req?.headers?.grant_type) { } else if (req?.headers?.grant_type) {
grant_type = req?.headers?.grant_type; grant_type = req?.headers?.grant_type;
client_id = req?.headers?.client_id; client_id = req?.headers?.client_id;
client_secret = req?.headers?.client_secret; client_secret = req?.headers?.client_secret;
scope = req?.headers?.scope;
} }
console.log('client_cred', client_id, client_secret, scope);
if (grant_type !== 'client_credentials') { if (grant_type !== 'client_credentials') {
return res.status(401).json({ error: 'Invalid Grant Type, expected "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) { if (!client) {
return res.status(401).json({ error: 'Invalid client' }); return res.status(401).json({ error: 'Invalid client details or scope' });
} }
const token = generateUniqueString(); const token = generateUniqueString();
tokens.push({ tokens.push({
token, token,
client_id, 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) => { 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) => { 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') { if (grant_type !== 'password') {
return res.status(401).json({ error: 'Invalid Grant Type' }); return res.status(401).json({ error: 'Invalid Grant Type' });