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