SSO auth (1)
This commit is contained in:
@@ -11,15 +11,30 @@ use tracing::{error, info, warn};
|
||||
static REFRESH_LOCK: once_cell::sync::Lazy<Mutex<()>> =
|
||||
once_cell::sync::Lazy::new(|| Mutex::new(()));
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct OAuthFlowTracker {
|
||||
pub active_flow_id: u64,
|
||||
pub background_auth_token: Option<CancellationToken>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AuthState {
|
||||
pub auth_pass: Option<AuthPass>,
|
||||
pub background_refresh_token: Option<tokio_util::sync::CancellationToken>,
|
||||
pub oauth_flow: OAuthFlowTracker,
|
||||
pub background_refresh_token: Option<CancellationToken>,
|
||||
}
|
||||
|
||||
pub fn init_auth_state() -> AuthState {
|
||||
let auth_pass = match load_auth_pass() {
|
||||
Ok(pass) => pass,
|
||||
Ok(Some(pass)) if has_supported_auth_pass(&pass) => Some(pass),
|
||||
Ok(Some(_)) => {
|
||||
warn!("Discarding stored auth pass from unsupported auth format");
|
||||
if let Err(err) = clear_auth_pass() {
|
||||
error!("Failed to clear unsupported auth pass: {}", err);
|
||||
}
|
||||
None
|
||||
}
|
||||
Ok(None) => None,
|
||||
Err(e) => {
|
||||
warn!("Failed to load auth pass from keyring: {e}");
|
||||
None
|
||||
@@ -29,10 +44,50 @@ pub fn init_auth_state() -> AuthState {
|
||||
|
||||
AuthState {
|
||||
auth_pass,
|
||||
oauth_flow: OAuthFlowTracker::default(),
|
||||
background_refresh_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn has_supported_auth_pass(auth_pass: &AuthPass) -> bool {
|
||||
auth_pass.issued_at.is_some()
|
||||
&& auth_pass.refresh_token.is_some()
|
||||
&& auth_pass.refresh_expires_in.is_some()
|
||||
}
|
||||
|
||||
pub fn begin_auth_flow() -> (u64, CancellationToken) {
|
||||
let mut guard = lock_w!(FDOLL);
|
||||
if let Some(cancel_token) = guard.auth.oauth_flow.background_auth_token.take() {
|
||||
cancel_token.cancel();
|
||||
}
|
||||
|
||||
guard.auth.oauth_flow.active_flow_id = guard.auth.oauth_flow.active_flow_id.saturating_add(1);
|
||||
let flow_id = guard.auth.oauth_flow.active_flow_id;
|
||||
let cancel_token = CancellationToken::new();
|
||||
guard.auth.oauth_flow.background_auth_token = Some(cancel_token.clone());
|
||||
|
||||
(flow_id, cancel_token)
|
||||
}
|
||||
|
||||
pub fn clear_auth_flow_state(flow_id: u64) -> bool {
|
||||
let mut guard = lock_w!(FDOLL);
|
||||
if guard.auth.oauth_flow.active_flow_id != flow_id {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(cancel_token) = guard.auth.oauth_flow.background_auth_token.take() {
|
||||
cancel_token.cancel();
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn is_auth_flow_active(flow_id: u64) -> bool {
|
||||
let guard = lock_r!(FDOLL);
|
||||
guard.auth.oauth_flow.active_flow_id == flow_id
|
||||
&& guard.auth.oauth_flow.background_auth_token.is_some()
|
||||
}
|
||||
|
||||
/// Returns the auth pass object, including access token and metadata.
|
||||
/// Automatically refreshes if expired and clears session on refresh failure.
|
||||
pub async fn get_auth_pass_with_refresh() -> Option<AuthPass> {
|
||||
@@ -41,14 +96,22 @@ pub async fn get_auth_pass_with_refresh() -> Option<AuthPass> {
|
||||
|
||||
let Some(issued_at) = auth_pass.issued_at else {
|
||||
warn!("Auth pass missing issued_at timestamp, clearing");
|
||||
lock_w!(FDOLL).auth.auth_pass = None;
|
||||
clear_invalid_auth().await;
|
||||
return None;
|
||||
};
|
||||
|
||||
let current_time = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs();
|
||||
let expires_at = issued_at.saturating_add(auth_pass.expires_in);
|
||||
let expired = current_time >= expires_at;
|
||||
if !expired {
|
||||
let access_expires_at = issued_at.saturating_add(auth_pass.expires_in);
|
||||
let refresh_expires_at = auth_pass
|
||||
.refresh_expires_in
|
||||
.map(|refresh_expires_in| issued_at.saturating_add(refresh_expires_in));
|
||||
|
||||
if refresh_expires_at.is_some_and(|refresh_expires_at| current_time >= refresh_expires_at) {
|
||||
clear_expired_auth().await;
|
||||
return None;
|
||||
}
|
||||
|
||||
if current_time < access_expires_at {
|
||||
return Some(auth_pass);
|
||||
}
|
||||
|
||||
@@ -58,31 +121,54 @@ pub async fn get_auth_pass_with_refresh() -> Option<AuthPass> {
|
||||
let current_time = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs();
|
||||
let Some(issued_at) = auth_pass.issued_at else {
|
||||
warn!("Auth pass missing issued_at timestamp after refresh lock, clearing");
|
||||
lock_w!(FDOLL).auth.auth_pass = None;
|
||||
clear_invalid_auth().await;
|
||||
return None;
|
||||
};
|
||||
let expires_at = issued_at.saturating_add(auth_pass.expires_in);
|
||||
let expired = current_time >= expires_at;
|
||||
if !expired {
|
||||
|
||||
let access_expires_at = issued_at.saturating_add(auth_pass.expires_in);
|
||||
let refresh_expires_at = auth_pass
|
||||
.refresh_expires_in
|
||||
.map(|refresh_expires_in| issued_at.saturating_add(refresh_expires_in));
|
||||
|
||||
if refresh_expires_at.is_some_and(|refresh_expires_at| current_time >= refresh_expires_at) {
|
||||
clear_expired_auth().await;
|
||||
return None;
|
||||
}
|
||||
|
||||
if current_time < access_expires_at {
|
||||
return Some(auth_pass);
|
||||
}
|
||||
|
||||
info!("Access token expired, attempting refresh");
|
||||
match refresh_token(&auth_pass.access_token).await {
|
||||
let Some(refresh_token_value) = auth_pass.refresh_token.as_deref() else {
|
||||
warn!("Auth pass missing refresh token, clearing session");
|
||||
clear_invalid_auth().await;
|
||||
return None;
|
||||
};
|
||||
|
||||
match refresh_token(refresh_token_value).await {
|
||||
Ok(new_pass) => Some(new_pass),
|
||||
Err(e) => {
|
||||
error!("Failed to refresh token: {}", e);
|
||||
lock_w!(FDOLL).auth.auth_pass = None;
|
||||
if let Err(e) = clear_auth_pass() {
|
||||
error!("Failed to clear expired auth pass: {}", e);
|
||||
}
|
||||
destruct_user_session().await;
|
||||
open_welcome_window();
|
||||
clear_expired_auth().await;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn clear_expired_auth() {
|
||||
lock_w!(FDOLL).auth.auth_pass = None;
|
||||
if let Err(e) = clear_auth_pass() {
|
||||
error!("Failed to clear expired auth pass: {}", e);
|
||||
}
|
||||
destruct_user_session().await;
|
||||
open_welcome_window();
|
||||
}
|
||||
|
||||
async fn clear_invalid_auth() {
|
||||
clear_expired_auth().await;
|
||||
}
|
||||
|
||||
async fn refresh_if_expiring_soon() {
|
||||
let Some(auth_pass) = ({ lock_r!(FDOLL).auth.auth_pass.clone() }) else {
|
||||
return;
|
||||
@@ -98,6 +184,19 @@ async fn refresh_if_expiring_soon() {
|
||||
};
|
||||
|
||||
let access_expires_at = issued_at.saturating_add(auth_pass.expires_in);
|
||||
if current_time >= access_expires_at && auth_pass.refresh_token.is_none() {
|
||||
clear_expired_auth().await;
|
||||
return;
|
||||
}
|
||||
|
||||
let refresh_expires_at = auth_pass
|
||||
.refresh_expires_in
|
||||
.map(|refresh_expires_in| issued_at.saturating_add(refresh_expires_in));
|
||||
if refresh_expires_at.is_some_and(|refresh_expires_at| current_time >= refresh_expires_at) {
|
||||
clear_expired_auth().await;
|
||||
return;
|
||||
}
|
||||
|
||||
if access_expires_at.saturating_sub(current_time) >= 60 {
|
||||
return;
|
||||
}
|
||||
@@ -118,18 +217,30 @@ async fn refresh_if_expiring_soon() {
|
||||
};
|
||||
|
||||
let access_expires_at = latest_issued_at.saturating_add(latest_pass.expires_in);
|
||||
if current_time >= access_expires_at && latest_pass.refresh_token.is_none() {
|
||||
clear_expired_auth().await;
|
||||
return;
|
||||
}
|
||||
|
||||
let refresh_expires_at = latest_pass
|
||||
.refresh_expires_in
|
||||
.map(|refresh_expires_in| latest_issued_at.saturating_add(refresh_expires_in));
|
||||
if refresh_expires_at.is_some_and(|refresh_expires_at| current_time >= refresh_expires_at) {
|
||||
clear_expired_auth().await;
|
||||
return;
|
||||
}
|
||||
|
||||
if access_expires_at.saturating_sub(current_time) >= 60 {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(e) = refresh_token(&latest_pass.access_token).await {
|
||||
let Some(refresh_token_value) = latest_pass.refresh_token.as_deref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(e) = refresh_token(refresh_token_value).await {
|
||||
warn!("Background refresh failed: {}", e);
|
||||
lock_w!(FDOLL).auth.auth_pass = None;
|
||||
if let Err(e) = clear_auth_pass() {
|
||||
error!("Failed to clear auth pass after refresh failure: {}", e);
|
||||
}
|
||||
destruct_user_session().await;
|
||||
open_welcome_window();
|
||||
clear_expired_auth().await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user