Skip to content

Commit

Permalink
feat: digest md5 response
Browse files Browse the repository at this point in the history
  • Loading branch information
anwarulislam committed Sep 30, 2024
1 parent 7f72410 commit 02af23e
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 90 deletions.
122 changes: 39 additions & 83 deletions packages/hoppscotch-common/src/helpers/auth/digest.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// import crypto from "crypto"
import { md5 } from "js-md5"
import * as E from "fp-ts/Either"

import { getService } from "~/modules/dioc"
Expand All @@ -7,104 +7,61 @@ import { InterceptorService } from "~/services/interceptor.service"
export interface DigestAuthParams {
username: string
password: string
realm?: string
nonce?: string
realm: string
nonce: string
uri: string
method: string
qop?: string
algorithm: string
qop: string
nc?: string
opaque?: string
cnonce?: string // client nonce (optional but typically required in qop='auth')
}

const randomBytes = (size: number) => {
let bytes = new Uint8Array(size)
if (typeof window !== "undefined" && window.crypto) {
bytes = window.crypto.getRandomValues(bytes)
}
return Buffer.from(bytes)
}

export function generateDigestAuthHeader(params: DigestAuthParams): string {
// Function to generate Digest Auth Header
export async function generateDigestAuthHeader(params: DigestAuthParams) {
const {
username,
password,
realm = "",
nonce = "",
realm,
nonce,
uri,
method,
algorithm = "MD5",
qop,
nc = "00000001", // Nonce count (incrementing in case of multiple requests)
nc = "00000001",
opaque,
cnonce = randomBytes(16).toString("hex"), // client nonce
cnonce,
} = params

// HA1 = MD5(username:realm:password)
// const ha1 = crypto
// .createHash("md5")
// .update(`${username}:${realm}:${password}`)
// .digest("hex")

const ha1 = crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(`${username}:${realm}:${password}`)
)

// HA2 = MD5(method:uri)
// const ha2 = crypto.createHash("md5").update(`${method}:${uri}`).digest("hex")
const ha2 = crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(`${method}:${uri}`)
)

// Response calculation
let response
if (qop) {
// qop = 'auth' is typically used
// response = crypto
// .createHash("md5")
// .update(`${ha1}:${nonce}:${nc}:${cnonce}:${qop}:${ha2}`)
// .digest("hex")

response = crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(`${ha1}:${nonce}:${nc}:${cnonce}:${qop}:${ha2}`)
)
} else {
// Without qop
// response = crypto
// .createHash("md5")
// .update(`${ha1}:${nonce}:${ha2}`)
// .digest("hex")
response = crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(`${ha1}:${nonce}:${ha2}`)
)
}
// Generate client nonce if not provided
const generatedCnonce = cnonce || md5(`${Math.random()}`)

// Construct the Authorization header
const authHeader = [
`Digest username="${username}"`,
`realm="${realm}"`,
`nonce="${nonce}"`,
`uri="${uri}"`,
`response="${response}"`,
qop ? `qop=${qop}` : "",
qop ? `nc=${nc}` : "",
qop ? `cnonce="${cnonce}"` : "",
opaque ? `opaque="${opaque}"` : "",
]
.filter(Boolean) // Remove empty strings
.join(", ")
// Step 1: Hash the username, realm, and password
const ha1 = md5(`${username}:${realm}:${password}`)

// Step 2: Hash the method and URI
const ha2 = md5(`${method}:${uri}`)

// Step 3: Compute the response hash
const response = md5(`${ha1}:${nonce}:${nc}:${generatedCnonce}:${qop}:${ha2}`)

// Build the Digest header
let authHeader = `Digest username="${username}", realm="${realm}", nonce="${nonce}", uri="${uri}", algorithm="${algorithm}", response="${response}", qop=${qop}, nc=${nc}, cnonce="${generatedCnonce}"`

if (opaque) {
authHeader += `, opaque="${opaque}"`
}

return authHeader
}

export interface DigestAuthInfo {
realm: string
nonce: string
qop?: string
qop: string
opaque?: string
algorithm: string
}

export async function fetchInitialDigestAuthInfo(
Expand All @@ -116,28 +73,29 @@ export async function fetchInitialDigestAuthInfo(
const initialResponse = await service.runRequest({
url,
method,
maxRedirects: 0,
// withCredentials: true,
}).response

if (E.isLeft(initialResponse))
throw new Error(`Unexpected response: ${initialResponse.left.toString()}`)
throw new Error(`Unexpected response: ${initialResponse.left}`)

// Check if the response status is 401 (which is expected in Digest Auth flow)
if (initialResponse.right.status === 401) {
console.log("Initial response:", initialResponse.right)
// const authHeader = initialResponse.right.headers.get("www-authenticate")
const authHeader = initialResponse.right.headers["www-authenticate"]

if (authHeader) {
const authParams = parseDigestAuthHeader(authHeader)
console.log("WWW-Authenticate header:", authParams)
if (authParams && authParams.realm) {
if (
authParams &&
authParams.realm &&
authParams.nonce &&
authParams.qop
) {
return {
realm: authParams.realm,
nonce: authParams.nonce,
qop: authParams.qop,
opaque: authParams.opaque,
algorithm: authParams.algorithm,
}
}
}
Expand Down Expand Up @@ -166,7 +124,5 @@ function parseDigestAuthHeader(
authParams[parts[0]] = parts[1].replace(/"/g, "")
})

console.log("Parsed auth params:", authParams)

return authParams
}
21 changes: 14 additions & 7 deletions packages/hoppscotch-common/src/helpers/utils/EffectiveURL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,22 +113,29 @@ export const getComputedAuthHeaders = async (
method
)

console.log("Digest Auth Info:", authInfo)

// Step 2: Set up the parameters for the digest authentication header
const digestAuthParams: DigestAuthParams = {
username: parseTemplateString(request.auth.username, envVars),
password: parseTemplateString(request.auth.password, envVars),
realm: authInfo.realm,
nonce: authInfo.nonce,
realm: request.auth.realm
? parseTemplateString(request.auth.realm, envVars)
: authInfo.realm,
nonce: request.auth.nonce
? parseTemplateString(authInfo.nonce, envVars)
: authInfo.nonce,
uri: parseTemplateString(endpoint, envVars),
method,
qop: authInfo.qop,
opaque: authInfo.opaque,
algorithm: request.auth.algorithm ?? authInfo.algorithm,
qop: request.auth.qop
? parseTemplateString(request.auth.qop, envVars)
: authInfo.qop,
opaque: request.auth.opaque
? parseTemplateString(request.auth.opaque, envVars)
: authInfo.opaque,
}

// Step 3: Generate the Authorization header
const authHeaderValue = generateDigestAuthHeader(digestAuthParams)
const authHeaderValue = await generateDigestAuthHeader(digestAuthParams)

console.log("Digest Auth Header:", authHeaderValue)

Expand Down

0 comments on commit 02af23e

Please sign in to comment.