Fix authentication validation to properly reject invalid CalDAV servers
All checks were successful
Build and Push Docker Image / docker (push) Successful in 1m9s
All checks were successful
Build and Push Docker Image / docker (push) Successful in 1m9s
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -580,12 +580,32 @@ 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 {
|
||||
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
|
||||
|
||||
@@ -55,7 +55,42 @@ fn get_theme_event_colors() -> Vec<String> {
|
||||
|
||||
#[function_component]
|
||||
pub fn App() -> Html {
|
||||
let auth_token = use_state(|| -> Option<String> { LocalStorage::get("auth_token").ok() });
|
||||
let auth_token = use_state(|| -> Option<String> { 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::<String>("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<UserInfo> { None });
|
||||
let color_picker_open = use_state(|| -> Option<String> { None });
|
||||
|
||||
@@ -53,6 +53,50 @@ impl AuthService {
|
||||
self.post_json("/auth/login", &request).await
|
||||
}
|
||||
|
||||
pub async fn verify_token(&self, token: &str) -> Result<bool, String> {
|
||||
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<T: Serialize, R: for<'de> Deserialize<'de>>(
|
||||
&self,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user