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}