From be48143f774770ad1b89f2491473306f55004847 Mon Sep 17 00:00:00 2001 From: rUv Date: Wed, 3 Jun 2026 11:07:34 +0200 Subject: [PATCH] fix(auth): match the Bearer scheme case-insensitively (RFC 6750) (#929) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `require_bearer` parsed the Authorization header with `strip_prefix("Bearer ")`, which is case-sensitive. Per RFC 6750 §2.1 / RFC 7235 §2.1 the auth-scheme is case-insensitive, so a correct token sent as `Authorization: bearer ` (or `BEARER`, or with extra whitespace) was rejected with a confusing "invalid bearer token" 401 — needless friction when setting up `RUVIEW_API_TOKEN` (the active #864/#924 theme). Now the scheme is matched with `eq_ignore_ascii_case` and leading token whitespace trimmed. The token comparison itself is unchanged — still exact and constant-time (`ct_eq`) — so this does not weaken auth: a wrong token or a non-Bearer scheme (`Basic …`) still returns 401. New test `accepts_case_insensitive_bearer_scheme` covers `bearer`/`BEARER`/ extra-space (accept) and wrong-token/`Basic` (still reject). bearer_auth suite: 9 passed. --- .../src/bearer_auth.rs | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/v2/crates/wifi-densepose-sensing-server/src/bearer_auth.rs b/v2/crates/wifi-densepose-sensing-server/src/bearer_auth.rs index c7acd168..be313afa 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/bearer_auth.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/bearer_auth.rs @@ -100,7 +100,17 @@ pub async fn require_bearer( .headers() .get(AUTHORIZATION) .and_then(|v| v.to_str().ok()) - .and_then(|s| s.strip_prefix("Bearer ")); + // RFC 6750 §2.1 / RFC 7235 §2.1: the auth-scheme ("Bearer") is + // case-insensitive. Match it as such (and tolerate extra leading + // whitespace before the token) so a correct token isn't rejected + // just because a client sent `bearer`/`BEARER`. The token compare + // below stays exact + constant-time. + .and_then(|s| { + let (scheme, token) = s.split_once(' ')?; + scheme + .eq_ignore_ascii_case("Bearer") + .then(|| token.trim_start()) + }); let ok = supplied .map(|s| ct_eq(s.as_bytes(), expected.as_bytes())) .unwrap_or(false); @@ -185,6 +195,31 @@ mod tests { ); } + #[tokio::test] + async fn accepts_case_insensitive_bearer_scheme() { + // RFC 6750 §2.1 / RFC 7235 §2.1: the auth-scheme is case-insensitive. + // A correct token must authenticate regardless of scheme casing or + // extra whitespace; a wrong token must still be rejected. + async fn req_status(auth_value: &str) -> StatusCode { + let r = wrap(AuthState::from_token("s3cr3t")); + let mut req = Request::builder() + .method("GET") + .uri("/api/v1/info") + .body(Body::empty()) + .unwrap(); + req.headers_mut() + .insert(AUTHORIZATION, auth_value.parse().unwrap()); + r.oneshot(req).await.unwrap().status() + } + assert_eq!(req_status("Bearer s3cr3t").await, StatusCode::OK); + assert_eq!(req_status("bearer s3cr3t").await, StatusCode::OK); + assert_eq!(req_status("BEARER s3cr3t").await, StatusCode::OK); + assert_eq!(req_status("Bearer s3cr3t").await, StatusCode::OK); // extra space + // Scheme leniency must NOT weaken the token check. + assert_eq!(req_status("bearer nope").await, StatusCode::UNAUTHORIZED); + assert_eq!(req_status("Basic s3cr3t").await, StatusCode::UNAUTHORIZED); + } + #[tokio::test] async fn enabled_blocks_api_with_wrong_bearer() { let r = wrap(AuthState::from_token("s3cr3t"));