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/digest auth updates #3697

Merged
merged 9 commits into from
Jan 2, 2025
77 changes: 60 additions & 17 deletions packages/bruno-electron/src/ipc/network/digestauth-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const crypto = require('crypto');
const { URL } = require('url');

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

function stripQuotes(str) {
Expand All @@ -15,7 +15,10 @@ function containsDigestHeader(response) {
}

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

function md5(input) {
Expand All @@ -24,11 +27,10 @@ function md5(input) {

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

console.debug(request);
console.debug('Digest Auth Interceptor Initialized');

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

Expand All @@ -37,41 +39,82 @@ function addDigestInterceptor(axiosInstance, request) {
(error) => {
const originalRequest = error.config;

// Prevent retry loops
if (originalRequest._retry) {
return Promise.reject(error);
}
originalRequest._retry = true;

if (
error.response?.status === 401 &&
containsDigestHeader(error.response) &&
!containsAuthorizationHeader(originalRequest)
) {
console.debug('Processing Digest Authentication Challenge');
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);
.split(',')
.map((pair) => pair.split('=').map((item) => item.trim()).map(stripQuotes))
.reduce((acc, [key, value]) => {
const normalizedKey = key.toLowerCase().replace('digest ', '');
if (normalizedKey && value !== undefined) {
acc[normalizedKey] = value;
}
return acc;
}, {});

// Validate required auth details
if (!authDetails.realm || !authDetails.nonce) {
console.warn('Missing required auth details (realm or nonce)');
return Promise.reject(error);
}

console.debug("Auth Details: \n", authDetails);

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

if (authDetails.algorithm && authDetails.algorithm.toUpperCase() !== 'MD5') {
console.warn(`Unsupported Digest algorithm: ${algo}`);
console.warn(`Unsupported Digest algorithm: ${authDetails.algorithm}`);
return Promise.reject(error);
} else {
authDetails.algorithm = 'MD5';
}
const uri = new URL(request.url).pathname;
const HA1 = md5(`${username}:${authDetails['Digest realm']}:${password}`);

const uri = new URL(request.url, request.baseURL || 'http://localhost').pathname; // Handle relative URLs
const HA1 = md5(`${username}:${authDetails.realm}:${password}`);
const HA2 = md5(`${request.method}:${uri}`);
const response = md5(`${HA1}:${authDetails.nonce}:${nonceCount}:${cnonce}:auth:${HA2}`);
const response = md5(
`${HA1}:${authDetails.nonce}:${nonceCount}:${cnonce}:auth:${HA2}`
);

const headerFields = [
`username="${username}"`,
`realm="${authDetails.realm}"`,
`nonce="${authDetails.nonce}"`,
`uri="${uri}"`,
`qop="auth"`,
`algorithm="${authDetails.algorithm}"`,
`response="${response}"`,
`nc="${nonceCount}"`,
`cnonce="${cnonce}"`,
];

if (authDetails.opaque) {
headerFields.push(`opaque="${authDetails.opaque}"`);
}

const authorizationHeader =
`Digest username="${username}",realm="${authDetails['Digest realm']}",` +
`nonce="${authDetails.nonce}",uri="${uri}",qop="auth",algorithm="${authDetails.algorithm}",` +
`response="${response}",nc="${nonceCount}",cnonce="${cnonce}"`;
const authorizationHeader = `Digest ${headerFields.join(', ')}`;

// Ensure headers are initialized
originalRequest.headers = originalRequest.headers || {};
originalRequest.headers['Authorization'] = authorizationHeader;

console.debug(`Authorization: ${originalRequest.headers['Authorization']}`);

delete originalRequest.digestConfig;

return axiosInstance(originalRequest);
}

Expand Down
Loading