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: OAuth 2.0 Client Credentials Basic Auth #2164

Open
wants to merge 4 commits into
base: fix/oauth2
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import SingleLineEditor from 'components/SingleLineEditor';
import { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import ClientCredentialsMethodSelector from 'components/RequestPane/Auth/OAuth2/ClientCredentialsMethodSelector';

const OAuth2AuthorizationCode = ({ collection }) => {
const dispatch = useDispatch();
Expand All @@ -20,7 +21,8 @@ const OAuth2AuthorizationCode = ({ collection }) => {

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

const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, state, pkce } = oAuth;
const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, clientSecretMethod, scope, state, pkce } =
oAuth;

const handleChange = (key, value) => {
dispatch(
Expand All @@ -34,6 +36,7 @@ const OAuth2AuthorizationCode = ({ collection }) => {
accessTokenUrl,
clientId,
clientSecret,
clientSecretMethod,
scope,
state,
pkce,
Expand All @@ -55,6 +58,7 @@ const OAuth2AuthorizationCode = ({ collection }) => {
accessTokenUrl,
clientId,
clientSecret,
clientSecretMethod,
scope,
state,
pkce: !Boolean(oAuth?.['pkce'])
Expand Down Expand Up @@ -92,6 +96,7 @@ const OAuth2AuthorizationCode = ({ collection }) => {
onChange={handlePKCEToggle}
/>
</div>
<ClientCredentialsMethodSelector collection={collection} oAuth={oAuth} />
</StyledWrapper>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import SingleLineEditor from 'components/SingleLineEditor';
import { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import ClientCredentialsMethodSelector from 'components/RequestPane/Auth/OAuth2/ClientCredentialsMethodSelector';

const OAuth2ClientCredentials = ({ collection }) => {
const dispatch = useDispatch();
Expand All @@ -20,7 +21,7 @@ const OAuth2ClientCredentials = ({ collection }) => {

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

const { accessTokenUrl, clientId, clientSecret, scope } = oAuth;
const { accessTokenUrl, clientId, clientSecret, clientSecretMethod, scope } = oAuth;

const handleChange = (key, value) => {
dispatch(
Expand All @@ -32,6 +33,7 @@ const OAuth2ClientCredentials = ({ collection }) => {
accessTokenUrl,
clientId,
clientSecret,
clientSecretMethod,
scope,
[key]: value
}
Expand Down Expand Up @@ -60,6 +62,7 @@ const OAuth2ClientCredentials = ({ collection }) => {
</div>
);
})}
<ClientCredentialsMethodSelector collection={collection} oAuth={oAuth} />
</StyledWrapper>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/Redux
import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import ClientCredentialsMethodSelector from 'components/RequestPane/Auth/OAuth2/ClientCredentialsMethodSelector';

const OAuth2PasswordCredentials = ({ collection }) => {
const dispatch = useDispatch();
Expand All @@ -20,7 +21,7 @@ const OAuth2PasswordCredentials = ({ collection }) => {

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

const { accessTokenUrl, username, password, clientId, clientSecret, scope } = oAuth;
const { accessTokenUrl, username, password, clientId, clientSecret, clientSecretMethod, scope } = oAuth;

const handleChange = (key, value) => {
dispatch(
Expand All @@ -34,6 +35,7 @@ const OAuth2PasswordCredentials = ({ collection }) => {
password,
clientId,
clientSecret,
clientSecretMethod,
scope,
[key]: value
}
Expand Down Expand Up @@ -62,6 +64,7 @@ const OAuth2PasswordCredentials = ({ collection }) => {
</div>
);
})}
<ClientCredentialsMethodSelector collection={collection} oAuth={oAuth} />
</StyledWrapper>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
import ClientCredentialsMethodSelector from 'components/RequestPane/Auth/OAuth2/ClientCredentialsMethodSelector';

const OAuth2AuthorizationCode = ({ item, collection }) => {
const dispatch = useDispatch();
Expand All @@ -20,7 +21,8 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {

const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));

const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, state, pkce } = oAuth;
const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, clientSecretMethod, scope, state, pkce } =
oAuth;

const handleChange = (key, value) => {
dispatch(
Expand All @@ -35,6 +37,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
accessTokenUrl,
clientId,
clientSecret,
clientSecretMethod,
state,
scope,
pkce,
Expand All @@ -57,6 +60,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
accessTokenUrl,
clientId,
clientSecret,
clientSecretMethod,
state,
scope,
pkce: !Boolean(oAuth?.['pkce'])
Expand Down Expand Up @@ -96,6 +100,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
onChange={handlePKCEToggle}
/>
</div>
<ClientCredentialsMethodSelector item={item} collection={collection} oAuth={oAuth} />
</StyledWrapper>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
import ClientCredentialsMethodSelector from 'components/RequestPane/Auth/OAuth2/ClientCredentialsMethodSelector';

const OAuth2ClientCredentials = ({ item, collection }) => {
const dispatch = useDispatch();
Expand All @@ -20,7 +21,7 @@ const OAuth2ClientCredentials = ({ item, collection }) => {

const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));

const { accessTokenUrl, clientId, clientSecret, scope } = oAuth;
const { accessTokenUrl, clientId, clientSecret, clientSecretMethod, scope } = oAuth;

const handleChange = (key, value) => {
dispatch(
Expand All @@ -33,6 +34,7 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
accessTokenUrl,
clientId,
clientSecret,
clientSecretMethod,
scope,
[key]: value
}
Expand Down Expand Up @@ -62,6 +64,7 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
</div>
);
})}
<ClientCredentialsMethodSelector item={item} collection={collection} oAuth={oAuth} />
</StyledWrapper>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import styled from 'styled-components';

const Wrapper = styled.div`
font-size: 0.8125rem;

.client-credentials-secret-mode-selector {
padding: 0.5rem 0px;
border-radius: 3px;
border: solid 1px ${(props) => props.theme.input.border};
background-color: ${(props) => props.theme.input.bg};

.client-credentials-secret-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;
}
}
`;

export default Wrapper;
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React, { forwardRef, useEffect, useRef } from 'react';
import Dropdown from 'components/Dropdown';
import StyledWrapper from './StyledWrapper';
import { IconCaretDown } from '@tabler/icons';
import { updateAuth, updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { useDispatch } from 'react-redux';
import { humanizeOAuth2ClientSecretMethod } from 'utils/collections';

const ClientCredentialsMethodSelector = ({ item, collection, oAuth }) => {
Copy link
Contributor Author

@pietrygamat pietrygamat Apr 25, 2024

Choose a reason for hiding this comment

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

The same component is used in RequestPane and in CollectionSettings/Auth. While the convention in the project seems to be using duplicate, almost identical components, I feel this approach is easier to manage. Perhaps it should be moved into a different location then?

Copy link
Contributor

Choose a reason for hiding this comment

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

seconded - perhaps we should have a shared components directory

const clientSecretMethods = ['client_credentials_basic', 'client_credentials_post'];

const dispatch = useDispatch();
const dropDownRef = useRef();
const onDropdownCreate = (ref) => (dropDownRef.current = ref);

const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-end client-credentials-secret-label select-none">
{humanizeOAuth2ClientSecretMethod(oAuth?.clientSecretMethod)}{' '}
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
</div>
);
});

const onClientSecretMethodChange = (clientSecretMethod) => {
if (item) {
// Update request level authentication
dispatch(
updateAuth({
mode: 'oauth2',
collectionUid: collection.uid,
itemUid: item.uid,
content: {
...oAuth,
clientSecretMethod: clientSecretMethod
}
})
);
} else {
// Update collection level authentication
dispatch(
updateCollectionAuth({
mode: 'oauth2',
collectionUid: collection.uid,
content: {
...oAuth,
clientSecretMethod: clientSecretMethod
}
})
);
}
};

useEffect(() => {
!oAuth?.clientSecretMethod && onClientSecretMethodChange(clientSecretMethods[0]);
}, [oAuth.clientSecretMethod]);

return (
<StyledWrapper>
<label className="block font-medium mb-2">Send Client Credentials</label>
<div className="inline-flex items-center cursor-pointer client-credentials-secret-mode-selector w-fit">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
{clientSecretMethods.map((item, index) => (
<div
key={item}
className="dropdown-item"
onClick={() => {
dropDownRef.current.hide();
onClientSecretMethodChange(item);
}}
>
{' '}
{humanizeOAuth2ClientSecretMethod(item)}
{index === 0 ? ` (Default)` : ``}
</div>
))}
</Dropdown>
</div>
</StyledWrapper>
);
};
export default ClientCredentialsMethodSelector;
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
Expand All @@ -7,6 +7,7 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
import ClientCredentialsMethodSelector from 'components/RequestPane/Auth/OAuth2/ClientCredentialsMethodSelector';

const OAuth2PasswordCredentials = ({ item, collection }) => {
const dispatch = useDispatch();
Expand All @@ -20,7 +21,7 @@ const OAuth2PasswordCredentials = ({ item, collection }) => {

const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));

const { accessTokenUrl, username, password, clientId, clientSecret, scope } = oAuth;
const { accessTokenUrl, username, password, clientId, clientSecret, clientSecretMethod, scope } = oAuth;

const handleChange = (key, value) => {
dispatch(
Expand All @@ -35,6 +36,7 @@ const OAuth2PasswordCredentials = ({ item, collection }) => {
password,
clientId,
clientSecret,
clientSecretMethod,
scope,
[key]: value
}
Expand Down Expand Up @@ -64,6 +66,7 @@ const OAuth2PasswordCredentials = ({ item, collection }) => {
</div>
);
})}
<ClientCredentialsMethodSelector item={item} collection={collection} oAuth={oAuth} />
</StyledWrapper>
);
};
Expand Down
18 changes: 17 additions & 1 deletion packages/bruno-app/src/utils/collections/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export const moveCollectionItemToRootOfCollection = (collection, draggedItem) =>
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
collection.items = sortBy(collection.items, (item) => item.seq);
collection.items.push(draggedItem);
if (draggedItem.type == 'folder') {
if (draggedItem.type === 'folder') {
draggedItem.pathname = path.join(collection.pathname, draggedItem.name);
} else {
draggedItem.pathname = path.join(collection.pathname, draggedItem.filename);
Expand Down Expand Up @@ -733,6 +733,22 @@ export const humanizeGrantType = (mode) => {
return label;
};

export const humanizeOAuth2ClientSecretMethod = (mode) => {
let label = 'N/A';
switch (mode) {
case 'client_credentials_basic': {
label = 'As Basic Auth Header';
break;
}
case 'client_credentials_post': {
label = 'In Request Body';
break;
}
}

return label;
};
Comment on lines +736 to +750
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO: move to i18n/translation, but this should best be done in separate PR


export const refreshUidsInItem = (item) => {
item.uid = uuid();

Expand Down
Loading
Loading