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}