pub mod callback;
pub mod oauth1;
pub mod oauth2;
pub mod pending;
use crate::config::Config;
use crate::error::{Result, XurlError};
use crate::output::OutputConfig;
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,
}
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 {
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(
std::env::var("REDIRECT_URI").ok(),
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,
}
}
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(
std::env::var("REDIRECT_URI").ok(),
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(|| XurlError::auth("TokenNotFound: OAuth1 token not found"))?;
let oauth1_token = token
.oauth1
.as_ref()
.ok_or_else(|| XurlError::auth("TokenNotFound: OAuth1 token not found"))?;
oauth1::build_oauth1_header(method, url_str, oauth1_token, additional_params)
}
pub fn get_oauth2_header(&mut self, username: &str) -> Result<String> {
let app_name = self.app_name.clone();
if username.is_empty() {
if self
.token_store
.get_first_oauth2_token_for_app(&app_name)
.is_some()
{
let access_token = self.refresh_oauth2_token(username)?;
return Ok(format!("Bearer {access_token}"));
}
if self
.token_store
.get_oauth2_token_unnamed_for_app(&app_name)
.is_some()
{
let access_token = self.refresh_oauth2_token(username)?;
return Ok(format!("Bearer {access_token}"));
}
} else {
if self.token_store.get_oauth2_token(username).is_some()
|| self
.token_store
.get_first_oauth2_token_for_app(&app_name)
.is_some()
{
let access_token = self.refresh_oauth2_token(username)?;
return Ok(format!("Bearer {access_token}"));
}
}
let out = OutputConfig::new(
crate::output::OutputFormat::Text,
false,
false,
crate::cli::ColorChoice::Auto,
);
let mut stdout = std::io::stdout();
let access_token = self.oauth2_flow(username, &out, &mut stdout)?;
Ok(format!("Bearer {access_token}"))
}
pub fn oauth2_flow(
&mut self,
username: &str,
out: &OutputConfig,
stdout: &mut dyn std::io::Write,
) -> Result<String> {
oauth2::run_oauth2_flow(self, username, out, stdout, |url| open::that(url))
}
pub fn refresh_oauth2_token(&mut self, username: &str) -> Result<String> {
oauth2::refresh_oauth2_token(self, username)
}
pub fn remote_oauth2_step1(&self, pending_path: &std::path::Path) -> Result<String> {
oauth2::run_remote_step1(self, pending_path)
}
pub fn remote_oauth2_step2(
&mut self,
redirect_url: &str,
username: &str,
pending_path: &std::path::Path,
) -> Result<String> {
oauth2::run_remote_step2(self, redirect_url, username, pending_path)
}
pub fn get_bearer_token_header(&self) -> Result<String> {
resolve_bearer_token(
std::env::var("XURL_BEARER_TOKEN").ok(),
&self.token_store,
&self.app_name,
)
}
pub(crate) fn fetch_username(&self, access_token: &str) -> Result<String> {
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(
self.config.http_timeout_secs,
))
.build()
.unwrap_or_else(|_| reqwest::blocking::Client::new());
let resp = client
.get(&self.config.info_url)
.header("Authorization", format!("Bearer {access_token}"))
.send()
.map_err(|e| XurlError::auth_with_cause("NetworkError", &e))?;
let body: serde_json::Value = resp
.json()
.map_err(|e| XurlError::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(|| {
XurlError::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(|| XurlError::auth("TokenNotFound: bearer token not found"))?;
let bearer = token
.bearer
.as_ref()
.ok_or_else(|| XurlError::auth("TokenNotFound: bearer token not found"))?;
Ok(format!("Bearer {bearer}"))
}
const _: fn() = || {
fn _assert_send_sync<T: Send + Sync>() {}
_assert_send_sync::<Auth>();
};