aboutsummaryrefslogtreecommitdiffstats
path: root/src/api
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-08 15:35:25 +0000
committertv <tv@krebsco.de>2026-11-08 16:47:09 +0100
commitd48aa13cad73030095fd2677ec0b4cea95912eba (patch)
treeedad18049cb63989e9c38f72c2e1c9bfd15f4608 /src/api
parent4acb78f3bec2f82d8d0a6b2ad3f9f328aa10c98a (diff)
auth: no password prompt over the dashboard's own requests
Opening a dashboard through its login page showed a 401 on /auth/login and the browser's password prompt. The page asked for its token by the browser's password first (POST /auth/login), with none to send, and the 401 carried a Basic challenge, which a browser answers with its prompt. The page now asks by its session first (POST /auth/session), then by the browser's password or the proxy's. And a 401 to a page's own requests (Sec-Fetch-Dest: empty: fetch, EventSource) carries no challenge: a page has its login of its own. Navigations and clients without the header (curl) are still challenged.
Diffstat (limited to 'src/api')
-rw-r--r--src/api/auth.rs11
-rw-r--r--src/api/dashboard/dashboard.html4
2 files changed, 10 insertions, 5 deletions
diff --git a/src/api/auth.rs b/src/api/auth.rs
index f28aa22..acf9d46 100644
--- a/src/api/auth.rs
+++ b/src/api/auth.rs
@@ -316,15 +316,20 @@ pub(crate) async fn basic_auth(
)
.into_response();
}
- // A challenge only where basic authentication can answer it.
+ // A challenge only where basic authentication can answer it, and not to a page's
+ // own requests (`Sec-Fetch-Dest: empty`: fetch, EventSource): the browser would
+ // ask for a password over the page, which has its own login (`/login`).
+ let from_a_page = headers
+ .get("sec-fetch-dest")
+ .is_some_and(|dest| dest == "empty");
return match state.users {
- Some(_) => (
+ Some(_) if !from_a_page => (
StatusCode::UNAUTHORIZED,
[(WWW_AUTHENTICATE, r#"Basic realm="Restricted""#)],
"Unauthorized",
)
.into_response(),
- None => (StatusCode::UNAUTHORIZED, "Unauthorized").into_response(),
+ _ => (StatusCode::UNAUTHORIZED, "Unauthorized").into_response(),
};
};
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index b80e0d2..0f31d7c 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -2314,10 +2314,10 @@ async function obtainTokens() {
clearTimeout(tokenTimer);
try {
// Refreshed as the session (`POST /auth/session`), whose cookie lets the page in;
- // the first: by the browser's password or the proxy's, else by that cookie.
+ // the first: by that cookie, else by the browser's password or the proxy's.
token = token && token.expiresAt > Date.now() + 60000
? await tokenCall("../auth/session", token.token)
- : await tokenCall("../auth/login").catch(() => tokenCall("../auth/session"));
+ : await tokenCall("../auth/session").catch(() => tokenCall("../auth/login"));
narrowToken = await tokenCall("../auth/refresh", token.token, { limit: READ_MEMBERS });
tokenWhy = null;
const life = Math.min(token.expiresAt, narrowToken.expiresAt) - Date.now();