1use std::{sync::Arc, time::Duration};
12
13use bitwarden_access_token::AccessToken;
14use bitwarden_core::{OrganizationId, key_management::KeySlotIds};
15use bitwarden_crypto::KeyStore;
16use tokio::{
17 sync::{Mutex, watch},
18 time::Instant,
19};
20
21use crate::{
22 auth::identity::{AuthError, AuthSuccess, IdentityClient},
23 crypto::{CryptoModuleError, unwrap_org_key},
24};
25
26const TOKEN_RENEW_MARGIN_SECS: u64 = 5 * 60;
28
29const BACKOFF_BASE: Duration = Duration::from_secs(1);
30
31const BACKOFF_CAP: Duration = Duration::from_secs(30);
32
33const NO_DEADLINE_MAX_TRIES: u32 = 3;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub(crate) enum SessionPhase {
39 Authenticating,
41 Active,
43 Expired,
45 Revoked,
47 Closed,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub(crate) enum SessionLost {
54 Revoked,
56 Closed,
58}
59
60#[derive(Debug, thiserror::Error)]
62pub(crate) enum SessionError {
63 #[error("session lost: {0:?}")]
65 Lost(SessionLost),
66 #[error("transient renewal error: {0}")]
68 Transient(String),
69}
70
71struct SessionState {
72 phase: SessionPhase,
73 bearer: Option<String>,
75 expires_at: Option<Instant>,
77 key_store: KeyStore<KeySlotIds>,
79 organization_id: Option<OrganizationId>,
81 phase_tx: watch::Sender<SessionPhase>,
82}
83
84impl SessionState {
85 fn needs_renewal(&self) -> bool {
87 match self.expires_at {
88 Some(t) => {
89 let margin = Duration::from_secs(TOKEN_RENEW_MARGIN_SECS);
90 match t.checked_duration_since(Instant::now()) {
91 Some(remaining) => remaining <= margin,
92 None => true, }
94 }
95 None => true,
96 }
97 }
98
99 fn active_bearer(&self) -> Result<String, SessionError> {
103 match (&self.bearer, self.phase) {
104 (Some(bearer), _) => Ok(bearer.clone()),
105 (None, SessionPhase::Revoked) => Err(SessionError::Lost(SessionLost::Revoked)),
106 (None, SessionPhase::Closed) => Err(SessionError::Lost(SessionLost::Closed)),
107 (None, _) => Err(SessionError::Transient("session has no bearer".to_owned())),
108 }
109 }
110
111 fn set_phase(&mut self, phase: SessionPhase) {
112 self.phase = phase;
113 self.phase_tx.send_if_modified(|p| {
114 *p = phase;
115 true
116 });
117 }
118
119 fn apply_success(
120 &mut self,
121 success: AuthSuccess,
122 token: &AccessToken,
123 ) -> Result<(), CryptoModuleError> {
124 use bitwarden_sensitive_value::ExposeSensitive as _;
125
126 let expires_at = Instant::now() + Duration::from_secs(success.expires_in);
127
128 unwrap_org_key(
129 &self.key_store,
130 success.organization_id,
131 token,
132 &success.encrypted_payload,
133 )?;
134
135 self.bearer = Some(success.access_token.expose().to_owned());
136 self.expires_at = Some(expires_at);
137 self.organization_id = Some(success.organization_id);
138 self.set_phase(SessionPhase::Active);
139 Ok(())
140 }
141
142 fn enter_terminal(&mut self, lost: SessionLost) {
144 let phase = match lost {
145 SessionLost::Revoked => SessionPhase::Revoked,
146 SessionLost::Closed => SessionPhase::Closed,
147 };
148 self.set_phase(phase);
149 self.bearer = None;
150 self.expires_at = None;
151 self.key_store.clear();
153 }
154}
155
156pub(crate) struct SessionManager {
159 state: Mutex<SessionState>,
160 identity: IdentityClient,
161 token: AccessToken,
162}
163
164impl std::fmt::Debug for SessionManager {
165 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166 f.debug_struct("SessionManager").finish_non_exhaustive()
167 }
168}
169
170impl SessionManager {
171 pub(crate) async fn new(
175 identity: IdentityClient,
176 token: AccessToken,
177 key_store: KeyStore<KeySlotIds>,
178 ) -> Result<Arc<Self>, SessionError> {
179 let (phase_tx, _phase_rx) = watch::channel(SessionPhase::Authenticating);
180
181 let state = SessionState {
182 phase: SessionPhase::Authenticating,
183 bearer: None,
184 expires_at: None,
185 key_store,
186 organization_id: None,
187 phase_tx,
188 };
189
190 let mgr = Arc::new(Self {
191 state: Mutex::new(state),
192 identity,
193 token,
194 });
195
196 mgr.renew_with_backoff(None, None).await?;
197
198 Ok(mgr)
199 }
200
201 pub(crate) async fn phase(&self) -> SessionPhase {
202 self.state.lock().await.phase
203 }
204
205 #[cfg(test)]
207 pub(crate) async fn key_store(&self) -> KeyStore<KeySlotIds> {
208 self.state.lock().await.key_store.clone()
209 }
210
211 pub(crate) async fn organization_id(&self) -> Option<OrganizationId> {
213 self.state.lock().await.organization_id
214 }
215
216 pub(crate) async fn bearer(&self, deadline: Option<Instant>) -> Result<String, SessionError> {
220 self.renew_with_backoff(deadline, None).await
221 }
222
223 pub(crate) async fn force_refresh(
226 &self,
227 stale: &str,
228 deadline: Option<Instant>,
229 ) -> Result<String, SessionError> {
230 self.renew_with_backoff(deadline, Some(stale)).await
231 }
232
233 pub(crate) async fn close(&self) {
235 let mut guard = self.state.lock().await;
236 if !matches!(guard.phase, SessionPhase::Revoked | SessionPhase::Closed) {
237 guard.enter_terminal(SessionLost::Closed);
238 }
239 }
240
241 async fn renew_with_backoff(
246 &self,
247 deadline: Option<Instant>,
248 stale: Option<&str>,
249 ) -> Result<String, SessionError> {
250 let mut delay = BACKOFF_BASE;
251 let mut tries: u32 = 0;
252
253 loop {
254 let mut guard = self.state.lock().await;
255
256 match guard.phase {
257 SessionPhase::Revoked => return Err(SessionError::Lost(SessionLost::Revoked)),
258 SessionPhase::Closed => return Err(SessionError::Lost(SessionLost::Closed)),
259 SessionPhase::Active
260 if !guard.needs_renewal() && guard.bearer.as_deref() != stale =>
261 {
262 return guard.active_bearer();
263 }
264 _ => {}
265 }
266
267 guard.set_phase(SessionPhase::Authenticating);
268
269 let result = self.identity.authenticate(&self.token).await;
271
272 let (kind, err_msg) = match result {
273 Ok(success) => match guard.apply_success(success, &self.token) {
274 Ok(()) => {
275 tracing::info!(retry = tries, "session authenticated");
276 return guard.active_bearer();
277 }
278 Err(e) => ("org key unwrap failed", e.to_string()),
280 },
281 Err(AuthError::Rejected) => {
282 guard.enter_terminal(SessionLost::Revoked);
283 tracing::warn!(
284 "session entered Revoked phase (credential rejected by identity server)"
285 );
286 return Err(SessionError::Lost(SessionLost::Revoked));
287 }
288 Err(AuthError::Transient(msg)) => ("transient", msg),
289 Err(AuthError::Protocol) => (
290 "protocol error",
291 "identity server returned unexpected response".to_owned(),
292 ),
293 };
294
295 tries += 1;
296
297 guard.set_phase(SessionPhase::Expired);
299 drop(guard);
300
301 if deadline.is_none() && tries >= NO_DEADLINE_MAX_TRIES {
302 tracing::warn!(
303 retry = tries,
304 "session renewal failed ({kind}): {err_msg}; giving up after {tries} attempts"
305 );
306 return Err(SessionError::Transient(err_msg));
307 }
308
309 let sleep_dur = match deadline {
310 None => delay,
311 Some(dl) => {
312 let remaining = dl.saturating_duration_since(Instant::now());
313 if remaining.is_zero() {
314 tracing::warn!(
315 retry = tries,
316 "session renewal deadline exceeded before retry; last error: {err_msg}"
317 );
318 return Err(SessionError::Transient(
319 "renewal deadline exceeded".to_owned(),
320 ));
321 }
322 delay.min(remaining)
323 }
324 };
325
326 tracing::warn!(
327 retry = tries,
328 sleep_ms = sleep_dur.as_millis(),
329 "session renewal failed ({kind}): {err_msg}; retrying after {sleep_dur:?}"
330 );
331 tokio::time::sleep(sleep_dur).await;
332 delay = (delay * 2).min(BACKOFF_CAP);
333 }
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use std::time::Duration;
340
341 use bitwarden_crypto::{
342 KeyDecryptable, PrimitiveEncryptable, SymmetricCryptoKey, SymmetricKeyAlgorithm,
343 };
344 use wiremock::{
345 Mock, MockServer, ResponseTemplate,
346 matchers::{method, path},
347 };
348
349 use super::*;
350 use crate::{
351 auth::identity::IdentityClient,
352 test_support::{
353 access_token_tagged, encrypted_payload_for, test_org_id, test_org_slot as org_slot,
354 test_token,
355 },
356 };
357
358 fn success_response(
360 bearer: &str,
361 expires_in: u64,
362 encrypted_payload: &str,
363 ) -> ResponseTemplate {
364 let bearer = access_token_tagged(test_org_id(), bearer);
365 let body = format!(
366 r#"{{"access_token":"{bearer}","expires_in":{expires_in},"encrypted_payload":"{encrypted_payload}"}}"#
367 );
368 ResponseTemplate::new(200)
369 .set_body_string(body)
370 .insert_header("content-type", "application/json")
371 }
372
373 fn rejected_response() -> ResponseTemplate {
374 ResponseTemplate::new(400)
375 .set_body_string(r#"{"error":"invalid_client"}"#)
376 .insert_header("content-type", "application/json")
377 }
378
379 #[tokio::test]
380 async fn successful_auth_populates_bearer_and_org_key() {
381 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
382 let encrypted_payload = encrypted_payload_for(&org_key);
383
384 let server = MockServer::start().await;
385 Mock::given(method("POST"))
386 .and(path("/connect/token"))
387 .respond_with(success_response("my-bearer", 3600, &encrypted_payload))
388 .mount(&server)
389 .await;
390
391 let client = IdentityClient::new(server.uri()).expect("client");
392 let mgr = SessionManager::new(client, test_token(), KeyStore::default())
393 .await
394 .expect("SessionManager::new");
395
396 assert_eq!(mgr.phase().await, SessionPhase::Active);
397
398 let bearer = mgr.bearer(None).await.expect("bearer");
399 assert_eq!(bearer, access_token_tagged(test_org_id(), "my-bearer"));
400 assert_eq!(server.received_requests().await.unwrap().len(), 1);
401
402 let store = mgr.key_store().await;
404 let probe_enc = {
405 let mut ctx = store.context();
406 "probe-value"
407 .encrypt(&mut ctx, org_slot())
408 .expect("encrypt probe")
409 };
410 let decrypted: String = probe_enc.decrypt_with_key(&org_key).expect("decrypt probe");
411 assert_eq!(decrypted, "probe-value");
412 }
413
414 #[tokio::test]
415 async fn expiry_margin_triggers_renewal() {
416 let org_key1 = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
418 let payload1 = encrypted_payload_for(&org_key1);
419 let org_key2 = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
420 let payload2 = encrypted_payload_for(&org_key2);
421
422 let server = MockServer::start().await;
423 Mock::given(method("POST"))
424 .and(path("/connect/token"))
425 .respond_with(success_response("first-token", 0, &payload1))
426 .up_to_n_times(1)
427 .mount(&server)
428 .await;
429 Mock::given(method("POST"))
430 .and(path("/connect/token"))
431 .respond_with(success_response("second-token", 3600, &payload2))
432 .mount(&server)
433 .await;
434
435 let client = IdentityClient::new(server.uri()).expect("client");
436 let mgr = SessionManager::new(client, test_token(), KeyStore::default())
437 .await
438 .expect("SessionManager::new");
439
440 let bearer = mgr.bearer(None).await.expect("bearer");
441 assert_eq!(bearer, access_token_tagged(test_org_id(), "second-token"));
442 assert_eq!(server.received_requests().await.unwrap().len(), 2);
443 }
444
445 #[tokio::test]
446 async fn concurrent_bearer_calls_coalesce_to_single_renewal() {
447 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
448 let payload = encrypted_payload_for(&org_key);
449
450 let server = MockServer::start().await;
451 Mock::given(method("POST"))
453 .and(path("/connect/token"))
454 .respond_with(success_response("initial-tok", 0, &payload))
455 .up_to_n_times(1)
456 .mount(&server)
457 .await;
458
459 let org_key2 = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
460 let payload2 = encrypted_payload_for(&org_key2);
461 Mock::given(method("POST"))
463 .and(path("/connect/token"))
464 .respond_with(
465 success_response("renewed-tok", 3600, &payload2)
466 .set_delay(Duration::from_millis(50)),
467 )
468 .mount(&server)
469 .await;
470
471 let client = IdentityClient::new(server.uri()).expect("client");
472 let mgr = Arc::new(
473 SessionManager::new(client, test_token(), KeyStore::default())
474 .await
475 .expect("SessionManager::new"),
476 );
477
478 let handles: Vec<_> = (0..5)
479 .map(|_| {
480 let m = Arc::clone(&mgr);
481 tokio::spawn(async move { m.bearer(None).await })
482 })
483 .collect();
484
485 for h in handles {
486 assert_eq!(
487 h.await.expect("spawn").expect("bearer"),
488 access_token_tagged(test_org_id(), "renewed-tok")
489 );
490 }
491
492 assert_eq!(server.received_requests().await.unwrap().len(), 2);
494 }
495
496 #[tokio::test]
497 async fn invalid_client_at_startup_returns_revoked() {
498 let server = MockServer::start().await;
499 Mock::given(method("POST"))
500 .and(path("/connect/token"))
501 .respond_with(rejected_response())
502 .mount(&server)
503 .await;
504
505 let client = IdentityClient::new(server.uri()).expect("client");
506 let result = SessionManager::new(client, test_token(), KeyStore::default()).await;
507
508 assert!(
509 matches!(result, Err(SessionError::Lost(SessionLost::Revoked))),
510 "expected Lost(Revoked), got {result:?}"
511 );
512 }
513
514 #[tokio::test]
515 async fn revoked_session_short_circuits_without_identity_hits() {
516 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
517 let payload = encrypted_payload_for(&org_key);
518
519 let server = MockServer::start().await;
520 Mock::given(method("POST"))
521 .and(path("/connect/token"))
522 .respond_with(success_response("tok", 3600, &payload))
523 .up_to_n_times(1)
524 .mount(&server)
525 .await;
526 Mock::given(method("POST"))
527 .and(path("/connect/token"))
528 .respond_with(rejected_response())
529 .mount(&server)
530 .await;
531
532 let client = IdentityClient::new(server.uri()).expect("client");
533 let mgr = SessionManager::new(client, test_token(), KeyStore::default())
534 .await
535 .expect("first auth succeeds");
536
537 let _ = mgr
539 .force_refresh(&access_token_tagged(test_org_id(), "tok"), None)
540 .await;
541 assert_eq!(mgr.phase().await, SessionPhase::Revoked);
542
543 let count_before = server.received_requests().await.unwrap().len();
544
545 let err = mgr.bearer(None).await.expect_err("should be lost");
546 assert!(matches!(err, SessionError::Lost(SessionLost::Revoked)));
547
548 assert_eq!(
549 server.received_requests().await.unwrap().len(),
550 count_before
551 );
552 }
553
554 #[tokio::test]
555 async fn close_transitions_to_closed_and_short_circuits() {
556 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
557 let payload = encrypted_payload_for(&org_key);
558
559 let server = MockServer::start().await;
560 Mock::given(method("POST"))
561 .and(path("/connect/token"))
562 .respond_with(success_response("tok", 3600, &payload))
563 .mount(&server)
564 .await;
565
566 let client = IdentityClient::new(server.uri()).expect("client");
567 let mgr = SessionManager::new(client, test_token(), KeyStore::default())
568 .await
569 .expect("auth");
570
571 mgr.close().await;
572 assert_eq!(mgr.phase().await, SessionPhase::Closed);
573
574 let count_before = server.received_requests().await.unwrap().len();
575 let err = mgr.bearer(None).await.expect_err("should be lost");
576 assert!(matches!(err, SessionError::Lost(SessionLost::Closed)));
577 assert_eq!(
578 server.received_requests().await.unwrap().len(),
579 count_before
580 );
581 }
582
583 #[tokio::test]
584 async fn secrets_cleared_on_revoked() {
585 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
586 let payload = encrypted_payload_for(&org_key);
587
588 let server = MockServer::start().await;
589 Mock::given(method("POST"))
590 .and(path("/connect/token"))
591 .respond_with(success_response("tok", 3600, &payload))
592 .up_to_n_times(1)
593 .mount(&server)
594 .await;
595 Mock::given(method("POST"))
596 .and(path("/connect/token"))
597 .respond_with(rejected_response())
598 .mount(&server)
599 .await;
600
601 let client = IdentityClient::new(server.uri()).expect("client");
602 let mgr = SessionManager::new(client, test_token(), KeyStore::default())
603 .await
604 .expect("first auth");
605
606 let _ = mgr
607 .force_refresh(&access_token_tagged(test_org_id(), "tok"), None)
608 .await;
609 assert_eq!(mgr.phase().await, SessionPhase::Revoked);
610
611 let err = mgr.bearer(None).await.expect_err("must be lost");
612 assert!(matches!(err, SessionError::Lost(SessionLost::Revoked)));
613
614 let store = mgr.key_store().await;
615 assert!(
616 !store.context().has_symmetric_key(org_slot()),
617 "org key slot must be cleared after revocation"
618 );
619 }
620
621 #[tokio::test]
623 async fn key_store_handle_taken_before_revocation_loses_the_org_key() {
624 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
625 let payload = encrypted_payload_for(&org_key);
626
627 let server = MockServer::start().await;
628 Mock::given(method("POST"))
629 .and(path("/connect/token"))
630 .respond_with(success_response("tok", 3600, &payload))
631 .up_to_n_times(1)
632 .mount(&server)
633 .await;
634 Mock::given(method("POST"))
635 .and(path("/connect/token"))
636 .respond_with(rejected_response())
637 .mount(&server)
638 .await;
639
640 let client = IdentityClient::new(server.uri()).expect("client");
641 let mgr = SessionManager::new(client, test_token(), KeyStore::default())
642 .await
643 .expect("first auth");
644
645 let hoisted = mgr.key_store().await;
647 assert!(
648 hoisted.context().has_symmetric_key(org_slot()),
649 "org key must be installed before revocation"
650 );
651
652 let _ = mgr
653 .force_refresh(&access_token_tagged(test_org_id(), "tok"), None)
654 .await;
655 assert_eq!(mgr.phase().await, SessionPhase::Revoked);
656
657 assert!(
658 !hoisted.context().has_symmetric_key(org_slot()),
659 "a handle taken before revocation must not still hold the org key"
660 );
661 }
662
663 #[tokio::test]
665 async fn key_store_handle_taken_before_close_loses_the_org_key() {
666 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
667 let payload = encrypted_payload_for(&org_key);
668
669 let server = MockServer::start().await;
670 Mock::given(method("POST"))
671 .and(path("/connect/token"))
672 .respond_with(success_response("tok", 3600, &payload))
673 .mount(&server)
674 .await;
675
676 let client = IdentityClient::new(server.uri()).expect("client");
677 let mgr = SessionManager::new(client, test_token(), KeyStore::default())
678 .await
679 .expect("auth");
680
681 let hoisted = mgr.key_store().await;
682 assert!(hoisted.context().has_symmetric_key(org_slot()));
683
684 mgr.close().await;
685
686 assert!(
687 !hoisted.context().has_symmetric_key(org_slot()),
688 "a handle taken before close must not still hold the org key"
689 );
690 }
691
692 #[tokio::test]
693 async fn secrets_cleared_on_closed() {
694 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
695 let payload = encrypted_payload_for(&org_key);
696
697 let server = MockServer::start().await;
698 Mock::given(method("POST"))
699 .and(path("/connect/token"))
700 .respond_with(success_response("tok", 3600, &payload))
701 .mount(&server)
702 .await;
703
704 let client = IdentityClient::new(server.uri()).expect("client");
705 let mgr = SessionManager::new(client, test_token(), KeyStore::default())
706 .await
707 .expect("auth");
708
709 mgr.close().await;
710
711 let store = mgr.key_store().await;
712 assert!(
713 !store.context().has_symmetric_key(org_slot()),
714 "org key slot must be cleared after close"
715 );
716 }
717
718 #[tokio::test]
719 async fn force_refresh_reuses_token_if_already_renewed() {
720 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
721 let payload = encrypted_payload_for(&org_key);
722
723 let server = MockServer::start().await;
724 Mock::given(method("POST"))
725 .and(path("/connect/token"))
726 .respond_with(success_response("fresh-tok", 3600, &payload))
727 .mount(&server)
728 .await;
729
730 let client = IdentityClient::new(server.uri()).expect("client");
731 let mgr = SessionManager::new(client, test_token(), KeyStore::default())
732 .await
733 .expect("auth");
734
735 let count_after_init = server.received_requests().await.unwrap().len();
736
737 let result = mgr
739 .force_refresh("old-stale-value", None)
740 .await
741 .expect("force_refresh");
742 assert_eq!(result, access_token_tagged(test_org_id(), "fresh-tok"));
743 assert_eq!(
744 server.received_requests().await.unwrap().len(),
745 count_after_init,
746 "no additional identity call should be made"
747 );
748 }
749
750 #[tokio::test]
751 async fn concurrent_force_refresh_with_same_stale_bearer_renews_once() {
752 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
753 let payload = encrypted_payload_for(&org_key);
754
755 let server = MockServer::start().await;
756 Mock::given(method("POST"))
757 .and(path("/connect/token"))
758 .respond_with(success_response("initial-tok", 3600, &payload))
759 .up_to_n_times(1)
760 .mount(&server)
761 .await;
762 Mock::given(method("POST"))
764 .and(path("/connect/token"))
765 .respond_with(
766 success_response("renewed-tok", 3600, &payload)
767 .set_delay(Duration::from_millis(50)),
768 )
769 .mount(&server)
770 .await;
771
772 let client = IdentityClient::new(server.uri()).expect("client");
773 let mgr = SessionManager::new(client, test_token(), KeyStore::default())
774 .await
775 .expect("SessionManager::new");
776
777 let guard = mgr.state.lock().await;
780 let stale = access_token_tagged(test_org_id(), "initial-tok");
781 let handles: Vec<_> = (0..5)
782 .map(|_| {
783 let m = Arc::clone(&mgr);
784 let stale = stale.clone();
785 tokio::spawn(async move { m.force_refresh(&stale, None).await })
786 })
787 .collect();
788 tokio::time::sleep(Duration::from_millis(20)).await;
789 drop(guard);
790
791 for h in handles {
792 assert_eq!(
793 h.await.expect("spawn").expect("force_refresh"),
794 access_token_tagged(test_org_id(), "renewed-tok")
795 );
796 }
797
798 assert_eq!(server.received_requests().await.unwrap().len(), 2);
800 }
801
802 #[tokio::test]
803 async fn org_key_re_derived_on_every_refresh() {
804 let org_key1 = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
805 let payload1 = encrypted_payload_for(&org_key1);
806 let org_key2 = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
807 let payload2 = encrypted_payload_for(&org_key2);
808
809 let server = MockServer::start().await;
810 Mock::given(method("POST"))
811 .and(path("/connect/token"))
812 .respond_with(success_response("tok1", 0, &payload1))
813 .up_to_n_times(1)
814 .mount(&server)
815 .await;
816 Mock::given(method("POST"))
817 .and(path("/connect/token"))
818 .respond_with(success_response("tok2", 3600, &payload2))
819 .mount(&server)
820 .await;
821
822 let client = IdentityClient::new(server.uri()).expect("client");
823 let mgr = SessionManager::new(client, test_token(), KeyStore::default())
824 .await
825 .expect("first auth");
826
827 let _bearer2 = mgr.bearer(None).await.expect("second bearer");
829
830 let store = mgr.key_store().await;
831 let probe_enc = {
832 let mut ctx = store.context();
833 "check-key".encrypt(&mut ctx, org_slot()).expect("encrypt")
834 };
835
836 let ok: Result<String, _> = probe_enc.decrypt_with_key(&org_key2);
837 assert!(ok.is_ok(), "probe must decrypt under the new org key");
838 let fail: Result<String, _> = probe_enc.decrypt_with_key(&org_key1);
839 assert!(
840 fail.is_err(),
841 "probe must NOT decrypt under the old org key"
842 );
843 }
844
845 #[tokio::test]
848 async fn org_key_unwrap_failure_retries_with_backoff_and_expires() {
849 let server = MockServer::start().await;
850 Mock::given(method("POST"))
851 .and(path("/connect/token"))
852 .respond_with(success_response("tok", 3600, "not-a-valid-payload"))
853 .mount(&server)
854 .await;
855
856 let client = IdentityClient::new(server.uri()).expect("client");
857 let (phase_tx, _phase_rx) = watch::channel(SessionPhase::Authenticating);
858 let mgr = SessionManager {
859 state: Mutex::new(SessionState {
860 phase: SessionPhase::Authenticating,
861 bearer: None,
862 expires_at: None,
863 key_store: KeyStore::default(),
864 organization_id: None,
865 phase_tx,
866 }),
867 identity: client,
868 token: test_token(),
869 };
870
871 let deadline = Instant::now() + Duration::from_millis(1500);
874 let err = mgr
875 .bearer(Some(deadline))
876 .await
877 .expect_err("unwrap must fail");
878 assert!(
879 matches!(err, SessionError::Transient(_)),
880 "expected Transient, got {err:?}"
881 );
882 assert!(
883 server.received_requests().await.unwrap().len() > 1,
884 "must retry with backoff"
885 );
886 assert_eq!(mgr.phase().await, SessionPhase::Expired);
887 }
888
889 #[test]
890 fn active_bearer_maps_phase_and_bearer() {
891 let state = |phase, bearer: Option<&str>| {
892 let (phase_tx, _phase_rx) = watch::channel(phase);
893 SessionState {
894 phase,
895 bearer: bearer.map(str::to_owned),
896 expires_at: None,
897 key_store: KeyStore::default(),
898 organization_id: None,
899 phase_tx,
900 }
901 };
902
903 assert_eq!(
904 state(SessionPhase::Active, Some("x"))
905 .active_bearer()
906 .expect("bearer"),
907 "x"
908 );
909 assert!(matches!(
910 state(SessionPhase::Closed, None).active_bearer(),
911 Err(SessionError::Lost(SessionLost::Closed))
912 ));
913 assert!(matches!(
914 state(SessionPhase::Revoked, None).active_bearer(),
915 Err(SessionError::Lost(SessionLost::Revoked))
916 ));
917 assert!(matches!(
918 state(SessionPhase::Active, None).active_bearer(),
919 Err(SessionError::Transient(_))
920 ));
921 }
922}