Skip to main content

bitwarden_server_communication_config/
config.rs

1use serde::{Deserialize, Serialize};
2
3/// Server communication configuration
4#[derive(Debug, Clone, Serialize, Deserialize)]
5#[bitwarden_ffi::wasm_record]
6#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
7pub struct ServerCommunicationConfig {
8    /// Bootstrap configuration determining how to establish server communication
9    pub bootstrap: BootstrapConfig,
10}
11
12/// Bootstrap configuration for server communication
13#[derive(Debug, Clone, Serialize, Deserialize)]
14#[bitwarden_ffi::wasm_record]
15#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
16#[serde(tag = "type", rename_all = "camelCase")]
17pub enum BootstrapConfig {
18    /// Direct connection with no special authentication requirements
19    Direct,
20    /// SSO cookie vendor configuration for load balancer authentication
21    SsoCookieVendor(SsoCookieVendorConfig),
22}
23
24/// SSO cookie vendor configuration
25///
26/// This configuration is provided by the server.
27#[derive(Clone, Serialize, Deserialize)]
28#[serde(rename_all = "camelCase")]
29#[bitwarden_ffi::wasm_record]
30#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
31pub struct SsoCookieVendorConfig {
32    /// Identity provider login URL for browser redirect during bootstrap
33    pub idp_login_url: Option<String>,
34    /// Cookie name (base name, without shard suffix)
35    pub cookie_name: String,
36    /// Cookie domain for validation
37    pub cookie_domain: String,
38    /// Vault URL for cookie acquisition redirect
39    ///
40    /// This is the full vault URL (scheme + host + port) where the browser
41    /// should be redirected for SSO cookie acquisition.
42    pub vault_url: String,
43    /// Acquired cookies
44    ///
45    /// For sharded cookies, this contains multiple entries with names like
46    /// `AWSELBAuthSessionCookie-0`, `AWSELBAuthSessionCookie-1`, etc.
47    /// For unsharded cookies, this contains a single entry with the base name.
48    pub cookie_value: Option<Vec<crate::AcquiredCookie>>,
49}
50
51/// Request to set server communication configuration for a hostname
52///
53/// This is the input type for
54/// [`ServerCommunicationConfigClient::set_communication_type`](crate::ServerCommunicationConfigClient::set_communication_type).
55/// Unlike [`ServerCommunicationConfig`], this type does not include acquired cookies,
56/// since cookies are managed separately via
57/// [`ServerCommunicationConfigClient::acquire_cookie`](crate::ServerCommunicationConfigClient::acquire_cookie).
58#[derive(Debug, Clone, Serialize, Deserialize)]
59#[bitwarden_ffi::wasm_record]
60#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
61pub struct SetCommunicationTypeRequest {
62    /// Bootstrap configuration determining how to establish server communication
63    pub bootstrap: BootstrapConfigRequest,
64}
65
66/// Bootstrap configuration variant for [`SetCommunicationTypeRequest`]
67#[derive(Debug, Clone, Serialize, Deserialize)]
68#[bitwarden_ffi::wasm_record]
69#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
70#[serde(tag = "type", rename_all = "camelCase")]
71pub enum BootstrapConfigRequest {
72    /// Direct connection with no special authentication requirements
73    Direct,
74    /// SSO cookie vendor configuration for load balancer authentication
75    SsoCookieVendor(SsoCookieVendorConfigRequest),
76}
77
78/// SSO cookie vendor configuration for [`SetCommunicationTypeRequest`]
79///
80/// Contains the server-provided configuration fields without acquired cookies.
81#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83#[bitwarden_ffi::wasm_record]
84#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
85pub struct SsoCookieVendorConfigRequest {
86    /// Identity provider login URL for browser redirect during bootstrap
87    pub idp_login_url: Option<String>,
88    /// Cookie name (base name, without shard suffix)
89    pub cookie_name: String,
90    /// Cookie domain for validation
91    pub cookie_domain: String,
92    /// Vault URL for cookie acquisition redirect
93    ///
94    /// This is the full vault URL (scheme + host + port) where the browser
95    /// should be redirected for SSO cookie acquisition.
96    pub vault_url: String,
97}
98
99// We manually implement Debug to make sure we don't print sensitive cookie values
100impl std::fmt::Debug for SsoCookieVendorConfig {
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        f.debug_struct("SsoCookieVendorConfig")
103            .field("idp_login_url", &self.idp_login_url)
104            .field("cookie_name", &self.cookie_name)
105            .field("cookie_domain", &self.cookie_domain)
106            .field("vault_url", &self.vault_url)
107            .field(
108                "cookie_value",
109                &self.cookie_value.as_ref().map(|_| "[REDACTED]"),
110            )
111            .finish()
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn direct_bootstrap_serialization() {
121        let config = ServerCommunicationConfig {
122            bootstrap: BootstrapConfig::Direct,
123        };
124
125        let json = serde_json::to_string(&config).unwrap();
126        assert!(json.contains("\"type\":\"direct\""));
127
128        let deserialized: ServerCommunicationConfig = serde_json::from_str(&json).unwrap();
129        assert!(matches!(deserialized.bootstrap, BootstrapConfig::Direct));
130    }
131
132    #[test]
133    fn sso_cookie_vendor_serialization() {
134        let config = ServerCommunicationConfig {
135            bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
136                idp_login_url: Some("https://timeloop-auth.acme.com/login".to_string()),
137                cookie_name: "ALBAuthSessionCookie".to_string(),
138                cookie_domain: "vault.example.com".to_string(),
139                vault_url: "https://vault.example.com".to_string(),
140                cookie_value: None,
141            }),
142        };
143
144        let json = serde_json::to_string(&config).unwrap();
145        assert!(json.contains("\"type\":\"ssoCookieVendor\""));
146        assert!(json.contains("timeloop-auth.acme.com"));
147        assert!(json.contains("ALBAuthSessionCookie"));
148
149        // Verify SDK can parse server JSON with camelCase fields
150        let server_json = r#"{"bootstrap":{"type":"ssoCookieVendor","idpLoginUrl":"https://idp.example.com/login","cookieName":"TestCookie","cookieDomain":"example.com","vaultUrl":"https://vault.example.com"}}"#;
151        let parsed = serde_json::from_str::<ServerCommunicationConfig>(server_json).unwrap();
152        if let BootstrapConfig::SsoCookieVendor(vendor) = parsed.bootstrap {
153            assert_eq!(
154                vendor.idp_login_url,
155                Some("https://idp.example.com/login".to_string())
156            );
157            assert_eq!(vendor.cookie_name, "TestCookie");
158            assert_eq!(vendor.cookie_domain, "example.com");
159        } else {
160            panic!("Expected SsoCookieVendor variant");
161        }
162
163        let deserialized: ServerCommunicationConfig = serde_json::from_str(&json).unwrap();
164        if let BootstrapConfig::SsoCookieVendor(vendor_config) = deserialized.bootstrap {
165            assert_eq!(
166                vendor_config.idp_login_url,
167                Some("https://timeloop-auth.acme.com/login".to_string())
168            );
169            assert_eq!(vendor_config.cookie_name, "ALBAuthSessionCookie");
170            assert_eq!(vendor_config.cookie_domain, "vault.example.com");
171            assert!(vendor_config.cookie_value.is_none());
172        } else {
173            panic!("Expected SsoCookieVendor variant");
174        }
175    }
176
177    #[test]
178    fn cookie_value_some_and_none() {
179        use crate::AcquiredCookie;
180
181        // Test with None
182        let config_none = SsoCookieVendorConfig {
183            idp_login_url: Some("https://example.com".to_string()),
184            cookie_name: "TestCookie".to_string(),
185            cookie_domain: "example.com".to_string(),
186            vault_url: "https://vault.example.com".to_string(),
187            cookie_value: None,
188        };
189
190        let json_none = serde_json::to_string(&config_none).unwrap();
191        let deserialized_none: SsoCookieVendorConfig = serde_json::from_str(&json_none).unwrap();
192        assert!(deserialized_none.cookie_value.is_none());
193
194        // Test with Some - single cookie
195        let config_some = SsoCookieVendorConfig {
196            idp_login_url: Some("https://example.com".to_string()),
197            cookie_name: "TestCookie".to_string(),
198            cookie_domain: "example.com".to_string(),
199            vault_url: "https://vault.example.com".to_string(),
200            cookie_value: Some(vec![AcquiredCookie {
201                name: "TestCookie".to_string(),
202                value: "eyJhbGciOiJFUzI1NiIsImtpZCI6Im...".to_string(),
203            }]),
204        };
205
206        let json_some = serde_json::to_string(&config_some).unwrap();
207        let deserialized_some: SsoCookieVendorConfig = serde_json::from_str(&json_some).unwrap();
208        assert_eq!(deserialized_some.cookie_value.as_ref().unwrap().len(), 1);
209        assert_eq!(
210            deserialized_some.cookie_value.as_ref().unwrap()[0].name,
211            "TestCookie"
212        );
213
214        // Test with multiple shards
215        let config_sharded = SsoCookieVendorConfig {
216            idp_login_url: Some("https://example.com".to_string()),
217            cookie_name: "TestCookie".to_string(),
218            cookie_domain: "example.com".to_string(),
219            vault_url: "https://vault.example.com".to_string(),
220            cookie_value: Some(vec![
221                AcquiredCookie {
222                    name: "TestCookie-0".to_string(),
223                    value: "shard1".to_string(),
224                },
225                AcquiredCookie {
226                    name: "TestCookie-1".to_string(),
227                    value: "shard2".to_string(),
228                },
229                AcquiredCookie {
230                    name: "TestCookie-2".to_string(),
231                    value: "shard3".to_string(),
232                },
233            ]),
234        };
235
236        let json_sharded = serde_json::to_string(&config_sharded).unwrap();
237        let deserialized_sharded: SsoCookieVendorConfig =
238            serde_json::from_str(&json_sharded).unwrap();
239        assert_eq!(deserialized_sharded.cookie_value.as_ref().unwrap().len(), 3);
240        assert_eq!(
241            deserialized_sharded.cookie_value.as_ref().unwrap()[0].name,
242            "TestCookie-0"
243        );
244    }
245
246    #[test]
247    fn enum_variants() {
248        let direct = BootstrapConfig::Direct;
249        assert!(matches!(direct, BootstrapConfig::Direct));
250
251        let vendor = BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
252            idp_login_url: Some("https://example.com".to_string()),
253            cookie_name: "Cookie".to_string(),
254            cookie_domain: "example.com".to_string(),
255            vault_url: "https://vault.example.com".to_string(),
256            cookie_value: None,
257        });
258        assert!(matches!(vendor, BootstrapConfig::SsoCookieVendor(_)));
259    }
260
261    #[test]
262    fn debug_output_redacts_cookie_value() {
263        use crate::AcquiredCookie;
264
265        // Test that cookie values are not exposed in Debug output
266        let config_with_cookie = SsoCookieVendorConfig {
267            idp_login_url: Some("https://example.com/login".to_string()),
268            cookie_name: "SessionCookie".to_string(),
269            cookie_domain: "example.com".to_string(),
270            vault_url: "https://vault.example.com".to_string(),
271            cookie_value: Some(vec![AcquiredCookie {
272                name: "SessionCookie".to_string(),
273                value: "super-secret-cookie-value-abc123".to_string(),
274            }]),
275        };
276
277        let debug_output = format!("{:?}", config_with_cookie);
278
279        // Should contain non-sensitive fields
280        assert!(debug_output.contains("SsoCookieVendorConfig"));
281        assert!(debug_output.contains("example.com/login"));
282        assert!(debug_output.contains("SessionCookie"));
283        assert!(debug_output.contains("example.com"));
284
285        // Should NOT contain the actual cookie value
286        assert!(!debug_output.contains("super-secret-cookie-value-abc123"));
287        // Should show redaction marker
288        assert!(debug_output.contains("[REDACTED]"));
289    }
290}