diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js index 5cf3a5f7a..747ee4d61 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js @@ -61,6 +61,15 @@ const AuthMode = ({ collection }) => { > Bearer Token +
{ + dropdownTippyRef.current.hide(); + onModeChange('digest'); + }} + > + Digest Auth +
{ diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/StyledWrapper.js new file mode 100644 index 000000000..c2bb5d207 --- /dev/null +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/StyledWrapper.js @@ -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; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/index.js new file mode 100644 index 000000000..3e084c06d --- /dev/null +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/index.js @@ -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 ( + + +
+ handleUsernameChange(val)} + collection={collection} + /> +
+ + +
+ handlePasswordChange(val)} + collection={collection} + /> +
+
+ ); +}; + +export default DigestAuth; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/index.js index d9e80358b..7873cfcd0 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/index.js @@ -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'; @@ -25,6 +26,9 @@ const Auth = ({ collection }) => { case 'bearer': { return ; } + case 'digest': { + return ; + } } }; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js b/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js index 10130c3a9..8eb4fee90 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js @@ -62,6 +62,15 @@ const AuthMode = ({ item, collection }) => { > Bearer Token
+
{ + dropdownTippyRef.current.hide(); + onModeChange('digest'); + }} + > + Digest Auth +
{ diff --git a/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/StyledWrapper.js new file mode 100644 index 000000000..c2bb5d207 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/StyledWrapper.js @@ -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; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js new file mode 100644 index 000000000..e43f18c46 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js @@ -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 ( + + +
+ handleUsernameChange(val)} + onRun={handleRun} + collection={collection} + /> +
+ + +
+ handlePasswordChange(val)} + onRun={handleRun} + collection={collection} + /> +
+
+ ); +}; + +export default DigestAuth; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/index.js index 669f4a843..bd388737e 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/index.js @@ -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 }) => { @@ -20,6 +21,9 @@ const Auth = ({ item, collection }) => { case 'bearer': { return ; } + case 'digest': { + return ; + } } }; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 4ad30d204..24655f4f0 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -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; } } } @@ -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; } } }, diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index 0b98fbc28..05dd0fb43 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -494,6 +494,10 @@ export const humanizeRequestAuthMode = (mode) => { label = 'Bearer Token'; break; } + case 'digest': { + label = 'Digest Auth'; + break; + } } return label; diff --git a/packages/bruno-app/src/utils/importers/insomnia-collection.js b/packages/bruno-app/src/utils/importers/insomnia-collection.js index 8d8113b49..80e9bb0a0 100644 --- a/packages/bruno-app/src/utils/importers/insomnia-collection.js +++ b/packages/bruno-app/src/utils/importers/insomnia-collection.js @@ -80,7 +80,8 @@ const transformInsomniaRequestItem = (request, index, allRequests) => { auth: { mode: 'none', basic: null, - bearer: null + bearer: null, + digest: null }, headers: [], params: [], diff --git a/packages/bruno-app/src/utils/importers/openapi-collection.js b/packages/bruno-app/src/utils/importers/openapi-collection.js index 1ca6e25b7..20a915bd4 100644 --- a/packages/bruno-app/src/utils/importers/openapi-collection.js +++ b/packages/bruno-app/src/utils/importers/openapi-collection.js @@ -69,7 +69,8 @@ const transformOpenapiRequestItem = (request) => { auth: { mode: 'none', basic: null, - bearer: null + bearer: null, + digest: null }, headers: [], params: [], diff --git a/packages/bruno-electron/src/ipc/network/digestauth-helper.js b/packages/bruno-electron/src/ipc/network/digestauth-helper.js new file mode 100644 index 000000000..fdcf77cc2 --- /dev/null +++ b/packages/bruno-electron/src/ipc/network/digestauth-helper.js @@ -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 }; diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index daace4792..6a4969931 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -23,6 +23,7 @@ const { HttpProxyAgent } = require('http-proxy-agent'); const { SocksProxyAgent } = require('socks-proxy-agent'); const { makeAxiosInstance } = require('./axios-instance'); const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper'); +const { addDigestInterceptor } = require('./digestauth-helper'); const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../../utils/proxy-util'); // override the default escape function to prevent escaping @@ -168,6 +169,10 @@ const configureRequest = async (collectionUid, request, envVars, collectionVaria delete request.awsv4config; } + if (request.digestConfig) { + addDigestInterceptor(axiosInstance, request); + } + request.timeout = preferencesUtil.getRequestTimeout(); return axiosInstance; diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index 31d5cab55..6df6a7c1a 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -131,6 +131,12 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces request.awsv4config.profileName = interpolate(request.awsv4config.profileName) || ''; } + // interpolate vars for digest auth + if (request.digestConfig) { + request.digestConfig.username = interpolate(request.digestConfig.username) || ''; + request.digestConfig.password = interpolate(request.digestConfig.password) || ''; + } + return request; }; diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 6e3f123c1..d5e17a6fd 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -28,6 +28,12 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { case 'bearer': axiosRequest.headers['authorization'] = `Bearer ${get(collectionAuth, 'bearer.token')}`; break; + case 'digest': + axiosRequest.digestConfig = { + username: get(collectionAuth, 'digest.username'), + password: get(collectionAuth, 'digest.password') + }; + break; } } @@ -52,6 +58,11 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { case 'bearer': axiosRequest.headers['authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`; break; + case 'digest': + axiosRequest.digestConfig = { + username: get(request, 'auth.digest.username'), + password: get(request, 'auth.digest.password') + }; } } diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index d8d51d3b1..fbe289974 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -23,7 +23,7 @@ const { outdentString } = require('../../v1/src/utils'); */ const grammar = ohm.grammar(`Bru { BruFile = (meta | http | query | headers | auths | bodies | varsandassert | script | tests | docs)* - auths = authawsv4 | authbasic | authbearer + auths = authawsv4 | authbasic | authbearer | authdigest bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body bodyforms = bodyformurlencoded | bodymultipart @@ -79,6 +79,7 @@ const grammar = ohm.grammar(`Bru { authawsv4 = "auth:awsv4" dictionary authbasic = "auth:basic" dictionary authbearer = "auth:bearer" dictionary + authdigest = "auth:digest" dictionary body = "body" st* "{" nl* textblock tagend bodyjson = "body:json" st* "{" nl* textblock tagend @@ -350,6 +351,21 @@ const sem = grammar.createSemantics().addAttribute('ast', { } }; }, + authdigest(_1, dictionary) { + const auth = mapPairListToKeyValPairs(dictionary.ast, false); + const usernameKey = _.find(auth, { name: 'username' }); + const passwordKey = _.find(auth, { name: 'password' }); + const username = usernameKey ? usernameKey.value : ''; + const password = passwordKey ? passwordKey.value : ''; + return { + auth: { + digest: { + username, + password + } + } + }; + }, bodyformurlencoded(_1, dictionary) { return { body: { diff --git a/packages/bruno-lang/v2/src/collectionBruToJson.js b/packages/bruno-lang/v2/src/collectionBruToJson.js index 4569736f1..c24f6e6ae 100644 --- a/packages/bruno-lang/v2/src/collectionBruToJson.js +++ b/packages/bruno-lang/v2/src/collectionBruToJson.js @@ -4,7 +4,7 @@ const { outdentString } = require('../../v1/src/utils'); const grammar = ohm.grammar(`Bru { BruFile = (meta | query | headers | auth | auths | vars | script | tests | docs)* - auths = authawsv4 | authbasic | authbearer + auths = authawsv4 | authbasic | authbearer | authdigest nl = "\\r"? "\\n" st = " " | "\\t" @@ -41,6 +41,7 @@ const grammar = ohm.grammar(`Bru { authawsv4 = "auth:awsv4" dictionary authbasic = "auth:basic" dictionary authbearer = "auth:bearer" dictionary + authdigest = "auth:digest" dictionary script = scriptreq | scriptres scriptreq = "script:pre-request" st* "{" nl* textblock tagend @@ -226,6 +227,21 @@ const sem = grammar.createSemantics().addAttribute('ast', { } }; }, + authdigest(_1, dictionary) { + const auth = mapPairListToKeyValPairs(dictionary.ast, false); + const usernameKey = _.find(auth, { name: 'username' }); + const passwordKey = _.find(auth, { name: 'password' }); + const username = usernameKey ? usernameKey.value : ''; + const password = passwordKey ? passwordKey.value : ''; + return { + auth: { + digest: { + username, + password + } + } + }; + }, varsreq(_1, dictionary) { const vars = mapPairListToKeyValPairs(dictionary.ast); _.each(vars, (v) => { diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 56b5ec478..f4959500a 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -114,6 +114,15 @@ ${indentString(`password: ${auth.basic.password}`)} ${indentString(`token: ${auth.bearer.token}`)} } +`; + } + + if (auth && auth.digest) { + bru += `auth:digest { +${indentString(`username: ${auth.digest.username}`)} +${indentString(`password: ${auth.digest.password}`)} +} + `; } diff --git a/packages/bruno-lang/v2/src/jsonToCollectionBru.js b/packages/bruno-lang/v2/src/jsonToCollectionBru.js index ea928a68f..08a3abad5 100644 --- a/packages/bruno-lang/v2/src/jsonToCollectionBru.js +++ b/packages/bruno-lang/v2/src/jsonToCollectionBru.js @@ -102,6 +102,15 @@ ${indentString(`password: ${auth.basic.password}`)} ${indentString(`token: ${auth.bearer.token}`)} } +`; + } + + if (auth && auth.digest) { + bru += `auth:digest { +${indentString(`username: ${auth.digest.username}`)} +${indentString(`password: ${auth.digest.password}`)} +} + `; } diff --git a/packages/bruno-lang/v2/tests/fixtures/collection.bru b/packages/bruno-lang/v2/tests/fixtures/collection.bru index a02be30cb..44a66c8dc 100644 --- a/packages/bruno-lang/v2/tests/fixtures/collection.bru +++ b/packages/bruno-lang/v2/tests/fixtures/collection.bru @@ -21,6 +21,11 @@ auth:bearer { token: 123 } +auth:digest { + username: john + password: secret +} + vars:pre-request { departingDate: 2020-01-01 ~returningDate: 2020-01-02 diff --git a/packages/bruno-lang/v2/tests/fixtures/collection.json b/packages/bruno-lang/v2/tests/fixtures/collection.json index de827d11e..7bda2534d 100644 --- a/packages/bruno-lang/v2/tests/fixtures/collection.json +++ b/packages/bruno-lang/v2/tests/fixtures/collection.json @@ -27,6 +27,10 @@ }, "bearer": { "token": "123" + }, + "digest": { + "username": "john", + "password": "secret" } }, "vars": { diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru index 22168b194..21b20477b 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.bru +++ b/packages/bruno-lang/v2/tests/fixtures/request.bru @@ -40,6 +40,11 @@ auth:bearer { token: 123 } +auth:digest { + username: john + password: secret +} + body:json { { "hello": "world" diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json index def7b5f08..c23c46474 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.json +++ b/packages/bruno-lang/v2/tests/fixtures/request.json @@ -59,6 +59,10 @@ }, "bearer": { "token": "123" + }, + "digest": { + "username": "john", + "password": "secret" } }, "body": { diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index e63d8c3d2..37e6629af 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -94,11 +94,19 @@ const authBearerSchema = Yup.object({ .noUnknown(true) .strict(); +const authDigestSchema = Yup.object({ + username: Yup.string().nullable(), + password: Yup.string().nullable() +}) + .noUnknown(true) + .strict(); + const authSchema = Yup.object({ - mode: Yup.string().oneOf(['none', 'awsv4', 'basic', 'bearer']).required('mode is required'), + mode: Yup.string().oneOf(['none', 'awsv4', 'basic', 'bearer', 'digest']).required('mode is required'), awsv4: authAwsV4Schema.nullable(), basic: authBasicSchema.nullable(), - bearer: authBearerSchema.nullable() + bearer: authBearerSchema.nullable(), + digest: authDigestSchema.nullable() }) .noUnknown(true) .strict();