Skip to main content

bitwarden_access_connector/auth/
token_handler.rs

1//! Bridges the access connector's session into `bitwarden_core::Client`.
2//!
3//! The connector authenticates through its own [`IdentityClient`][crate::auth::identity], so this
4//! handler owns no token state; it supplies core's auth middleware and holds the session for it.
5//!
6//! It is built empty: [`TokenHandler::initialize_middleware`] fills in the key store during client
7//! construction, then [`AccessConnectorTokenHandler::set_session`] adds the session.
8
9use std::sync::{Arc, RwLock};
10
11use bitwarden_core::{auth::TokenHandler, key_management::KeySlotIds};
12use bitwarden_crypto::KeyStore;
13use bitwarden_state::registry::StateRegistry;
14
15use crate::{api::AccessConnectorAuthMiddleware, auth::session::SessionManager};
16
17/// Token handler for the PAM access connector.
18#[derive(Clone, Default)]
19pub(crate) struct AccessConnectorTokenHandler {
20    inner: Arc<RwLock<Inner>>,
21}
22
23#[derive(Default)]
24struct Inner {
25    /// Set by [`TokenHandler::initialize_middleware`] during client construction.
26    key_store: Option<KeyStore<KeySlotIds>>,
27    /// Set by [`AccessConnectorTokenHandler::set_session`] once the session has authenticated.
28    session: Option<Arc<SessionManager>>,
29}
30
31impl AccessConnectorTokenHandler {
32    /// The key store core created. Clones of a [`KeyStore`] share their keys, so clearing it on
33    /// revocation reaches core too.
34    pub(crate) fn key_store(&self) -> Option<KeyStore<KeySlotIds>> {
35        self.inner
36            .read()
37            .expect("RwLock is not poisoned")
38            .key_store
39            .clone()
40    }
41
42    /// Requests made before this lands cannot be authenticated.
43    pub(crate) fn set_session(&self, session: Arc<SessionManager>) {
44        self.inner
45            .write()
46            .expect("RwLock is not poisoned")
47            .session
48            .replace(session);
49    }
50
51    pub(crate) fn session(&self) -> Option<Arc<SessionManager>> {
52        self.inner
53            .read()
54            .expect("RwLock is not poisoned")
55            .session
56            .clone()
57    }
58}
59
60#[async_trait::async_trait]
61impl TokenHandler for AccessConnectorTokenHandler {
62    fn initialize_middleware(
63        &self,
64        _state_registry: &StateRegistry,
65        _identity_config: bitwarden_api_base::Configuration,
66        key_store: KeyStore<KeySlotIds>,
67    ) -> Arc<dyn reqwest_middleware::Middleware> {
68        self.inner
69            .write()
70            .expect("RwLock is not poisoned")
71            .key_store
72            .replace(key_store);
73
74        Arc::new(AccessConnectorAuthMiddleware::new(self.clone()))
75    }
76
77    /// No-op: [`SessionManager`] owns the bearer and its renewal.
78    async fn set_tokens(&self, _token: String, _refresh_token: Option<String>, _expires_in: u64) {}
79}
80
81#[cfg(test)]
82mod tests {
83    use bitwarden_api_api::apis::AuthRequired;
84    use wiremock::{
85        Mock, MockServer, ResponseTemplate,
86        matchers::{method, path},
87    };
88
89    use super::*;
90
91    /// Startup reads the key store back out of the handler, so `build` must have populated it.
92    #[test]
93    fn initialize_middleware_back_populates_the_key_store() {
94        let handler = AccessConnectorTokenHandler::default();
95        assert!(handler.key_store().is_none());
96
97        let _client = bitwarden_core::ClientBuilder::new()
98            .with_token_handler(Arc::new(handler.clone()))
99            .build();
100
101        assert!(
102            handler.key_store().is_some(),
103            "ClientBuilder::build must leave the key store available"
104        );
105    }
106
107    /// The handler's key store is the client's, not a copy: clearing one empties the other.
108    #[test]
109    fn the_key_store_is_shared_with_the_client() {
110        use bitwarden_core::key_management::SymmetricKeySlotId;
111        use bitwarden_crypto::{SymmetricCryptoKey, SymmetricKeyAlgorithm};
112
113        let handler = AccessConnectorTokenHandler::default();
114        let client = bitwarden_core::ClientBuilder::new()
115            .with_token_handler(Arc::new(handler.clone()))
116            .build();
117
118        let slot = SymmetricKeySlotId::Organization(
119            "3f1a5c4e-7b2d-4f8a-9c6e-1d2b3a4c5d6e"
120                .parse()
121                .expect("uuid"),
122        );
123        let store = handler.key_store().expect("key store");
124        #[allow(deprecated)]
125        store
126            .context_mut()
127            .set_symmetric_key(
128                slot,
129                SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac),
130            )
131            .expect("set key");
132
133        assert!(
134            client
135                .internal
136                .get_key_store()
137                .context()
138                .has_symmetric_key(slot)
139        );
140
141        store.clear();
142
143        assert!(
144            !client
145                .internal
146                .get_key_store()
147                .context()
148                .has_symmetric_key(slot),
149            "clearing the session's store must empty the client's"
150        );
151    }
152
153    /// An authenticated request before a session is installed must fail, not go out bearer-less.
154    #[tokio::test]
155    async fn an_authenticated_request_without_a_session_fails_and_is_not_sent() {
156        let server = MockServer::start().await;
157        Mock::given(method("GET"))
158            .and(path("/test"))
159            .respond_with(ResponseTemplate::new(200))
160            .mount(&server)
161            .await;
162
163        let handler = AccessConnectorTokenHandler::default();
164        let client = reqwest_middleware::ClientBuilder::new(
165            bitwarden_api_base::new_http_client_builder()
166                .build()
167                .expect("http client"),
168        )
169        .with(AccessConnectorAuthMiddleware::new(handler))
170        .build();
171
172        client
173            .get(format!("{}/test", server.uri()))
174            .with_extension(AuthRequired::Bearer)
175            .send()
176            .await
177            .expect_err("no session means no bearer");
178
179        assert!(
180            server.received_requests().await.unwrap().is_empty(),
181            "the request must not reach the server unauthenticated"
182        );
183    }
184}