Skip to main content

stack_auth/
access_key_strategy.rs

1use cts_common::{CtsServiceDiscovery, Region, ServiceDiscovery};
2
3use crate::access_key::AccessKey;
4use crate::access_key_refresher::AccessKeyRefresher;
5use crate::auto_refresh::AutoRefresh;
6use crate::token_store::{NoStore, TokenStore};
7use crate::{ensure_trailing_slash, AuthError, AuthStrategy, SecretToken, ServiceToken};
8
9/// An [`AuthStrategy`] that uses a static access key to authenticate.
10///
11/// The first call to [`get_token`](AuthStrategy::get_token) authenticates with
12/// the server. Subsequent calls return the cached token until it expires, at
13/// which point re-authentication happens automatically.
14///
15/// When constructed via [`AccessKeyStrategyBuilder::with_token_store`], the
16/// strategy also persists tokens through an external [`TokenStore`] so that
17/// short-lived strategy instances (e.g. one per Edge Function request) can
18/// share a cache and avoid re-authenticating every cold start.
19///
20/// # Example
21///
22/// ```no_run
23/// use stack_auth::{AccessKey, AccessKeyStrategy};
24/// use cts_common::Region;
25///
26/// let region = Region::aws("ap-southeast-2").unwrap();
27/// let key: AccessKey = "CSAKmyKeyId.myKeySecret".parse().unwrap();
28/// let strategy = AccessKeyStrategy::new(region, key).unwrap();
29/// ```
30pub struct AccessKeyStrategy<S = NoStore> {
31    inner: AutoRefresh<AccessKeyRefresher, S>,
32}
33
34impl AccessKeyStrategy {
35    /// Create a new `AccessKeyStrategy` for the given region and access key.
36    ///
37    /// The auth endpoint is resolved automatically via service discovery.
38    pub fn new(region: Region, access_key: AccessKey) -> Result<Self, AuthError> {
39        Self::builder(region, access_key).build()
40    }
41
42    /// Return a builder for configuring an `AccessKeyStrategy` before construction.
43    ///
44    /// # Example
45    ///
46    /// ```no_run
47    /// use stack_auth::{AccessKey, AccessKeyStrategy};
48    /// use cts_common::Region;
49    ///
50    /// let region = Region::aws("ap-southeast-2").unwrap();
51    /// let key: AccessKey = "CSAKmyKeyId.myKeySecret".parse().unwrap();
52    /// let strategy = AccessKeyStrategy::builder(region, key)
53    ///     .audience("my-audience")
54    ///     .build()
55    ///     .unwrap();
56    /// ```
57    pub fn builder(region: Region, access_key: AccessKey) -> AccessKeyStrategyBuilder {
58        AccessKeyStrategyBuilder {
59            region,
60            access_key: access_key.into_secret_token(),
61            audience: None,
62            base_url_override: None,
63            token_store: NoStore,
64        }
65    }
66}
67
68impl<S: TokenStore> AuthStrategy for &AccessKeyStrategy<S> {
69    async fn get_token(self) -> Result<ServiceToken, AuthError> {
70        Ok(self.inner.get_token().await?)
71    }
72}
73
74/// Builder for [`AccessKeyStrategy`].
75///
76/// Created via [`AccessKeyStrategy::builder`].
77pub struct AccessKeyStrategyBuilder<S = NoStore> {
78    region: Region,
79    access_key: SecretToken,
80    audience: Option<String>,
81    base_url_override: Option<url::Url>,
82    token_store: S,
83}
84
85impl<S> AccessKeyStrategyBuilder<S> {
86    /// Set the audience for token requests.
87    pub fn audience(mut self, audience: impl Into<String>) -> Self {
88        self.audience = Some(audience.into());
89        self
90    }
91
92    /// Override the base URL resolved by service discovery.
93    ///
94    /// Useful for pointing at a local or mock auth server during testing.
95    #[cfg(any(test, feature = "test-utils"))]
96    pub fn base_url(mut self, url: url::Url) -> Self {
97        self.base_url_override = Some(url);
98        self
99    }
100
101    /// Wire an external [`TokenStore`] into the strategy.
102    ///
103    /// On every call to [`get_token`](AuthStrategy::get_token), if no token is
104    /// cached in memory, the store is consulted before falling back to
105    /// re-authenticating with the access key. After every successful refresh
106    /// or initial auth, the new token is written back to the store. Use this
107    /// from short-lived strategy instances (Edge Functions, Workers, proxy
108    /// worker pools) to share a service-token cache across processes.
109    ///
110    /// Returns a new builder with the store type erased into the chain — see
111    /// [`InMemoryTokenStore`](crate::InMemoryTokenStore) and
112    /// [`TokenStoreFn`](crate::TokenStoreFn) for ready-made
113    /// implementations.
114    pub fn with_token_store<T: TokenStore>(self, store: T) -> AccessKeyStrategyBuilder<T> {
115        AccessKeyStrategyBuilder {
116            region: self.region,
117            access_key: self.access_key,
118            audience: self.audience,
119            base_url_override: self.base_url_override,
120            token_store: store,
121        }
122    }
123}
124
125impl<S: TokenStore> AccessKeyStrategyBuilder<S> {
126    /// Build the [`AccessKeyStrategy`].
127    ///
128    /// Resolves the base URL via service discovery unless overridden with
129    /// `base_url` (available when the `test-utils` feature is enabled).
130    pub fn build(self) -> Result<AccessKeyStrategy<S>, AuthError> {
131        let base_url = match self.base_url_override {
132            Some(url) => url,
133            None => crate::cts_base_url_from_env()?
134                .unwrap_or(CtsServiceDiscovery::endpoint(self.region)?),
135        };
136        let refresher = AccessKeyRefresher::new(
137            self.access_key,
138            ensure_trailing_slash(base_url),
139            self.audience,
140        );
141        Ok(AccessKeyStrategy {
142            inner: AutoRefresh::with_store(refresher, self.token_store),
143        })
144    }
145}