|
1 | 1 | use std::sync::Arc; |
2 | | -use std::time::{SystemTime, UNIX_EPOCH}; |
3 | 2 |
|
4 | 3 | use url::Url; |
5 | 4 |
|
@@ -67,15 +66,17 @@ impl Refresher for AccessKeyRefresher { |
67 | 66 | } |
68 | 67 |
|
69 | 68 | let auth_resp: AuthoriseResponse = resp.json().await?; |
70 | | - let now = SystemTime::now() |
71 | | - .duration_since(UNIX_EPOCH) |
72 | | - .unwrap_or_default() |
73 | | - .as_secs(); |
74 | 69 |
|
75 | 70 | Ok(Token { |
76 | 71 | access_token: auth_resp.access_token, |
77 | 72 | token_type: "Bearer".to_string(), |
78 | | - expires_at: now + auth_resp.expiry, |
| 73 | + // CTS `/api/authorise` returns `expiry` as an ABSOLUTE Unix epoch (it is |
| 74 | + // the JWT `exp` claim), NOT a relative duration. The previous `now + expiry` |
| 75 | + // pushed the local expiry decades into the future, so `AutoRefresh` never |
| 76 | + // considered the token expired and never refreshed it — the token then |
| 77 | + // silently died at its real (~15 min) `exp` and every request failed until |
| 78 | + // the process restarted. Use the value as-is. See CIP-3233. |
| 79 | + expires_at: auth_resp.expiry, |
79 | 80 | refresh_token: None, |
80 | 81 | region: None, |
81 | 82 | client_id: None, |
@@ -107,10 +108,17 @@ mod tests { |
107 | 108 | use std::sync::Arc; |
108 | 109 | use std::time::{SystemTime, UNIX_EPOCH}; |
109 | 110 |
|
110 | | - fn auth_response_json(access: &str, expiry: u64) -> serde_json::Value { |
| 111 | + /// Build a mock `/api/authorise` response. CTS returns `expiry` as an |
| 112 | + /// ABSOLUTE Unix epoch (the JWT `exp` claim), so model that faithfully: the |
| 113 | + /// token is valid for `expires_in_secs` from now. |
| 114 | + fn auth_response_json(access: &str, expires_in_secs: u64) -> serde_json::Value { |
| 115 | + let now = SystemTime::now() |
| 116 | + .duration_since(UNIX_EPOCH) |
| 117 | + .unwrap() |
| 118 | + .as_secs(); |
111 | 119 | serde_json::json!({ |
112 | 120 | "accessToken": access, |
113 | | - "expiry": expiry |
| 121 | + "expiry": now + expires_in_secs |
114 | 122 | }) |
115 | 123 | } |
116 | 124 |
|
@@ -146,6 +154,50 @@ mod tests { |
146 | 154 | } |
147 | 155 | } |
148 | 156 |
|
| 157 | + // ---- Regression: CTS `expiry` is an absolute epoch (CIP-3233) ---- |
| 158 | + |
| 159 | + /// CTS `/api/authorise` returns `expiry` as an ABSOLUTE Unix epoch (the JWT |
| 160 | + /// `exp` claim), not a relative duration. The refresher must use it as-is. |
| 161 | + /// |
| 162 | + /// Pre-fix (`expires_at = now + expiry`), this token's `expires_at` lands |
| 163 | + /// ~decades in the future, so `is_expired()` is never true — the token never |
| 164 | + /// refreshes and silently dies at its real ~15-minute `exp`. The assertion |
| 165 | + /// below fails under the pre-fix arithmetic (`expires_in()` ≈ 1.7e9) and |
| 166 | + /// passes with the fix (`expires_in()` ≈ 900). |
| 167 | + #[tokio::test] |
| 168 | + async fn access_key_expiry_is_absolute_epoch_not_relative() { |
| 169 | + let now = SystemTime::now() |
| 170 | + .duration_since(UNIX_EPOCH) |
| 171 | + .unwrap() |
| 172 | + .as_secs(); |
| 173 | + let absolute_expiry = now + 900; // a 15-minute token, as an absolute epoch |
| 174 | + |
| 175 | + let mut mocks = MockSet::new(); |
| 176 | + mocks.mock(move |when, then| { |
| 177 | + when.post().path("/api/authorise"); |
| 178 | + then.json(serde_json::json!({ |
| 179 | + "accessToken": "tok", |
| 180 | + "expiry": absolute_expiry |
| 181 | + })); |
| 182 | + }); |
| 183 | + let server = start_server(mocks).await; |
| 184 | + |
| 185 | + let refresher = |
| 186 | + AccessKeyRefresher::new(SecretToken::new("CSAKid.secret"), server.url(""), None); |
| 187 | + let token = refresher.refresh(&()).await.unwrap(); |
| 188 | + |
| 189 | + assert!( |
| 190 | + token.expires_in() <= 1000, |
| 191 | + "expires_in should be ~900s (absolute `expiry` used as-is); got {} \ |
| 192 | + — pre-fix `now + expiry` yields ~1.7e9", |
| 193 | + token.expires_in() |
| 194 | + ); |
| 195 | + assert!( |
| 196 | + !token.is_expired(), |
| 197 | + "a fresh 15-minute token must not be reported as already expired" |
| 198 | + ); |
| 199 | + } |
| 200 | + |
149 | 201 | // ---- Initial auth tests ---- |
150 | 202 |
|
151 | 203 | #[tokio::test] |
@@ -405,9 +457,15 @@ mod tests { |
405 | 457 | state.counting.enter(); |
406 | 458 | tokio::time::sleep(state.delay).await; |
407 | 459 | state.counting.exit(); |
| 460 | + // CTS returns `expiry` as an absolute epoch (JWT `exp`); model a token |
| 461 | + // valid for 1 hour from now. |
| 462 | + let now = SystemTime::now() |
| 463 | + .duration_since(UNIX_EPOCH) |
| 464 | + .unwrap() |
| 465 | + .as_secs(); |
408 | 466 | axum::Json(serde_json::json!({ |
409 | 467 | "accessToken": "refreshed-token", |
410 | | - "expiry": 3600 |
| 468 | + "expiry": now + 3600 |
411 | 469 | })) |
412 | 470 | } |
413 | 471 |
|
|
0 commit comments