Skip to content

feat: Add HTTP Digest Auth as an option for HTTP monitors - #7714

Open
cpfair wants to merge 3 commits into
louislam:masterfrom
cpfair:http-digest-auth
Open

feat: Add HTTP Digest Auth as an option for HTTP monitors#7714
cpfair wants to merge 3 commits into
louislam:masterfrom
cpfair:http-digest-auth

Conversation

@cpfair

@cpfair cpfair commented Aug 12, 2026

Copy link
Copy Markdown

Summary

There is a longstanding request to add support for HTTP digest auth (see #1798 and #6333). This change adds support for digest auth for HTTP(S) monitors, including keyword monitors.

The bulk of the complexity of this PR is in the actual digest auth implementation. I could not find a ready-to-use package to handle the digest parsing and response generation (at least, not to my own satisfaction):

  • axios-digest-auth would seem to want to own the complete request lifecycle, making integration difficult. It also references an old axios version.
  • digest-header has an obvious bug where passwords cannot contain :. Ignoring this bug, it does not support RFC7616 handshakes using SHA256.
  • node-digest-auth again wants to own the request lifecycle, and is furthermore not based on axios.
  • indigestion does not support RFC7616 handshakes using SHA256.
  • http-auth-utils, auth-header, www-authenticate - these came up in my searching, but they only parse the challenge header and/or format the response, they do not do the hard work of digest computation.

Is it a good idea to bake this much new logic into the application itself, rather than accepting the limitations of one of the above libraries (or publishing my own)? Maybe not! The code is structured in a way to isolate this logic to ease future refactors, at the very least.

AI was used to generate the HTTP digest auth implementation, which I reviewed afterwards for correctness and clarity.

I have tested this end-to-end against a real Hikvision NVR (circa 2018, MD5 algorithm) and against lighttpd (MD5 and SHA256 algorithms tested).

To minimize the scope of the PR, the username/password fields are recycled from the basic auth implementation, i.e. the digest auth username/password end up stored in the "basic auth" database columns, etc.

Please follow this checklist to avoid unnecessary back and forth (click to expand)
  • ⚠️ If there are Breaking change (a fix or feature that alters existing functionality in a way that could cause issues) I have called them out
  • 🧠 I have disclosed any use of LLMs/AI in this contribution and reviewed all generated content.
    I understand that I am responsible for and able to explain every line of code I submit.
  • 🔍 Any UI changes adhere to visual style of this project.
  • 🛠️ I have self-reviewed and self-tested my code to ensure it works as expected.
  • 📝 I have commented my code, especially in hard-to-understand areas (e.g., using JSDoc for methods).
  • 🤖 I added or updated automated tests where appropriate.
  • 📄 Documentation updates are included (if applicable).
  • 🧰 Dependency updates are listed and explained.
  • ⚠️ CI passes and is green.

Screenshots for Visual Changes

New option in the auth dropdown for HTTP(S) and HTTP(S) Keyword monitors.

image

@github-actions

Copy link
Copy Markdown
Contributor

Thanks for the PR! If anyone would like to help with testing, run: npx kuma-pr cpfair:http-digest-auth (requires Node.js and Docker)

@github-actions

Copy link
Copy Markdown
Contributor

Hello and thanks for lending a paw to Uptime Kuma! 🐻👋
As this is your first contribution, please be sure to check out our Pull Request guidelines.
In particular: - Mark your PR as Draft while you’re still making changes - Mark it as Ready for review once it’s fully ready
If you have any design or process questions, feel free to ask them right here in this pull request - unclear documentation is a bug too.


// The digest itself must be computed over the raw, unescaped username - an
// implementation that hashed the escaped wire form instead would fail this.
const ha1 = crypto.createHash("md5").update(`${username}:example.com:pa:ss:word`).digest("hex");

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use of MD5 is a necessary evil to support the majority of digest auth server implementations.

@autocarl autocarl left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 9c2aca27 as a correctness/interoperability pass. I found five reproducible issues; details are inline. The targeted ESLint run and all 19 added tests pass, but local HTTP verifier probes show that the current tests miss cache-busting, redirects, multiple challenges, qop-less session algorithms, and SHA-512-256. The check suite is otherwise green; CodeQL remains red on the MD5 alert already covered by its bot thread, so I did not duplicate it.

Comment thread server/digest-auth.js
}

const params = Object.fromEntries(
[...challenge.matchAll(/(\w+)=(?:"([^"]*)"|([^\s,]+))/g)].map(([, key, quotedValue, bareValue]) => [

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Parse one authentication challenge at a time. On 9c2aca27, Axios/Node combines multiple WWW-Authenticate fields into one comma-delimited value. This regex consumes everything after the first Digest, and Object.fromEntries then lets parameters from later schemes/challenges overwrite the selected Digest challenge. A local server returning Digest (realm="digest-realm") plus Basic (realm="basic-realm") produced a Digest response for basic-realm and stayed at 401; two Digest challenges likewise select the last parameters even though RFC 7616 orders algorithms by preference. Escaped quoted-pairs are also truncated. Please use a challenge-aware parser (or split rigorously outside quoted strings), unescape quoted values, and select the first supported Digest challenge. Add tests for multiple header fields/schemes, multiple Digest algorithms, and escaped quotes/backslashes.

Comment thread server/digest-auth.js
const algorithm = (challenge.algorithm || "MD5").toUpperCase();
const hash = (value) =>
crypto
.createHash(algorithm.startsWith("SHA-256") ? "sha256" : "md5")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Do not silently compute unsupported algorithms with MD5. SHA-512-256 is defined by RFC 7616, but this branch emits algorithm=SHA-512-256 while generating a 32-hex-character MD5 response; a local verifier correctly kept the monitor at 401. An arbitrary token such as FOO is mislabeled the same way. Please map supported algorithm tokens explicitly (sha512-256 is available in supported Node versions) and fail clearly for unsupported tokens rather than sending a response computed with another algorithm. Add the RFC SHA-512-256 vector and an unknown-algorithm rejection test.

Comment thread server/digest-auth.js
`response=${quoted(response)}`,
];
if (qop) {
parts.push(`qop=${qop}`, `nc=${nc}`, `cnonce=${quoted(cnonce)}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Send the cnonce used by a -sess algorithm even when qop is absent. Lines 69–70 include cnonce in session HA1, but this block puts it on the wire only when qop=auth was selected. For a qop-less MD5-sess challenge, I reproduced a second 401: the server cannot recompute HA1 because the response contains no cnonce (the RFC 2617 ambiguity is corrected by erratum 1649). Emit cnonce whenever the selected algorithm needs it, while keeping qop/nc conditional as appropriate, and add a qop-less MD5-sess verifier test.

Comment thread server/model/monitor.js
username: this.basic_auth_user,
password: this.basic_auth_pass,
method: options.method,
uri: pathname + search,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High — Hash the serialized request target, including Axios params. The monitor adds options.params.uptime_kuma_cachebuster, but this URI is derived only from options.url. I reproduced both requests reaching /health?uptime_kuma_cachebuster=abc while the retry sent uri="/health"; the server computed a different HA2 and returned 401. This makes Digest monitors with Cache Bust enabled stay DOWN against a conforming server. Build the Digest request-target from the same fully serialized Axios config that the adapter sends (including existing query parameters plus options.params) and add an end-to-end test with cache busting enabled.

Comment thread server/model/monitor.js
}),
};

return this.makeAxiosRequest(options, true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Bind the retry to the URL that actually issued the challenge. Axios follows redirects before surfacing the final 401, while options.url still names the original monitor URL. In a local /start -> /protected probe, this code hashed /start, restarted at /start, and remained 401. In a cross-origin variant, the retry sent the target realm/nonce response to the original origin, while the challenged target received no Authorization header. Capture and validate the effective response URL (or disable automatic redirects during Digest negotiation), then retry only the challenged target with its exact request-target. Please cover both same-origin and cross-origin redirects.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants