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

Add Digest Auth Support #119 #817

Merged
merged 1 commit into from
Nov 3, 2023
Merged
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 @@ -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