Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 80 additions & 34 deletions frontend/src-tauri/src/commands/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const STORE_FILE: &str = "connection.json";
const USER_INFO_KEY: &str = "user_info";
const TOKENS_STORE_FILE: &str = "tokens.json";
const REFRESH_TOKEN_STORE_KEY: &str = "refresh_token";
const AUTH_TOKEN_STORE_KEY: &str = "auth_token";
const KEYRING_SERVICE: &str = "stirling-pdf";
const KEYRING_TOKEN_KEY: &str = "auth-token";
const KEYRING_REFRESH_TOKEN_KEY: &str = "refresh-token";
Expand Down Expand Up @@ -40,73 +41,118 @@ fn get_refresh_token_keyring_entry() -> Result<Entry, String> {
}

#[tauri::command]
pub async fn save_auth_token(_app_handle: AppHandle, token: String) -> Result<(), String> {
pub async fn save_auth_token(app_handle: AppHandle, token: String) -> Result<(), String> {
let trimmed = token.trim();
if trimmed.is_empty() {
log::warn!("Attempted to save empty auth token");
return Err("Token cannot be empty".to_string());
}

let entry = get_keyring_entry()?;

if trimmed.len() != token.len() {
log::debug!("Auth token had surrounding whitespace; storing trimmed token");
}

entry
.set_password(trimmed)
.map_err(|e| {
log::error!("Failed to set password in keyring: {}", e);
format!("Failed to save token to keyring: {}", e)
})?;

// Verify the save worked
match entry.get_password() {
Ok(retrieved_token) => {
if retrieved_token != trimmed {
log::error!("Token verification failed: Retrieved token doesn't match");
return Err("Token verification failed after save".to_string());
// Try keyring first (works in environments where Credential Manager persists writes)
let entry = get_keyring_entry()?;
match entry.set_password(trimmed) {
Ok(_) => {
// Verify it persists. On managed Win10 Pro environments (GPO restricting
// Credential Manager, AV/EDR rules, roaming-profile DPAPI quirks) the
// read-back can return the wrong value or NoEntry even when set succeeded.
match entry.get_password() {
Ok(saved) if saved == trimmed => {
// Clear any stale fallback copy so the keyring stays authoritative.
if let Ok(store) = app_handle.store(TOKENS_STORE_FILE) {
if store.get(AUTH_TOKEN_STORE_KEY).is_some() {
store.delete(AUTH_TOKEN_STORE_KEY);
let _ = store.save();
}
}
log::info!("Auth token saved to keyring");
return Ok(());
}
_ => {
log::info!("Keyring did not persist auth token - using Tauri Store fallback");
}
}
}
Err(e) => {
log::error!("Token verification failed: {}", e);
return Err(format!("Token verification failed: {}", e));
log::info!("Keyring set failed for auth token: {} - using Tauri Store fallback", e);
Comment thread
ConnorYoh marked this conversation as resolved.
Outdated
}
}

// Fallback to Tauri Store (same pattern as save_refresh_token)
let store = app_handle
.store(TOKENS_STORE_FILE)
.map_err(|e| format!("Failed to access tokens store: {}", e))?;

store.set(
AUTH_TOKEN_STORE_KEY,
serde_json::to_value(trimmed)
.map_err(|e| format!("Failed to serialize token: {}", e))?,
);

store
.save()
.map_err(|e| format!("Failed to save tokens store: {}", e))?;

log::info!("Auth token saved to Tauri Store (fallback)");
Ok(())
}

#[tauri::command]
pub async fn get_auth_token(_app_handle: AppHandle) -> Result<Option<String>, String> {
pub async fn get_auth_token(app_handle: AppHandle) -> Result<Option<String>, String> {
// Try keyring first (production / unrestricted environments)
let entry = get_keyring_entry()?;

match entry.get_password() {
Ok(token) => Ok(Some(token)),
Err(keyring::Error::NoEntry) => Ok(None),
Ok(token) => return Ok(Some(token)),
Err(keyring::Error::NoEntry) => {
log::debug!("No auth token in keyring, trying Tauri Store");
}
Err(e) => {
log::error!("Failed to retrieve token from keyring: {}", e);
Err(format!("Failed to retrieve token: {}", e))
},
log::warn!("Keyring error reading auth token: {} - trying Tauri Store", e);
}
}

// Fallback to Tauri Store
let store = app_handle
.store(TOKENS_STORE_FILE)
.map_err(|e| format!("Failed to access tokens store: {}", e))?;

let token: Option<String> = store
.get(AUTH_TOKEN_STORE_KEY)
.and_then(|v| serde_json::from_value(v.clone()).ok());

Ok(token)
}

#[tauri::command]
pub async fn clear_auth_token(_app_handle: AppHandle) -> Result<(), String> {
pub async fn clear_auth_token(app_handle: AppHandle) -> Result<(), String> {
// Clear from keyring (best-effort)
let entry = get_keyring_entry()?;

// Delete the token - ignore error if it doesn't exist
match entry.delete_credential() {
Ok(_) | Err(keyring::Error::NoEntry) => Ok(()),
Ok(_) | Err(keyring::Error::NoEntry) => {}
Err(e) => {
log::warn!("Failed to delete keyring credential: {}. Attempting overwrite with empty token.", e);
// As a fallback, overwrite with an empty token so a stale value cannot be reused
match entry.set_password("") {
Ok(_) => Ok(()),
Err(e2) => Err(format!("Failed to clear token (delete + overwrite failed): {}", e2)),
// Overwrite with an empty token so a stale value cannot be reused
if let Err(e2) = entry.set_password("") {
log::warn!("Failed to overwrite keyring auth token: {}", e2);
}
},
}
}

// Clear from Tauri Store fallback
let store = app_handle
.store(TOKENS_STORE_FILE)
.map_err(|e| format!("Failed to access tokens store: {}", e))?;

store.delete(AUTH_TOKEN_STORE_KEY);

store
.save()
.map_err(|e| format!("Failed to save tokens store: {}", e))?;

Ok(())
}

#[tauri::command]
Expand Down
Loading