Skip to main content

better_fetch/
auth.rs

1//! Authentication for clients and individual requests.
2//!
3//! Configure with [`ClientBuilder::auth`](crate::ClientBuilder::auth) or
4//! [`RequestBuilder::auth`](crate::RequestBuilder::auth). Credentials can be static,
5//! resolved synchronously, or fetched asynchronously.
6
7use std::future::Future;
8use std::pin::Pin;
9use std::sync::Arc;
10
11use base64::Engine;
12use http::header::{HeaderValue, AUTHORIZATION};
13use http::HeaderMap;
14
15/// Authentication configuration for a client or request.
16#[derive(Clone)]
17pub enum Auth {
18    /// `Authorization: Bearer …`
19    Bearer {
20        /// Token source.
21        token: TokenSource,
22    },
23    /// `Authorization: Basic …`
24    Basic {
25        /// Username source.
26        username: TokenSource,
27        /// Password source.
28        password: TokenSource,
29    },
30    /// `Authorization: {prefix} {value}`
31    Custom {
32        /// Header scheme prefix (e.g. `"Token"`).
33        prefix: String,
34        /// Credential value source.
35        value: TokenSource,
36    },
37}
38
39/// Source for credential values (static, sync, or async).
40#[derive(Clone)]
41pub enum TokenSource {
42    /// Fixed string credential.
43    Static(String),
44    /// Resolved on each request via a sync closure.
45    Fn(Arc<dyn Fn() -> Option<String> + Send + Sync>),
46    /// Resolved on each request via an async provider.
47    AsyncFn(Arc<dyn AsyncTokenProvider>),
48}
49
50/// Async token resolver.
51pub trait AsyncTokenProvider: Send + Sync {
52    /// Returns the credential, or `None` to skip adding a header.
53    fn resolve(&self) -> Pin<Box<dyn Future<Output = Option<String>> + Send + '_>>;
54}
55
56impl<F, Fut> AsyncTokenProvider for F
57where
58    F: Send + Sync,
59    F: Fn() -> Fut,
60    Fut: Future<Output = Option<String>> + Send + 'static,
61{
62    fn resolve(&self) -> Pin<Box<dyn Future<Output = Option<String>> + Send + '_>> {
63        Box::pin((self)())
64    }
65}
66
67impl Auth {
68    /// Bearer token from a static string.
69    pub fn bearer(token: impl Into<String>) -> Self {
70        Self::Bearer {
71            token: TokenSource::Static(token.into()),
72        }
73    }
74
75    /// Bearer token from a closure (e.g. read from a cache).
76    ///
77    /// # Examples
78    ///
79    /// ```
80    /// use better_fetch::Auth;
81    ///
82    /// let auth = Auth::bearer_fn(|| Some("cached-token".into()));
83    /// ```
84    pub fn bearer_fn(f: impl Fn() -> Option<String> + Send + Sync + 'static) -> Self {
85        Self::Bearer {
86            token: TokenSource::Fn(Arc::new(f)),
87        }
88    }
89
90    /// Basic authentication with static username and password.
91    pub fn basic(username: impl Into<String>, password: impl Into<String>) -> Self {
92        Self::Basic {
93            username: TokenSource::Static(username.into()),
94            password: TokenSource::Static(password.into()),
95        }
96    }
97
98    /// Writes the `Authorization` header into `headers`.
99    pub async fn apply(&self, headers: &mut HeaderMap) -> crate::Result<()> {
100        match self {
101            Self::Bearer { token } => {
102                if let Some(value) = resolve_token(token).await? {
103                    set_authorization(headers, format!("Bearer {value}"))?;
104                }
105            }
106            Self::Basic { username, password } => {
107                let user = resolve_token(username).await?;
108                let pass = resolve_token(password).await?;
109                if let (Some(u), Some(p)) = (user, pass) {
110                    let encoded =
111                        base64::engine::general_purpose::STANDARD.encode(format!("{u}:{p}"));
112                    set_authorization(headers, format!("Basic {encoded}"))?;
113                }
114            }
115            Self::Custom { prefix, value } => {
116                if let Some(v) = resolve_token(value).await? {
117                    set_authorization(headers, format!("{prefix} {v}"))?;
118                }
119            }
120        }
121        Ok(())
122    }
123}
124
125async fn resolve_token(source: &TokenSource) -> crate::Result<Option<String>> {
126    match source {
127        TokenSource::Static(s) => Ok(Some(s.clone())),
128        TokenSource::Fn(f) => Ok(f()),
129        TokenSource::AsyncFn(f) => Ok(f.resolve().await),
130    }
131}
132
133fn set_authorization(headers: &mut HeaderMap, value: String) -> crate::Result<()> {
134    let header_value = HeaderValue::from_str(&value)
135        .map_err(|e| crate::error::Error::Other(format!("invalid authorization header: {e}")))?;
136    headers.insert(AUTHORIZATION, header_value);
137    Ok(())
138}