pub mod browser_flow;
pub mod config;
pub mod device_code;
mod file_store;
mod jwt;
pub mod refresh;
mod store;
mod token_store;
mod tokens;
pub use browser_flow::AuthSuccess;
pub use file_store::FileStore;
pub use jwt::extract_tenant_id;
pub use store::KeychainStore;
pub use token_store::TokenStore;
pub use tokens::TokenSet;
use pidge_core::TokenStorage;
use crate::error::ClientError;
pub struct AuthClient {
http: reqwest::Client,
client_id: String,
authority_base: String,
scope: String,
}
impl AuthClient {
pub fn from_env() -> Result<Self, ClientError> {
let client_id = config::client_id().ok_or(ClientError::NotProvisioned)?;
Ok(Self {
http: reqwest::Client::builder()
.user_agent(format!("pidge/{}", env!("CARGO_PKG_VERSION")))
.build()?,
client_id,
authority_base: config::AUTHORITY.to_string(),
scope: config::scope_string(),
})
}
pub fn for_test(client_id: impl Into<String>, authority_base: impl Into<String>) -> Self {
Self {
http: reqwest::Client::new(),
client_id: client_id.into(),
authority_base: authority_base.into(),
scope: config::scope_string(),
}
}
pub async fn run_browser_flow<F>(
&self,
on_authorize_url_ready: F,
) -> Result<AuthSuccess, ClientError>
where
F: FnOnce(&str),
{
browser_flow::run(
&self.http,
&self.authority_base,
&self.client_id,
&self.scope,
on_authorize_url_ready,
)
.await
}
pub async fn get_valid_token(&self, email: &str) -> Result<String, ClientError> {
let storage = storage_for(email);
let tokens =
TokenStore::load(email, storage)?.ok_or_else(|| ClientError::SessionExpired {
email: email.to_string(),
})?;
let access_token = if tokens.needs_refresh() {
let new_tokens = refresh::refresh(
&self.http,
&self.authority_base,
&self.client_id,
&tokens,
&self.scope,
email,
)
.await?;
TokenStore::save(email, &new_tokens, storage)?;
new_tokens.access_token
} else {
tokens.access_token
};
backfill_tenant_id(email, &access_token);
Ok(access_token)
}
}
fn backfill_tenant_id(email: &str, access_token: &str) {
let Ok(mut config) = pidge_core::Config::load() else {
return;
};
let Some(existing) = config.find(email).cloned() else {
return;
};
if !existing.tenant_id.is_empty() {
return;
}
let Some(tid) = jwt::extract_tenant_id(access_token) else {
return;
};
if tid.is_empty() {
return;
}
let mut updated = existing;
updated.tenant_id = tid;
config.add_account(updated);
let _ = config.save();
}
fn storage_for(email: &str) -> TokenStorage {
pidge_core::Config::load()
.ok()
.and_then(|c| c.find(email).map(|a| a.storage))
.unwrap_or_default()
}