nodedb_lite/sync/client/
token.rs1use nodedb_types::sync::wire::{TokenRefreshAckMsg, TokenRefreshMsg};
11
12use super::state::SyncClient;
13
14pub const TOKEN_REFRESH_MIN_BACKOFF_MS: u64 = 5_000; const TOKEN_REFRESH_MAX_BACKOFF_MS: u64 = 300_000; const TOKEN_PROVIDER_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
22
23impl SyncClient {
24 pub async fn should_refresh_token(&self) -> bool {
29 if self.config.token_provider.is_none() || self.config.token_lifetime_secs == 0 {
30 return false;
31 }
32 if *self.token_refresh_pending.lock().await {
33 return false;
34 }
35 let set_at = *self.token_set_at_ms.lock().await;
36 let now = crate::runtime::now_millis();
37 let elapsed_ms = now.saturating_sub(set_at);
38 let threshold_ms = self.config.token_lifetime_secs * 800; elapsed_ms >= threshold_ms
40 }
41
42 pub async fn is_refresh_backoff_elapsed(&self) -> bool {
48 let last = *self.token_last_attempt_ms.lock().await;
49 if last == 0 {
50 return true;
51 }
52 let backoff = *self.token_refresh_backoff_ms.lock().await;
53 let now = crate::runtime::now_millis();
54 now.saturating_sub(last) >= backoff
55 }
56
57 pub async fn initiate_token_refresh(&self) -> Option<TokenRefreshMsg> {
65 let provider = self.config.token_provider.as_ref()?;
66
67 *self.token_last_attempt_ms.lock().await = crate::runtime::now_millis();
69 *self.token_refresh_pending.lock().await = true;
70
71 tracing::info!("initiating JWT token refresh");
72
73 let fut = provider();
74 let result = tokio::time::timeout(TOKEN_PROVIDER_TIMEOUT, fut).await;
75
76 match result {
77 Ok(Some(new_token)) => {
78 *self.token_refresh_backoff_ms.lock().await = TOKEN_REFRESH_MIN_BACKOFF_MS;
80 Some(TokenRefreshMsg { new_token })
81 }
82 Ok(None) => {
83 tracing::warn!("token provider returned None; keeping push paused");
85 self.on_refresh_failure().await;
86 None
87 }
88 Err(_elapsed) => {
89 tracing::warn!(
91 timeout_secs = TOKEN_PROVIDER_TIMEOUT.as_secs(),
92 "token provider timed out; keeping push paused"
93 );
94 self.on_refresh_failure().await;
95 None
96 }
97 }
98 }
99
100 pub async fn handle_token_refresh_ack(&self, ack: &TokenRefreshAckMsg) {
102 *self.token_refresh_pending.lock().await = false;
103
104 if ack.success {
105 *self.token_set_at_ms.lock().await = crate::runtime::now_millis();
107 *self.push_paused_for_auth.lock().await = false;
108 *self.token_refresh_backoff_ms.lock().await = TOKEN_REFRESH_MIN_BACKOFF_MS;
109 tracing::info!(
110 expires_in_secs = ack.expires_in_secs,
111 "JWT token refresh accepted by Origin"
112 );
113 } else {
114 tracing::warn!(
116 error = ack.error.as_deref().unwrap_or("unknown"),
117 "JWT token refresh rejected by Origin; keeping push paused"
118 );
119 self.apply_backoff().await;
120 }
121 }
122
123 pub async fn pause_for_auth(&self) {
126 *self.push_paused_for_auth.lock().await = true;
127 tracing::warn!("delta push paused — auth failure, awaiting token refresh");
128 }
129
130 pub async fn is_push_paused_for_auth(&self) -> bool {
132 *self.push_paused_for_auth.lock().await
133 }
134
135 async fn on_refresh_failure(&self) {
143 *self.token_refresh_pending.lock().await = false;
144 self.apply_backoff().await;
147 }
148
149 async fn apply_backoff(&self) {
151 let mut backoff = self.token_refresh_backoff_ms.lock().await;
152 *backoff = (*backoff * 2).min(TOKEN_REFRESH_MAX_BACKOFF_MS);
153 tracing::debug!(
154 next_backoff_ms = *backoff,
155 "token refresh backoff increased"
156 );
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use crate::sync::client::SyncConfig;
164 use std::sync::Arc;
165
166 fn make_config_with_provider(returns: Option<&'static str>) -> SyncConfig {
167 let provider: crate::sync::client::config::TokenProvider =
168 Arc::new(move || Box::pin(async move { returns.map(|s| s.to_string()) }));
169 SyncConfig::new("wss://localhost:9090/sync", "test.jwt.token")
170 .with_token_provider(provider, 3600)
171 }
172
173 #[tokio::test]
174 async fn refresh_failure_keeps_push_paused() {
175 let config = make_config_with_provider(None); let client = SyncClient::new(config);
177 client.pause_for_auth().await;
178
179 let result = client.initiate_token_refresh().await;
181 assert!(result.is_none());
182
183 assert!(client.is_push_paused_for_auth().await);
185 assert!(!*client.token_refresh_pending.lock().await);
187 }
188
189 #[tokio::test]
190 async fn refresh_failure_applies_backoff() {
191 let config = make_config_with_provider(None);
192 let client = SyncClient::new(config);
193 client.pause_for_auth().await;
194
195 let initial_backoff = *client.token_refresh_backoff_ms.lock().await;
196
197 client.initiate_token_refresh().await;
198 let after_first = *client.token_refresh_backoff_ms.lock().await;
199 assert_eq!(
200 after_first,
201 (initial_backoff * 2).min(TOKEN_REFRESH_MAX_BACKOFF_MS)
202 );
203
204 client.initiate_token_refresh().await;
205 let after_second = *client.token_refresh_backoff_ms.lock().await;
206 assert_eq!(
207 after_second,
208 (after_first * 2).min(TOKEN_REFRESH_MAX_BACKOFF_MS)
209 );
210 }
211
212 #[tokio::test]
213 async fn refresh_success_clears_pause_and_resets_backoff() {
214 let config = make_config_with_provider(Some("fresh-jwt-token"));
215 let client = SyncClient::new(config);
216 client.pause_for_auth().await;
217
218 *client.token_refresh_backoff_ms.lock().await = 60_000;
220
221 let msg = client.initiate_token_refresh().await;
222 assert!(msg.is_some());
223 assert_eq!(msg.unwrap().new_token, "fresh-jwt-token");
224
225 client
227 .handle_token_refresh_ack(&TokenRefreshAckMsg {
228 success: true,
229 expires_in_secs: 3600,
230 error: None,
231 })
232 .await;
233
234 assert!(!client.is_push_paused_for_auth().await);
235 assert_eq!(
236 *client.token_refresh_backoff_ms.lock().await,
237 TOKEN_REFRESH_MIN_BACKOFF_MS
238 );
239 }
240
241 #[tokio::test]
242 async fn backoff_enforced_between_attempts() {
243 let config = make_config_with_provider(None);
244 let client = SyncClient::new(config);
245
246 *client.token_last_attempt_ms.lock().await = crate::runtime::now_millis();
248 *client.token_refresh_backoff_ms.lock().await = TOKEN_REFRESH_MIN_BACKOFF_MS;
249
250 assert!(!client.is_refresh_backoff_elapsed().await);
252
253 *client.token_last_attempt_ms.lock().await = 0;
255 assert!(client.is_refresh_backoff_elapsed().await);
256 }
257
258 #[tokio::test]
259 async fn backoff_capped_at_max() {
260 let config = make_config_with_provider(None);
261 let client = SyncClient::new(config);
262
263 *client.token_refresh_backoff_ms.lock().await = TOKEN_REFRESH_MAX_BACKOFF_MS / 2 + 1;
265 client.apply_backoff().await;
266 assert_eq!(
267 *client.token_refresh_backoff_ms.lock().await,
268 TOKEN_REFRESH_MAX_BACKOFF_MS
269 );
270
271 client.apply_backoff().await;
273 assert_eq!(
274 *client.token_refresh_backoff_ms.lock().await,
275 TOKEN_REFRESH_MAX_BACKOFF_MS
276 );
277 }
278}