feat: complete webview cookie extraction implementation
Some checks failed
Auto Tag / auto-tag (push) Successful in 6s
Test / rust-fmt-check (push) Failing after 2m5s
Release / build-macos-arm64 (push) Successful in 6m35s
Test / rust-clippy (push) Failing after 18m2s
Release / build-linux-arm64 (push) Failing after 22m15s
Test / rust-tests (push) Successful in 12m46s
Test / frontend-typecheck (push) Successful in 1m36s
Test / frontend-tests (push) Successful in 1m26s
Test / wiki-sync (push) Successful in 47s
Release / build-linux-amd64 (push) Successful in 21m0s
Release / build-windows-amd64 (push) Successful in 14m42s

Implement working cookie extraction using Tauri's IPC event system:

**How it works:**
1. Opens embedded browser window for user to login
2. User completes authentication (including SSO)
3. User clicks "Complete Login" button in UI
4. JavaScript injected into webview extracts `document.cookie`
5. Parsed cookies emitted via Tauri event: `tftsr-cookies-extracted`
6. Rust listens for event and receives cookie data
7. Cookies encrypted and stored in database

**Technical implementation:**
- Uses `window.__TAURI__.event.emit()` from injected JavaScript
- Rust listens via `app_handle.listen()` with Listener trait
- 10-second timeout with clear error messages
- Handles empty cookies and JavaScript errors gracefully
- Cross-platform compatible (no platform-specific APIs)

**Cookie limitations:**
- `document.cookie` only exposes non-HttpOnly cookies
- HttpOnly session cookies won't be captured via JavaScript
- For HttpOnly cookies, services must provide API tokens as fallback

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Shaun Arman 2026-04-03 17:31:48 -05:00
parent c885f2cc8f
commit e45e4277ea
2 changed files with 139 additions and 43 deletions

View File

@ -458,11 +458,15 @@ pub async fn extract_cookies_from_webview(
app_handle: tauri::AppHandle,
app_state: State<'_, AppState>,
) -> Result<ConnectionResult, String> {
// Extract cookies from the webview
let cookies = crate::integrations::webview_auth::extract_cookies_from_webview(
&webview_id,
// Get the webview window
let webview_window = app_handle
.get_webview_window(&webview_id)
.ok_or_else(|| "Webview window not found".to_string())?;
// Extract cookies using IPC mechanism (more reliable than platform-specific APIs)
let cookies = crate::integrations::webview_auth::extract_cookies_via_ipc(
&webview_window,
&app_handle,
&service,
)
.await?;

View File

@ -1,5 +1,5 @@
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
use tauri::{AppHandle, Listener, WebviewUrl, WebviewWindowBuilder, WebviewWindow};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtractedCredentials {
@ -52,8 +52,8 @@ pub async fn authenticate_with_webview(
.map_err(|e| format!("Failed to create webview: {}", e))?;
// Wait for user to complete login
// We'll detect this by checking if they reached a success page or dashboard
// For now, return a placeholder - actual implementation needs JS injection
// User will click "Complete Login" button in the UI after successful authentication
// This function just opens the window - extraction happens in extract_cookies_via_ipc
Ok(ExtractedCredentials {
cookies: vec![],
@ -61,50 +61,121 @@ pub async fn authenticate_with_webview(
})
}
/// Extract cookies from a webview after successful login.
/// This uses Tauri's webview cookie API to get session cookies.
pub async fn extract_cookies_from_webview(
webview_label: &str,
app_handle: &AppHandle,
_service: &str,
/// Extract cookies from a webview using Tauri's IPC mechanism.
/// This is the most reliable cross-platform approach.
pub async fn extract_cookies_via_ipc<R: tauri::Runtime>(
webview_window: &WebviewWindow<R>,
app_handle: &AppHandle<R>,
) -> Result<Vec<Cookie>, String> {
let webview = app_handle
.get_webview_window(webview_label)
.ok_or_else(|| "Webview window not found".to_string())?;
// Inject JavaScript that will send cookies via IPC
// Note: We use window.__TAURI__ which is the Tauri 2.x API exposed to webviews
let cookie_extraction_script = r#"
(async function() {
try {
// Wait for Tauri API to be available
if (typeof window.__TAURI__ === 'undefined') {
console.error('Tauri API not available');
return;
}
// Get all cookies from the webview
// Note: Tauri 2.x provides cookie manager via webview
// We need to use eval_script to extract cookies via JavaScript
const cookieString = document.cookie;
if (!cookieString || cookieString.trim() === '') {
await window.__TAURI__.event.emit('tftsr-cookies-extracted', { cookies: [] });
return;
}
let cookie_script = r#"
(function() {
const cookies = document.cookie.split(';').map(c => c.trim());
const parsed = cookies.map(cookie => {
const [nameValue, ...attrs] = cookie.split(';');
const [name, value] = nameValue.split('=');
return {
name: name.trim(),
value: value?.trim() || '',
domain: window.location.hostname,
path: '/',
secure: window.location.protocol === 'https:',
http_only: false,
expires: null
};
});
return JSON.stringify(parsed);
const cookies = cookieString.split(';').map(c => c.trim()).filter(c => c.length > 0);
const parsed = cookies.map(cookie => {
const equalIndex = cookie.indexOf('=');
if (equalIndex === -1) return null;
const name = cookie.substring(0, equalIndex).trim();
const value = cookie.substring(equalIndex + 1).trim();
return {
name: name,
value: value,
domain: window.location.hostname,
path: '/',
secure: window.location.protocol === 'https:',
http_only: false,
expires: null
};
}).filter(c => c !== null);
// Use Tauri's event API to send cookies back to Rust
await window.__TAURI__.event.emit('tftsr-cookies-extracted', { cookies: parsed });
console.log('Cookies extracted and emitted:', parsed.length);
} catch (e) {
console.error('Cookie extraction failed:', e);
try {
await window.__TAURI__.event.emit('tftsr-cookies-extracted', { cookies: [], error: e.message });
} catch (emitError) {
console.error('Failed to emit error:', emitError);
}
}
})();
"#;
let _result = webview
.eval(cookie_script)
.map_err(|e| format!("Failed to extract cookies: {}", e))?;
// Set up event listener first
let (tx, mut rx) = tokio::sync::mpsc::channel::<Result<Vec<Cookie>, String>>(1);
// Parse the JSON result
// TODO: Actually parse the cookies from the eval result
// For now, return empty - needs proper implementation
// Listen for the custom event from the webview
let listen_id = app_handle.listen("tftsr-cookies-extracted", move |event| {
tracing::debug!("Received cookies-extracted event");
Ok(vec![])
let payload_str = event.payload();
// Parse the payload JSON
match serde_json::from_str::<serde_json::Value>(payload_str) {
Ok(payload) => {
if let Some(error_msg) = payload.get("error").and_then(|e| e.as_str()) {
let _ = tx.try_send(Err(format!("JavaScript error: {}", error_msg)));
return;
}
if let Some(cookies_value) = payload.get("cookies") {
match serde_json::from_value::<Vec<Cookie>>(cookies_value.clone()) {
Ok(cookies) => {
tracing::info!("Parsed {} cookies from webview", cookies.len());
let _ = tx.try_send(Ok(cookies));
}
Err(e) => {
tracing::error!("Failed to parse cookies: {}", e);
let _ = tx.try_send(Err(format!("Failed to parse cookies: {}", e)));
}
}
} else {
let _ = tx.try_send(Err("No cookies field in payload".to_string()));
}
}
Err(e) => {
tracing::error!("Failed to parse event payload: {}", e);
let _ = tx.try_send(Err(format!("Failed to parse event payload: {}", e)));
}
}
});
// Inject the script into the webview
webview_window
.eval(cookie_extraction_script)
.map_err(|e| format!("Failed to inject cookie extraction script: {}", e))?;
tracing::info!("Cookie extraction script injected, waiting for response...");
// Wait for cookies with timeout
let result = tokio::time::timeout(tokio::time::Duration::from_secs(10), rx.recv())
.await
.map_err(|_| {
"Timeout waiting for cookies. Make sure you are logged in and on the correct page."
.to_string()
})?
.ok_or_else(|| "Failed to receive cookies from webview".to_string())?;
// Clean up event listener
app_handle.unlisten(listen_id);
result
}
/// Build cookie header string for HTTP requests
@ -153,4 +224,25 @@ mod tests {
let header = cookies_to_header(&cookies);
assert_eq!(header, "");
}
#[test]
fn test_cookie_json_serialization() {
let cookies = vec![Cookie {
name: "test".to_string(),
value: "value123".to_string(),
domain: "example.com".to_string(),
path: "/".to_string(),
secure: true,
http_only: false,
expires: None,
}];
let json = serde_json::to_string(&cookies).unwrap();
assert!(json.contains("\"name\":\"test\""));
assert!(json.contains("\"value\":\"value123\""));
let deserialized: Vec<Cookie> = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.len(), 1);
assert_eq!(deserialized[0].name, "test");
}
}