use tracing::instrument::WithSubscriber;
use crate::api::auth_matrix::WireScheme;
use crate::error::{AuthMismatch, Error, Result};
use super::render_template_template;
use super::source::{CredentialSource, SchemeFacts};
use super::{Client, RequestOptions, RequestTarget};
enum Selection {
Header(String),
OAuth2 { username: String },
}
impl Client {
pub async fn get_auth_header_public(&self, options: &RequestOptions) -> Result<String> {
self.get_auth_header(options).await
}
pub(super) async fn get_auth_header(&self, options: &RequestOptions) -> Result<String> {
let selected = {
let credentials = self.credentials().await;
self.select_scheme(&credentials, options)?
};
match selected {
Selection::Header(header) => Ok(header),
Selection::OAuth2 { username } => self.oauth2_bearer(&username).await,
}
}
async fn oauth2_bearer(&self, username: &str) -> Result<String> {
{
let credentials = self.credentials().await;
if !credentials.has_oauth2_token(username) {
return Err(Error::auth(crate::error::NO_OAUTH2_TOKEN));
}
if let Some(access_token) = credentials.unexpired_oauth2_access_token(username) {
return Ok(format!("Bearer {access_token}"));
}
}
let client = self.clone();
let username = username.to_string();
let access_token = tokio::spawn(
async move {
let mut credentials = client.credentials().await;
credentials
.refresh_oauth2_token(client.http(), &username)
.await
}
.with_current_subscriber(),
)
.await
.map_err(|e| Error::Internal(format!("token refresh task failed: {e}")))??;
Ok(format!("Bearer {access_token}"))
}
fn select_scheme(
&self,
credentials: &CredentialSource,
options: &RequestOptions,
) -> Result<Selection> {
let method_raw = options.method.to_uppercase();
let method = if method_raw.is_empty() {
"GET"
} else {
method_raw.as_str()
};
let endpoint = Endpoint::of(&options.target, method);
if options.auth_type.is_empty() {
self.detect_scheme(credentials, options, method, endpoint.as_ref())
} else {
self.explicit_scheme(credentials, options, method, endpoint.as_ref())
}
}
fn explicit_scheme(
&self,
credentials: &CredentialSource,
options: &RequestOptions,
method: &str,
endpoint: Option<&Endpoint<'_>>,
) -> Result<Selection> {
let auth_type = &options.auth_type;
if let Some(endpoint) = endpoint {
let requested = auth_type.to_ascii_lowercase();
if !endpoint.accepts(&requested) {
return Err(Error::from(AuthMismatch {
requested: Some(requested),
..endpoint.mismatch(credentials.active_app())
}));
}
}
let url = self.build_url(&options.target)?;
match auth_type.to_lowercase().as_str() {
"oauth1" => credentials
.oauth1_header(method, &url)
.map(Selection::Header),
"oauth2" => Ok(Selection::OAuth2 {
username: options.username.clone(),
}),
"app" => credentials.bearer_header().map(Selection::Header),
_ => Err(Error::auth(format!("invalid auth type: {auth_type}"))),
}
}
fn detect_scheme(
&self,
credentials: &CredentialSource,
options: &RequestOptions,
method: &str,
endpoint: Option<&Endpoint<'_>>,
) -> Result<Selection> {
let facts = credentials.scheme_facts();
let selected = WireScheme::ALL_BY_PREFERENCE.into_iter().find(|scheme| {
facts.available.contains(scheme)
&& endpoint.is_none_or(|endpoint| endpoint.accepts(scheme.as_wire()))
});
match selected {
None => Err(no_scheme(credentials, facts, endpoint)),
Some(WireScheme::OAuth2) => Ok(Selection::OAuth2 {
username: options.username.clone(),
}),
Some(WireScheme::OAuth1) => {
let url = self.build_url(&options.target)?;
credentials
.oauth1_header(method, &url)
.map(Selection::Header)
}
Some(WireScheme::App) => credentials.bearer_header().map(Selection::Header),
}
}
}
struct Endpoint<'a> {
path: &'a str,
method: &'a str,
target: &'a RequestTarget,
schemes: Vec<&'static str>,
}
impl<'a> Endpoint<'a> {
fn of(target: &'a RequestTarget, method: &'a str) -> Option<Self> {
let RequestTarget::Template { path, .. } = target else {
return None;
};
let schemes = crate::api::auth_matrix::supported_auth(method, path)?;
Some(Self {
path,
method,
target,
schemes: crate::api::auth_matrix::schemes_to_wire_list(schemes),
})
}
fn accepts(&self, scheme: &str) -> bool {
self.schemes.contains(&scheme)
}
fn mismatch(&self, app: Option<String>) -> AuthMismatch {
AuthMismatch {
endpoint: self.path.to_string(),
rendered_url: render_template_template(self.target).ok(),
method: self.method.to_string(),
requested: None,
supported: self.schemes.iter().map(|s| (*s).to_string()).collect(),
available_in_app: None,
app,
other_apps_with_creds: None,
}
}
}
fn no_scheme(
credentials: &CredentialSource,
facts: SchemeFacts,
endpoint: Option<&Endpoint<'_>>,
) -> Error {
let Some(endpoint) = endpoint else {
return Error::auth(crate::error::NO_AUTH_METHOD);
};
let mut other_apps_with_creds = None;
if facts.stored.is_empty() {
let other_apps =
credentials.other_apps_with_creds(facts.app.as_deref().unwrap_or_default());
if !other_apps.is_empty() {
other_apps_with_creds = Some(other_apps);
} else if facts.available.is_empty() {
return Error::auth(crate::error::NO_AUTH_METHOD);
}
}
let available_in_app = facts
.available
.iter()
.map(|scheme| scheme.as_wire().to_string())
.collect();
Error::from(AuthMismatch {
available_in_app: Some(available_in_app),
other_apps_with_creds,
..endpoint.mismatch(facts.app)
})
}