mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
Desktop: persist auth token to disk when Credential Manager is restricted (#6303)
Co-authored-by: James Brunton <[email protected]>
This commit is contained in:
co-authored by
James Brunton
parent
8fb9fced35
commit
6730ad7cbb
@@ -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";
|
||||
@@ -40,73 +41,134 @@ 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).
|
||||
// Any failure - including entry creation - falls through to the Tauri Store fallback
|
||||
// below, never an early return, so restricted environments still persist the token.
|
||||
match get_keyring_entry() {
|
||||
Ok(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::info!("Keyring set failed for auth token: {} - using Tauri Store fallback", e);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Token verification failed: {}", e);
|
||||
return Err(format!("Token verification failed: {}", e));
|
||||
log::info!("Keyring entry unavailable for auth token: {} - using Tauri Store fallback", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 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> {
|
||||
let entry = get_keyring_entry()?;
|
||||
|
||||
match entry.get_password() {
|
||||
Ok(token) => Ok(Some(token)),
|
||||
Err(keyring::Error::NoEntry) => Ok(None),
|
||||
Err(e) => {
|
||||
log::error!("Failed to retrieve token from keyring: {}", e);
|
||||
Err(format!("Failed to retrieve token: {}", e))
|
||||
pub async fn get_auth_token(app_handle: AppHandle) -> Result<Option<String>, String> {
|
||||
// Try keyring first (production / unrestricted environments). Any failure -
|
||||
// including entry creation - falls through to the Tauri Store fallback below.
|
||||
match get_keyring_entry() {
|
||||
Ok(entry) => match entry.get_password() {
|
||||
Ok(token) => return Ok(Some(token)),
|
||||
Err(keyring::Error::NoEntry) => {
|
||||
log::debug!("No auth token in keyring, trying Tauri Store");
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Keyring error reading auth token: {} - trying Tauri Store", e);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::warn!("Keyring entry unavailable for 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> {
|
||||
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(()),
|
||||
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)),
|
||||
pub async fn clear_auth_token(app_handle: AppHandle) -> Result<(), String> {
|
||||
// Clear from keyring (best-effort). Entry creation failure must not block clearing
|
||||
// the disk fallback, otherwise a stale fallback token could survive sign-out.
|
||||
match get_keyring_entry() {
|
||||
Ok(entry) => match entry.delete_credential() {
|
||||
Ok(_) | Err(keyring::Error::NoEntry) => {}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to delete keyring credential: {}. Attempting overwrite with empty token.", e);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::warn!("Keyring entry unavailable while clearing auth token: {} - clearing Tauri Store fallback only", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 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]
|
||||
|
||||
Reference in New Issue
Block a user