#[doc(hidden)]
pub mod callback;
pub mod credentials;
#[doc(hidden)]
pub mod oauth1;
pub mod oauth2;
#[doc(hidden)]
pub mod pending;
pub(crate) use credentials::DirectCredentials;
use credentials::REDACTED;
pub use credentials::{BoxError, OAuth1Credential, OAuth2Credential, OnTokenRefreshed};
use crate::config::Config;
use crate::error::{Error, Result};
use crate::store::TokenStore;
#[allow(clippy::struct_field_names)]
pub struct Auth {
pub token_store: TokenStore,
config: Config,
client_id: String,
client_secret: String,
client_id_from_env: bool,
client_secret_from_env: bool,
app_name: String,
redirect_uri_override: Option<String>,
bearer_token_override: Option<String>,
}
crate::assert_send_sync!(Auth);
impl std::fmt::Debug for Auth {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Auth")
.field("app_name", &self.app_name)
.field("store", &self.token_store.file_path)
.field("client_id", &self.client_id)
.field("client_secret", &REDACTED)
.field("client_id_from_env", &self.client_id_from_env)
.field("client_secret_from_env", &self.client_secret_from_env)
.field("redirect_uri", &self.config.redirect_uri)
.field(
"bearer_token_override",
&self.bearer_token_override.as_ref().map(|_| REDACTED),
)
.finish_non_exhaustive()
}
}
impl Auth {
#[must_use]
pub fn new(cfg: &Config) -> Self {
Self::new_with_store_path(cfg, &Config::default_store_path())
}
#[must_use]
pub fn new_with_store_path(cfg: &Config, store_path: &std::path::Path) -> Self {
Self::new_with_store_path_and_overrides(
cfg,
store_path,
&crate::config::EnvOverrides::from_env(),
)
}
#[must_use]
pub fn new_with_store_path_and_overrides(
cfg: &Config,
store_path: &std::path::Path,
overrides: &crate::config::EnvOverrides,
) -> Self {
let path_str = store_path.to_str().unwrap_or(".");
let ts =
TokenStore::new_with_credentials_and_path(&cfg.client_id, &cfg.client_secret, path_str);
let client_id_from_env = !cfg.client_id.is_empty();
let client_secret_from_env = !cfg.client_secret.is_empty();
let mut client_id = cfg.client_id.clone();
let mut client_secret = cfg.client_secret.clone();
let app_name = cfg.app_name.clone();
let app = ts.resolve_app(&app_name);
if !client_id_from_env {
client_id.clone_from(&app.client_id);
}
if !client_secret_from_env {
client_secret.clone_from(&app.client_secret);
}
let mut config = cfg.clone();
let resolved = crate::config::resolve_redirect_uri_from(
overrides.redirect_uri.clone(),
ts.get_app_redirect_uri(&app_name),
);
config.redirect_uri = resolved.uri;
config.redirect_uri_source = resolved.source;
config.redirect_uri_from_env = resolved.source.is_env_var();
Self {
token_store: ts,
config,
client_id,
client_secret,
client_id_from_env,
client_secret_from_env,
app_name,
redirect_uri_override: overrides.redirect_uri.clone(),
bearer_token_override: overrides.bearer_token.clone(),
}
}
#[must_use]
pub fn env_bearer_token_present(&self) -> bool {
self.bearer_token_override
.as_deref()
.is_some_and(|token| !token.is_empty())
}
#[must_use]
pub fn redirect_uri_override(&self) -> Option<&str> {
self.redirect_uri_override.as_deref()
}
pub fn with_app_name(&mut self, app_name: &str) {
self.app_name = app_name.to_string();
let app = self.token_store.resolve_app(app_name);
if !self.client_id_from_env {
self.client_id = app.client_id.clone();
}
if !self.client_secret_from_env {
self.client_secret = app.client_secret.clone();
}
let resolved = crate::config::resolve_redirect_uri_from(
self.redirect_uri_override.clone(),
self.token_store.get_app_redirect_uri(app_name),
);
self.config.redirect_uri = resolved.uri;
self.config.redirect_uri_source = resolved.source;
self.config.redirect_uri_from_env = resolved.source.is_env_var();
}
pub fn get_oauth1_header(
&self,
method: &str,
url_str: &str,
additional_params: Option<&std::collections::BTreeMap<String, String>>,
) -> Result<String> {
let token = self
.token_store
.get_oauth1_tokens_for_app(&self.app_name)
.ok_or_else(|| Error::auth("TokenNotFound: OAuth1 token not found"))?;
let oauth1_token = token
.oauth1
.as_ref()
.ok_or_else(|| Error::auth("TokenNotFound: OAuth1 token not found"))?;
oauth1::build_oauth1_header(method, url_str, oauth1_token, additional_params)
}
pub async fn get_oauth2_header(
&mut self,
http: &reqwest::Client,
username: &str,
) -> Result<String> {
if !self.has_oauth2_token(username) {
return Err(Error::auth(crate::error::NO_OAUTH2_TOKEN));
}
let access_token = self.refresh_oauth2_token(http, username).await?;
Ok(format!("Bearer {access_token}"))
}
#[must_use]
pub fn has_oauth2_token(&self, username: &str) -> bool {
let app_name = self.app_name.as_str();
let first = self
.token_store
.get_first_oauth2_token_for_app(app_name)
.is_some();
if username.is_empty() {
first
|| self
.token_store
.get_oauth2_token_unnamed_for_app(app_name)
.is_some()
} else {
self.token_store.get_oauth2_token(username).is_some() || first
}
}
#[must_use]
pub fn unexpired_oauth2_access_token(&self, username: &str) -> Option<String> {
oauth2::stored_oauth2_token(self, username)
.filter(|token| !oauth2::is_expired(token))
.map(|token| token.access_token)
}
pub async fn oauth2_flow<F>(
&mut self,
http: &reqwest::Client,
username: &str,
cancel: tokio_util::sync::CancellationToken,
browser_opener: F,
) -> Result<String>
where
F: Fn(&str) -> std::io::Result<()> + Send + Sync + 'static,
{
oauth2::run_oauth2_flow(self, http, username, cancel, browser_opener).await
}
pub async fn refresh_oauth2_token(
&mut self,
http: &reqwest::Client,
username: &str,
) -> Result<String> {
oauth2::refresh_oauth2_token(self, http, username).await
}
pub fn remote_oauth2_step1(&self, pending_path: &std::path::Path) -> Result<String> {
oauth2::run_remote_step1(self, pending_path)
}
pub async fn remote_oauth2_step2(
&mut self,
http: &reqwest::Client,
redirect_url: &str,
username: &str,
pending_path: &std::path::Path,
) -> Result<String> {
oauth2::run_remote_step2(self, http, redirect_url, username, pending_path).await
}
pub fn get_bearer_token_header(&self) -> Result<String> {
resolve_bearer_token(
self.bearer_token_override.clone(),
&self.token_store,
&self.app_name,
)
}
pub(crate) async fn fetch_username(
&self,
http: &reqwest::Client,
access_token: &str,
) -> Result<String> {
let resp = http
.get(&self.config.info_url)
.timeout(std::time::Duration::from_secs(
self.config.http_timeout_secs,
))
.header("Authorization", format!("Bearer {access_token}"))
.send()
.await
.map_err(|e| Error::auth_with_cause("NetworkError", &e))?;
let body: serde_json::Value = resp
.json()
.await
.map_err(|e| Error::auth_with_cause("JSONDeserializationError", &e))?;
body.get("data")
.and_then(|d| d.get("username"))
.and_then(|u| u.as_str())
.map(std::string::ToString::to_string)
.ok_or_else(|| {
Error::auth("UsernameNotFound: username not found when fetching username")
})
}
#[allow(dead_code)] #[must_use]
pub fn with_token_store(mut self, token_store: TokenStore) -> Self {
let new_app = token_store.resolve_app(&self.app_name);
if !self.client_id_from_env {
self.client_id = new_app.client_id.clone();
}
if !self.client_secret_from_env {
self.client_secret = new_app.client_secret.clone();
}
self.token_store = token_store;
self
}
#[allow(dead_code)] #[must_use]
pub fn token_store(&self) -> &TokenStore {
&self.token_store
}
#[must_use]
pub fn app_name(&self) -> &str {
&self.app_name
}
#[must_use]
pub fn client_id(&self) -> &str {
&self.client_id
}
#[must_use]
pub fn client_secret(&self) -> &str {
&self.client_secret
}
#[must_use]
pub fn auth_url(&self) -> &str {
&self.config.auth_url
}
#[must_use]
pub fn token_url(&self) -> &str {
&self.config.token_url
}
#[must_use]
pub fn redirect_uri(&self) -> &str {
&self.config.redirect_uri
}
#[must_use]
pub fn http_timeout_secs(&self) -> u64 {
self.config.http_timeout_secs
}
}
pub fn resolve_bearer_token(
env_token: Option<String>,
store: &TokenStore,
app_name: &str,
) -> Result<String> {
if let Some(token) = env_token
&& !token.is_empty()
{
return Ok(format!("Bearer {token}"));
}
let token = store
.get_bearer_token_for_app(app_name)
.ok_or_else(|| Error::auth("TokenNotFound: bearer token not found"))?;
let bearer = token
.bearer
.as_ref()
.ok_or_else(|| Error::auth("TokenNotFound: bearer token not found"))?;
Ok(format!("Bearer {bearer}"))
}