Skip to main content

link_r/auth/
mod.rs

1// Author: support@lituus.io
2// Copyright: lituus-io, all rights reserved
3//
4//! Consumer-injected authentication.
5//!
6//! `link-r` hardcodes no credentials. It is generic over an [`AuthProvider`]; the
7//! crate embedding it supplies one (a PAT, an OAuth/GDrive token, a signed URL).
8//! The per-fetch path — [`AuthProvider::credential`] — is synchronous and
9//! lock-free; only [`AuthProvider::refresh`] is async (rare, for expiring tokens),
10//! and because its future type is an associated type it flows through the GAT
11//! [`Fetcher`](crate::fetch::Fetcher) with no boxing.
12//!
13//! For the rare case where the provider is chosen at runtime from config, the
14//! object-safe [`DynAuthProvider`] escape hatch is the single justified `Box<dyn>`
15//! in the crate (one boxed future per refresh, ~hourly, off the hot path).
16
17pub mod anonymous;
18#[cfg(feature = "oauth")]
19pub mod oauth;
20pub mod static_token;
21
22pub use anonymous::AnonymousAuth;
23#[cfg(feature = "oauth")]
24pub use oauth::{FreshToken, OAuthRefreshAuth, TokenSource};
25pub use static_token::StaticTokenAuth;
26
27use crate::error::Result;
28use crate::resource::Resource;
29use std::borrow::Cow;
30use std::future::Future;
31use std::pin::Pin;
32
33/// The credential to apply to a request, returned cheaply (often borrowed) per fetch.
34#[derive(Clone, Debug)]
35#[non_exhaustive]
36pub enum Credential<'a> {
37    /// No credential (public resource).
38    Anonymous,
39    /// `Authorization: Bearer <token>`.
40    Bearer(Cow<'a, str>),
41    /// A custom header `name: value` (e.g. `x-api-key`).
42    Header {
43        /// The header name.
44        name: &'static str,
45        /// The header value.
46        value: Cow<'a, str>,
47    },
48    /// The URL is already signed; send it verbatim with no added credential.
49    Presigned,
50}
51
52/// Supplies credentials for outbound requests.
53pub trait AuthProvider: Send + Sync {
54    /// The future returned by [`AuthProvider::refresh`]. Static providers use
55    /// [`std::future::Ready`]; OAuth uses its token-exchange future.
56    type RefreshFuture<'a>: Future<Output = Result<()>> + Send + 'a
57    where
58        Self: 'a;
59
60    /// The credential to apply to a request for `resource`. Synchronous and
61    /// lock-free — this is the per-fetch hot path.
62    fn credential(&self, resource: &Resource) -> Credential<'_>;
63
64    /// Whether the cached credential is missing or near expiry. Default: never.
65    fn needs_refresh(&self) -> bool {
66        false
67    }
68
69    /// Refresh an expiring credential. Static providers return a ready no-op.
70    fn refresh(&self) -> Self::RefreshFuture<'_>;
71}
72
73/// An object-safe sibling of [`AuthProvider`] for runtime-selected providers.
74///
75/// This is the only `Box<dyn>` the crate condones, and only for the open-set
76/// "provider chosen from config" case; a known set of providers should use a
77/// consumer-side `enum` instead (zero allocation).
78pub trait DynAuthProvider: Send + Sync {
79    /// See [`AuthProvider::credential`].
80    fn credential(&self, resource: &Resource) -> Credential<'_>;
81    /// See [`AuthProvider::needs_refresh`].
82    fn needs_refresh(&self) -> bool;
83    /// See [`AuthProvider::refresh`]; the future is boxed here (the accepted cost).
84    fn refresh_boxed(&self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
85}
86
87impl<A: AuthProvider> DynAuthProvider for A {
88    fn credential(&self, resource: &Resource) -> Credential<'_> {
89        AuthProvider::credential(self, resource)
90    }
91    fn needs_refresh(&self) -> bool {
92        AuthProvider::needs_refresh(self)
93    }
94    fn refresh_boxed(&self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
95        Box::pin(self.refresh())
96    }
97}
98
99/// Re-enter the static world: a boxed [`DynAuthProvider`] is itself an
100/// [`AuthProvider`], so `Fetcher<Box<dyn DynAuthProvider>>` compiles.
101impl AuthProvider for Box<dyn DynAuthProvider> {
102    type RefreshFuture<'a>
103        = Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>
104    where
105        Self: 'a;
106
107    fn credential(&self, resource: &Resource) -> Credential<'_> {
108        (**self).credential(resource)
109    }
110    fn needs_refresh(&self) -> bool {
111        (**self).needs_refresh()
112    }
113    fn refresh(&self) -> Self::RefreshFuture<'_> {
114        (**self).refresh_boxed()
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use url::Url;
122
123    fn resource() -> Resource {
124        Resource::new(Url::parse("https://x.dev/a").unwrap())
125    }
126
127    #[tokio::test]
128    async fn dyn_auth_provider_roundtrips() {
129        let boxed: Box<dyn DynAuthProvider> = Box::new(StaticTokenAuth::bearer("secret"));
130        // used as a static AuthProvider via the blanket re-entry impl
131        let cred = AuthProvider::credential(&boxed, &resource());
132        assert!(matches!(cred, Credential::Bearer(_)));
133        assert!(!AuthProvider::needs_refresh(&boxed));
134        AuthProvider::refresh(&boxed).await.unwrap();
135    }
136}