Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement OAuth1 authorization method #2989

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/bruno-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"html-loader": "^3.0.1",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.4.5",
"oauth-1.0a": "^2.2.6",
"postcss": "8.4.47",
"style-loader": "^3.3.1",
"tailwindcss": "^3.4.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const AuthMode = ({ collection }) => {
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const authMode = get(collection, 'root.request.auth.mode');

const authModes = ['awsv4', 'basic', 'bearer', 'digest', 'ntlm', 'oauth1', 'oauth2', 'wsse', 'apikey', 'none'];

const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-center auth-mode-label select-none">
Expand All @@ -34,87 +36,21 @@ const AuthMode = ({ collection }) => {
<StyledWrapper>
<div className="inline-flex items-center cursor-pointer auth-mode-selector">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('awsv4');
}}
>
AWS Sig v4
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('basic');
}}
>
Basic Auth
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('wsse');
}}
>
WSSE Auth
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('bearer');
}}
>
Bearer Token
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('digest');
}}
>
Digest Auth
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('ntlm');
}}
>
NTLM Auth
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('oauth2');
}}
>
Oauth2
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('apikey');
}}
>
API Key
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('none');
}}
>
No Auth
</div>
{authModes.map((mode) => {
return (
<>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange(mode);
}}
>
{humanizeRequestAuthMode(mode)}
</div>
</>
);
})}
</Dropdown>
</div>
</StyledWrapper>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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};
}
.file-picker-wrapper {
max-width: 400px;
border-radius: 3px;
}
`;

export default Wrapper;
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import React, { forwardRef, useCallback, useRef } from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
import { useTranslation } from 'react-i18next';
import Dropdown from 'components/Dropdown';
import { IconCaretDown } from '@tabler/icons';
import FilePickerEditor from 'components/FilePickerEditor';
import path from 'path';
import { isWindowsOS } from 'utils/common/platform';
import slash from 'utils/common/slash';

const OAuth1 = ({ collection }) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const { storedTheme } = useTheme();

const oAuth1 = get(collection, 'root.request.auth.oauth1', {});

const handleSave = () => dispatch(saveCollectionRoot(collection.uid));

const refs = useRef(new Map());
const setRef = useCallback((ref, key) => {
if (ref) {
refs.current.set(key, ref);
} else {
refs.current.delete(key);
}
}, []);

const hideDropdown = (key) => {
const dropdown = refs.current.get(key);
if (dropdown?.hide) {
dropdown.hide();
}
};

const handleChange = (key, val) => {
console.log(key, val);
dispatch(
updateCollectionAuth({
mode: 'oauth1',
collectionUid: collection.uid,
content: {
...oAuth1, [key]: val
}
})
);
};

const relativeFile = (file) => {
if (file) {
if (isWindowsOS()) {
return slash((path.win32.relative(collection.pathname, file)));
} else {
return path.posix.relative(collection.pathname, file);
}
} else {
return '';
}
};

const optionDisplayName = (options, key) => {
const option = (options.find(option => option.key === key));
return option?.label ? t(option.label) : key;
};

return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
{inputsConfig.map((input) => {
const { key, label, type, options } = input;
return (
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
<label className="block font-medium">{t(label)}</label>
{type === 'Dropdown' ?
<div className="inline-flex items-center cursor-pointer grant-type-mode-selector w-fit">
<Dropdown icon={(<div
className="flex items-center justify-end grant-type-label select-none">{optionDisplayName(options, oAuth1[key])}
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} /></div>)}
onCreate={(ref) => setRef(ref, key)}
placement="bottom-end"
children={options.map((option) => (
<div
className="dropdown-item"
onClick={() => {
hideDropdown(key);
handleChange(key, option.key ? option.key : option);
}}>
{option.label ? t(option.label) : option}
</div>
))}
/>
</div>
: ''}
{type === 'SingleLineEditor' ?
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={oAuth1[key] || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange(key, val)}
collection={collection}
/>
</div>
: ''}
{type === 'FilePickerEditor' ?
<div className="file-picker-wrapper">
<FilePickerEditor
value={[oAuth1[key]] || []}
onChange={(val) => handleChange(key, relativeFile(val[0]))}
collection={collection}
/>
</div>
: ''}
</div>
);
})}
</StyledWrapper>
);
};

export default OAuth1;
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const inputsConfig = [
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of these inputs are required to fill (consumer key/secret), and some will be filled by Bruno during execution if found empty (verifier, access token). Some will only make sense under certain conditions (rsa-private key is only needed when using RSA signing method, but then it's mandatory).

The UX improvements like hints or auto-disabling controls depending on other choices may be introduced in the future, but I guess it's better left out of this PR.

{
key: 'consumerKey',
label: 'AUTHORIZATION.OAUTH1.CONSUMER_KEY_FIELD',
type: 'SingleLineEditor'
},
{
key: 'consumerSecret',
label: 'AUTHORIZATION.OAUTH1.CONSUMER_SECRET_FIELD',
type: 'SingleLineEditor'
},
{
key: 'requestTokenUrl',
label: 'AUTHORIZATION.OAUTH1.REQUEST_TOKEN_URL_FIELD',
type: 'SingleLineEditor'
},
{
key: 'accessTokenUrl',
label: 'AUTHORIZATION.OAUTH1.ACCESS_TOKEN_URL_FIELD',
type: 'SingleLineEditor'
},
{
key: 'authorizeUrl',
label: 'AUTHORIZATION.OAUTH1.AUTHORIZE_URL_FIELD',
type: 'SingleLineEditor'
},
{
key: 'callbackUrl',
label: 'AUTHORIZATION.OAUTH1.CALLBACK_TOKEN_URL_FIELD',
type: 'SingleLineEditor'
},
{
key: 'verifier',
label: 'AUTHORIZATION.OAUTH1.OAUTH_VERIFIER_FIELD',
type: 'SingleLineEditor'
},
{
key: 'accessToken',
label: 'AUTHORIZATION.OAUTH1.ACCESS_TOKEN_FIELD',
type: 'SingleLineEditor'
},
{
key: 'accessTokenSecret',
label: 'AUTHORIZATION.OAUTH1.ACCESS_TOKEN_SECRET_FIELD',
type: 'SingleLineEditor'
},
{
key: 'rsaPrivateKey',
label: 'AUTHORIZATION.OAUTH1.RSA_PRIVATE_KEY_FIELD',
type: 'FilePickerEditor'
},
{
key: 'signatureMethod',
label: 'AUTHORIZATION.OAUTH1.SIGNATURE_METHOD_FIELD',
type: 'Dropdown',
options: ['HMAC-SHA1', 'HMAC-SHA256', 'HMAC-SHA512', 'RSA-SHA1', 'RSA-SHA256', 'RSA-SHA512', 'PLAINTEXT']
},
{
key: 'parameterTransmissionMethod',
label: 'AUTHORIZATION.OAUTH1.PARAM_TRANSMISSION_METHOD_FIELD',
type: 'Dropdown',
options: [
{
key: 'authorization_header',
label: 'AUTHORIZATION.OAUTH1.PARAM_TRANSMISSION_METHOD.AUTHORIZATION_HEADER'
},
{
key: 'request_body',
label: 'AUTHORIZATION.OAUTH1.PARAM_TRANSMISSION_METHOD.REQUEST_BODY'
},
{
key: 'query_param',
label: 'AUTHORIZATION.OAUTH1.PARAM_TRANSMISSION_METHOD.QUERY_PARAM'
}
]
}
];

export { inputsConfig };
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import ApiKeyAuth from './ApiKeyAuth/';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import OAuth2 from './OAuth2';
import OAuth1 from 'components/CollectionSettings/Auth/OAuth1';
import NTLMAuth from './NTLMAuth';


const Auth = ({ collection }) => {
const authMode = get(collection, 'root.request.auth.mode');
const dispatch = useDispatch();
Expand All @@ -36,7 +36,10 @@ const Auth = ({ collection }) => {
}
case 'ntlm': {
return <NTLMAuth collection={collection} />;
}
}
case 'oauth1': {
return <OAuth1 collection={collection} />;
}
case 'oauth2': {
return <OAuth2 collection={collection} />;
}
Expand Down
Loading
Loading