From 8329244c69e94f1cbbbcb0dab24618c34bf4f955 Mon Sep 17 00:00:00 2001 From: Connor Johnstone Date: Thu, 4 Sep 2025 16:06:18 -0400 Subject: [PATCH] Fix authentication validation to properly reject invalid CalDAV servers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Backend: Enhance CalDAV discovery to require at least one valid 207 response - Backend: Fail authentication if no valid CalDAV endpoints are found - Frontend: Add token verification on app startup to validate stored tokens - Frontend: Clear invalid tokens when login fails or token verification fails - Frontend: Prevent users with invalid tokens from accessing calendar page This resolves the issue where invalid servers (like google.com) were incorrectly accepted as valid CalDAV servers, and ensures proper authentication flow. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- backend/src/calendar.rs | 26 ++++++++++++++++--- frontend/src/app.rs | 37 ++++++++++++++++++++++++++- frontend/src/auth.rs | 44 ++++++++++++++++++++++++++++++++ frontend/src/components/login.rs | 4 +++ 4 files changed, 107 insertions(+), 4 deletions(-) diff --git a/backend/src/calendar.rs b/backend/src/calendar.rs index a6adf7b..f8abe76 100644 --- a/backend/src/calendar.rs +++ b/backend/src/calendar.rs @@ -580,14 +580,34 @@ impl CalDAVClient { let mut all_calendars = Vec::new(); + let mut has_valid_caldav_response = false; + for path in discovery_paths { println!("Trying discovery path: {}", path); - if let Ok(calendars) = self.discover_calendars_at_path(&path).await { - println!("Found {} calendar(s) at {}", calendars.len(), path); - all_calendars.extend(calendars); + match self.discover_calendars_at_path(&path).await { + Ok(calendars) => { + println!("Found {} calendar(s) at {}", calendars.len(), path); + has_valid_caldav_response = true; + all_calendars.extend(calendars); + } + Err(CalDAVError::ServerError(status)) => { + // HTTP error - this might be expected for some paths, continue trying + println!("Discovery path {} returned HTTP {}, trying next path", path, status); + } + Err(e) => { + // Network or other error - this suggests the server isn't reachable or isn't CalDAV + println!("Discovery failed for path {}: {:?}", path, e); + return Err(e); + } } } + // If we never got a valid CalDAV response (e.g., all requests failed), + // this is likely not a CalDAV server + if !has_valid_caldav_response { + return Err(CalDAVError::ServerError(404)); + } + // Remove duplicates all_calendars.sort(); all_calendars.dedup(); diff --git a/frontend/src/app.rs b/frontend/src/app.rs index f9ce1f4..517df61 100644 --- a/frontend/src/app.rs +++ b/frontend/src/app.rs @@ -55,7 +55,42 @@ fn get_theme_event_colors() -> Vec { #[function_component] pub fn App() -> Html { - let auth_token = use_state(|| -> Option { LocalStorage::get("auth_token").ok() }); + let auth_token = use_state(|| -> Option { None }); + + // Validate token on app startup + { + let auth_token = auth_token.clone(); + use_effect_with((), move |_| { + let auth_token = auth_token.clone(); + wasm_bindgen_futures::spawn_local(async move { + // Check if there's a stored token + if let Ok(stored_token) = LocalStorage::get::("auth_token") { + // Verify the stored token + let auth_service = crate::auth::AuthService::new(); + match auth_service.verify_token(&stored_token).await { + Ok(true) => { + // Token is valid, set it + web_sys::console::log_1(&"✅ Stored auth token is valid".into()); + auth_token.set(Some(stored_token)); + } + _ => { + // Token is invalid or verification failed, clear it + web_sys::console::log_1(&"❌ Stored auth token is invalid, clearing".into()); + let _ = LocalStorage::delete("auth_token"); + let _ = LocalStorage::delete("session_token"); + let _ = LocalStorage::delete("caldav_credentials"); + auth_token.set(None); + } + } + } else { + // No stored token + web_sys::console::log_1(&"â„šī¸ No stored auth token found".into()); + auth_token.set(None); + } + }); + || () + }); + } let user_info = use_state(|| -> Option { None }); let color_picker_open = use_state(|| -> Option { None }); diff --git a/frontend/src/auth.rs b/frontend/src/auth.rs index 4a84708..6a95c9f 100644 --- a/frontend/src/auth.rs +++ b/frontend/src/auth.rs @@ -53,6 +53,50 @@ impl AuthService { self.post_json("/auth/login", &request).await } + pub async fn verify_token(&self, token: &str) -> Result { + let window = web_sys::window().ok_or("No global window exists")?; + + let opts = RequestInit::new(); + opts.set_method("GET"); + opts.set_mode(RequestMode::Cors); + + let url = format!("{}/auth/verify", self.base_url); + let request = Request::new_with_str_and_init(&url, &opts) + .map_err(|e| format!("Request creation failed: {:?}", e))?; + + request + .headers() + .set("Authorization", &format!("Bearer {}", token)) + .map_err(|e| format!("Header setting failed: {:?}", e))?; + + let resp_value = JsFuture::from(window.fetch_with_request(&request)) + .await + .map_err(|e| format!("Network request failed: {:?}", e))?; + + let resp: Response = resp_value + .dyn_into() + .map_err(|e| format!("Response cast failed: {:?}", e))?; + + if resp.ok() { + let text = JsFuture::from( + resp.text() + .map_err(|e| format!("Text extraction failed: {:?}", e))?, + ) + .await + .map_err(|e| format!("Text promise failed: {:?}", e))?; + + let text_string = text.as_string().ok_or("Response text is not a string")?; + + // Parse the response to get the "valid" field + let response: serde_json::Value = serde_json::from_str(&text_string) + .map_err(|e| format!("JSON parsing failed: {}", e))?; + + Ok(response.get("valid").and_then(|v| v.as_bool()).unwrap_or(false)) + } else { + Ok(false) // Invalid token + } + } + // Helper method for POST requests with JSON body async fn post_json Deserialize<'de>>( &self, diff --git a/frontend/src/components/login.rs b/frontend/src/components/login.rs index 0e9498a..34015ca 100644 --- a/frontend/src/components/login.rs +++ b/frontend/src/components/login.rs @@ -145,6 +145,10 @@ pub fn Login(props: &LoginProps) -> Html { } Err(err) => { web_sys::console::log_1(&format!("❌ Login failed: {}", err).into()); + // Clear any existing invalid tokens + let _ = LocalStorage::delete("auth_token"); + let _ = LocalStorage::delete("session_token"); + let _ = LocalStorage::delete("caldav_credentials"); error_message.set(Some(err)); is_loading.set(false); }