use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Duration;
use axess_clock::{Clock, SystemClock};
use axess_identity::{TenantId, UserId};
use crate::delegated::error::DelegatedError;
use super::credential::{DelegatedCredentialStore, StoredDelegation, store_err};
use super::grant::refresh_token_grant;
use super::provider::DelegatedProvider;
const DEFAULT_REFRESH_SKEW_SECS: i64 = 60;
const DEFAULT_HTTP_TIMEOUT: Duration = Duration::from_secs(30);
const REFRESH_LOCK_PRUNE_THRESHOLD: usize = 1024;
fn default_http_client() -> reqwest::Client {
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
CLIENT
.get_or_init(|| {
reqwest::Client::builder()
.timeout(DEFAULT_HTTP_TIMEOUT)
.build()
.unwrap_or_default()
})
.clone()
}
type RefreshKey = (TenantId, UserId, String);
fn refresh_lock(key: RefreshKey) -> Arc<tokio::sync::Mutex<()>> {
static LOCKS: OnceLock<Mutex<HashMap<RefreshKey, Arc<tokio::sync::Mutex<()>>>>> =
OnceLock::new();
let mut map = LOCKS
.get_or_init(Default::default)
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if map.len() > REFRESH_LOCK_PRUNE_THRESHOLD {
map.retain(|_, lock| Arc::strong_count(lock) > 1);
}
Arc::clone(map.entry(key).or_default())
}
pub struct StoredDelegationSession<S>
where
S: DelegatedCredentialStore,
{
provider: Arc<DelegatedProvider>,
store: Arc<S>,
tenant: TenantId,
user: UserId,
http: reqwest::Client,
clock: Arc<dyn Clock>,
refresh_skew_secs: i64,
}
impl<S> StoredDelegationSession<S>
where
S: DelegatedCredentialStore,
{
pub fn new(
provider: Arc<DelegatedProvider>,
store: Arc<S>,
tenant: TenantId,
user: UserId,
) -> Self {
Self {
provider,
store,
tenant,
user,
http: default_http_client(),
clock: Arc::new(SystemClock),
refresh_skew_secs: DEFAULT_REFRESH_SKEW_SECS,
}
}
pub fn with_http_client(mut self, http: reqwest::Client) -> Self {
self.http = http;
self
}
pub fn with_clock(mut self, clock: Arc<dyn Clock>) -> Self {
self.clock = clock;
self
}
pub fn with_refresh_skew_secs(mut self, secs: i64) -> Self {
self.refresh_skew_secs = secs;
self
}
pub async fn get_access_token(&self) -> Result<String, DelegatedError> {
let cred = self.load_credential().await?;
let skew = chrono::Duration::seconds(self.refresh_skew_secs);
if cred.is_fresh(self.clock.now(), skew) {
return Ok((*cred.access_token).to_string());
}
let lock = refresh_lock((self.tenant, self.user, self.provider.name.clone()));
let _guard = lock.lock().await;
let cred = self.load_credential().await?;
if cred.is_fresh(self.clock.now(), skew) {
return Ok((*cred.access_token).to_string());
}
match self.refresh(&cred).await {
Err(DelegatedError::RefreshRejected) => {
let latest = self.load_credential().await?;
if latest.is_fresh(self.clock.now(), skew) {
return Ok((*latest.access_token).to_string());
}
if latest.refresh_token.as_deref() != cred.refresh_token.as_deref() {
return self.refresh(&latest).await;
}
Err(DelegatedError::RefreshRejected)
}
other => other,
}
}
async fn load_credential(&self) -> Result<StoredDelegation, DelegatedError> {
self.store
.load(&self.tenant, &self.user, &self.provider.name)
.await
.map_err(store_err)?
.ok_or(DelegatedError::NotConnected)
}
async fn refresh(&self, cred: &StoredDelegation) -> Result<String, DelegatedError> {
let refresh_token = cred
.refresh_token
.as_deref()
.ok_or(DelegatedError::RefreshRejected)?;
let refreshed =
refresh_token_grant(&self.provider, refresh_token, &self.http, &*self.clock).await?;
let final_credential = StoredDelegation {
refresh_token: refreshed
.refresh_token
.or_else(|| cred.refresh_token.clone()),
..refreshed
};
let access = (*final_credential.access_token).to_string();
self.store
.save(&self.tenant, &self.user, final_credential)
.await
.map_err(store_err)?;
Ok(access)
}
pub async fn revoke(&self) -> Result<(), DelegatedError> {
self.store
.revoke(&self.tenant, &self.user, &self.provider.name)
.await
.map_err(store_err)
}
}