Skip to content

Commit

Permalink
Merge pull request #817 from drinkbird/feature/digestauth
Browse files Browse the repository at this point in the history
Add Digest Auth Support #119
  • Loading branch information
helloanoop authored Nov 3, 2023
2 parents 0ce3dee + 3838848 commit 64923e4
Show file tree
Hide file tree
Showing 25 changed files with 401 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ const AuthMode = ({ collection }) => {
>
Bearer Token
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('digest');
}}
>
Digest Auth
</div>
<div
className="dropdown-item"
onClick={() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import styled from 'styled-components';

const Wrapper = styled.div`
label {
font-size: 0.8125rem;
}
.single-line-editor-wrapper {
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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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 { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';

const DigestAuth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();

const digestAuth = get(collection, 'root.request.auth.digest', {});

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

const handleUsernameChange = (username) => {
dispatch(
updateCollectionAuth({
mode: 'digest',
collectionUid: collection.uid,
content: {
username: username,
password: digestAuth.password
}
})
);
};

const handlePasswordChange = (password) => {
dispatch(
updateCollectionAuth({
mode: 'digest',
collectionUid: collection.uid,
content: {
username: digestAuth.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={digestAuth.username || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleUsernameChange(val)}
collection={collection}
/>
</div>

<label className="block font-medium mb-2">Password</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={digestAuth.password || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handlePasswordChange(val)}
collection={collection}
/>
</div>
</StyledWrapper>
);
};

export default DigestAuth;
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import AuthMode from './AuthMode';
import AwsV4Auth from './AwsV4Auth';
import BearerAuth from './BearerAuth';
import BasicAuth from './BasicAuth';
import DigestAuth from './DigestAuth';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';

Expand All @@ -25,6 +26,9 @@ const Auth = ({ collection }) => {
case 'bearer': {
return <BearerAuth collection={collection} />;
}
case 'digest': {
return <DigestAuth collection={collection} />;
}
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ const AuthMode = ({ item, collection }) => {
>
Bearer Token
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('digest');
}}
>
Digest Auth
</div>
<div
className="dropdown-item"
onClick={() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import styled from 'styled-components';

const Wrapper = styled.div`
label {
font-size: 0.8125rem;
}
.single-line-editor-wrapper {
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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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 DigestAuth = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();

const digestAuth = item.draft ? get(item, 'draft.request.auth.digest', {}) : get(item, 'request.auth.digest', {});

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

const handleUsernameChange = (username) => {
dispatch(
updateAuth({
mode: 'digest',
collectionUid: collection.uid,
itemUid: item.uid,
content: {
username: username,
password: digestAuth.password
}
})
);
};

const handlePasswordChange = (password) => {
dispatch(
updateAuth({
mode: 'digest',
collectionUid: collection.uid,
itemUid: item.uid,
content: {
username: digestAuth.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={digestAuth.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={digestAuth.password || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handlePasswordChange(val)}
onRun={handleRun}
collection={collection}
/>
</div>
</StyledWrapper>
);
};

export default DigestAuth;
4 changes: 4 additions & 0 deletions packages/bruno-app/src/components/RequestPane/Auth/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import AuthMode from './AuthMode';
import AwsV4Auth from './AwsV4Auth';
import BearerAuth from './BearerAuth';
import BasicAuth from './BasicAuth';
import DigestAuth from './DigestAuth';
import StyledWrapper from './StyledWrapper';

const Auth = ({ item, collection }) => {
Expand All @@ -20,6 +21,9 @@ const Auth = ({ item, collection }) => {
case 'bearer': {
return <BearerAuth collection={collection} item={item} />;
}
case 'digest': {
return <DigestAuth collection={collection} item={item} />;
}
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,10 @@ export const collectionsSlice = createSlice({
item.draft.request.auth.mode = 'basic';
item.draft.request.auth.basic = action.payload.content;
break;
case 'digest':
item.draft.request.auth.mode = 'digest';
item.draft.request.auth.digest = action.payload.content;
break;
}
}
}
Expand Down Expand Up @@ -976,6 +980,9 @@ export const collectionsSlice = createSlice({
case 'basic':
set(collection, 'root.request.auth.basic', action.payload.content);
break;
case 'digest':
set(collection, 'root.request.auth.digest', action.payload.content);
break;
}
}
},
Expand Down
4 changes: 4 additions & 0 deletions packages/bruno-app/src/utils/collections/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,10 @@ export const humanizeRequestAuthMode = (mode) => {
label = 'Bearer Token';
break;
}
case 'digest': {
label = 'Digest Auth';
break;
}
}

return label;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ const transformInsomniaRequestItem = (request, index, allRequests) => {
auth: {
mode: 'none',
basic: null,
bearer: null
bearer: null,
digest: null
},
headers: [],
params: [],
Expand Down
3 changes: 2 additions & 1 deletion packages/bruno-app/src/utils/importers/openapi-collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ const transformOpenapiRequestItem = (request) => {
auth: {
mode: 'none',
basic: null,
bearer: null
bearer: null,
digest: null
},
headers: [],
params: [],
Expand Down
79 changes: 79 additions & 0 deletions packages/bruno-electron/src/ipc/network/digestauth-helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const crypto = require('crypto');

function isStrPresent(str) {
return str && str !== '' && str !== 'undefined';
}

function stripQuotes(str) {
return str.replace(/"/g, '');
}

function containsDigestHeader(response) {
const authHeader = response?.headers?.['www-authenticate'];
return authHeader ? authHeader.trim().toLowerCase().startsWith('digest') : false;
}

function containsAuthorizationHeader(originalRequest) {
return Boolean(originalRequest.headers['Authorization']);
}

function md5(input) {
return crypto.createHash('md5').update(input).digest('hex');
}

function addDigestInterceptor(axiosInstance, request) {
const { username, password } = request.digestConfig;

console.debug(request);

if (!isStrPresent(username) || !isStrPresent(password)) {
console.warn('Required Digest Auth fields are not present');
return;
}

axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
const originalRequest = error.config;

if (
error.response?.status === 401 &&
containsDigestHeader(error.response) &&
!containsAuthorizationHeader(originalRequest)
) {
console.debug(error.response.headers['www-authenticate']);

const authDetails = error.response.headers['www-authenticate']
.split(', ')
.map((v) => v.split('=').map(stripQuotes))
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
console.debug(authDetails);

const nonceCount = '00000001';
const cnonce = crypto.randomBytes(24).toString('hex');

if (authDetails.algorithm.toUpperCase() !== 'MD5') {
console.warn(`Unsupported Digest algorithm: ${algo}`);
return Promise.reject(error);
}
const HA1 = md5(`${username}:${authDetails['Digest realm']}:${password}`);
const HA2 = md5(`${request.method}:${request.url}`);
const response = md5(`${HA1}:${authDetails.nonce}:${nonceCount}:${cnonce}:auth:${HA2}`);

const authorizationHeader =
`Digest username="${username}",realm="${authDetails['Digest realm']}",` +
`nonce="${authDetails.nonce}",uri="${request.url}",qop="auth",algorithm="${authDetails.algorithm}",` +
`response="${response}",nc="${nonceCount}",cnonce="${cnonce}"`;
originalRequest.headers['Authorization'] = authorizationHeader;
console.debug(`Authorization: ${originalRequest.headers['Authorization']}`);

delete originalRequest.digestConfig;
return axiosInstance(originalRequest);
}

return Promise.reject(error);
}
);
}

module.exports = { addDigestInterceptor };
Loading

0 comments on commit 64923e4

Please sign in to comment.