Skip to main content

bitwarden_auth/send_access/
access_token_request.rs

1/// Credentials for sending password secured access requests.
2/// Clone auto implements the standard lib's Clone trait, allowing us to create copies of this
3/// struct.
4#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
5#[serde(rename_all = "camelCase", deny_unknown_fields)]
6#[bitwarden_ffi::wasm_record]
7pub struct SendPasswordCredentials {
8    /// A Base64-encoded hash of the password protecting the send.
9    pub password_hash_b64: String,
10}
11
12/// Credentials for sending an OTP to the user's email address.
13/// This is used when the send requires email verification with an OTP.
14#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
15#[bitwarden_ffi::wasm_record]
16pub struct SendEmailCredentials {
17    /// The email address to which the OTP will be sent.
18    pub email: String,
19}
20
21/// Credentials for getting a send access token using an email and OTP.
22#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
23#[bitwarden_ffi::wasm_record]
24pub struct SendEmailOtpCredentials {
25    /// The email address to which the OTP will be sent.
26    pub email: String,
27    /// The one-time password (OTP) that the user has received via email.
28    pub otp: String,
29}
30
31/// The credentials used for send access requests.
32#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
33#[bitwarden_ffi::wasm_record]
34// Use untagged so that each variant can be serialized without a type tag.
35// For example, this allows us to serialize the password credentials as just
36// {"password_hash_b64": "value"} instead of {"type": "password", "password_hash_b64": "value"}.
37#[serde(untagged)]
38pub enum SendAccessCredentials {
39    #[allow(missing_docs)]
40    Password(SendPasswordCredentials),
41    // IMPORTANT: EmailOtp must come before Email due to #[serde(untagged)] deserialization.
42    // Serde tries variants in order and stops at the first match. Since EmailOtp has
43    // both "email" and "otp" fields, while Email only has "email", placing Email first
44    // would cause {"email": "...", "otp": "..."} to incorrectly match the Email variant
45    // (ignoring the "otp" field). We always must order untagged enum variants from most specific
46    // (most fields) to least specific (fewest fields).
47    #[allow(missing_docs)]
48    EmailOtp(SendEmailOtpCredentials),
49    #[allow(missing_docs)]
50    Email(SendEmailCredentials),
51}
52
53/// A request structure for requesting a send access token from the API.
54#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
55#[serde(rename_all = "camelCase", deny_unknown_fields)]
56#[bitwarden_ffi::wasm_record]
57pub struct SendAccessTokenRequest {
58    /// The id of the send for which the access token is requested.
59    pub send_id: String,
60
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    #[cfg_attr(feature = "wasm", tsify(optional))]
63    /// The optional send access credentials.
64    pub send_access_credentials: Option<SendAccessCredentials>,
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    mod send_access_token_request_tests {
72        use serde_json::{from_str, to_string};
73
74        use super::*;
75
76        #[test]
77        fn deserialize_camelcase_request() {
78            let json = r#"
79        {
80          "sendId": "abc123",
81          "sendAccessCredentials": { "passwordHashB64": "ha$h" }
82        }"#;
83
84            let req: SendAccessTokenRequest = from_str(json).unwrap();
85            assert_eq!(req.send_id, "abc123");
86
87            let creds = req.send_access_credentials.expect("expected Some");
88            match creds {
89                SendAccessCredentials::Password(p) => assert_eq!(p.password_hash_b64, "ha$h"),
90                _ => panic!("expected Password variant"),
91            }
92        }
93
94        #[test]
95        fn serialize_camelcase_request_with_credentials() {
96            let req = SendAccessTokenRequest {
97                send_id: "abc123".into(),
98                send_access_credentials: Some(SendAccessCredentials::Password(
99                    SendPasswordCredentials {
100                        password_hash_b64: "ha$h".into(),
101                    },
102                )),
103            };
104            let json = to_string(&req).unwrap();
105            assert_eq!(
106                json,
107                r#"{"sendId":"abc123","sendAccessCredentials":{"passwordHashB64":"ha$h"}}"#
108            );
109        }
110
111        #[test]
112        fn serialize_omits_optional_credentials_when_none() {
113            let req = SendAccessTokenRequest {
114                send_id: "abc123".into(),
115                send_access_credentials: None,
116            };
117            let json = to_string(&req).unwrap();
118            assert_eq!(json, r#"{"sendId":"abc123"}"#);
119        }
120
121        #[test]
122        fn roundtrip_camel_in_to_camel_out() {
123            let in_json = r#"
124        {
125          "sendId": "abc123",
126          "sendAccessCredentials": { "passwordHashB64": "ha$h" }
127        }"#;
128
129            let req: SendAccessTokenRequest = from_str(in_json).unwrap();
130            let out_json = to_string(&req).unwrap();
131            assert_eq!(
132                out_json,
133                r#"{"sendId":"abc123","sendAccessCredentials":{"passwordHashB64":"ha$h"}}"#
134            );
135        }
136
137        #[test]
138        fn snakecase_top_level_keys_are_rejected() {
139            let json = r#"
140        {
141          "send_id": "abc123",
142          "sendAccessCredentials": { "passwordHashB64": "ha$h" }
143        }"#;
144            let err = from_str::<SendAccessTokenRequest>(json).unwrap_err();
145            let msg = err.to_string();
146            assert!(
147                msg.contains("unknown field") && msg.contains("send_id"),
148                "unexpected: {msg}"
149            );
150        }
151
152        #[test]
153        fn extra_top_level_key_is_rejected() {
154            let json = r#"
155        {
156          "sendId": "abc123",
157          "sendAccessCredentials": { "passwordHashB64": "ha$h" },
158          "extra": "nope"
159        }"#;
160            let err = from_str::<SendAccessTokenRequest>(json).unwrap_err();
161            let msg = err.to_string();
162            assert!(
163                msg.contains("unknown field") && msg.contains("extra"),
164                "unexpected: {msg}"
165            );
166        }
167
168        #[test]
169        fn snakecase_nested_keys_are_rejected() {
170            let json = r#"
171    {
172      "sendId": "abc123",
173      "sendAccessCredentials": { "password_hash_b64": "ha$h" }
174    }"#;
175
176            let err = serde_json::from_str::<SendAccessTokenRequest>(json).unwrap_err();
177            let msg = err.to_string();
178            assert!(
179                msg.contains("did not match any variant"),
180                "unexpected: {msg}"
181            );
182        }
183
184        #[test]
185        fn extra_nested_key_is_rejected() {
186            let json = r#"
187        {
188          "sendId": "abc123",
189          "sendAccessCredentials": {
190            "passwordHashB64": "ha$h",
191            "extra": "nope"
192          }
193        }"#;
194            let err = from_str::<SendAccessTokenRequest>(json).unwrap_err();
195            let msg = err.to_string();
196            assert!(
197                msg.contains("did not match any variant"),
198                "unexpected: {msg}"
199            );
200        }
201    }
202
203    mod send_access_credentials_tests {
204        use super::*;
205
206        mod send_access_password_credentials_tests {
207            use serde_json::{from_str, to_string};
208
209            use super::*;
210
211            #[test]
212            fn deserialize_struct_camelcase_from_ts() {
213                let json = r#"{ "passwordHashB64": "ha$h" }"#;
214                let s: SendPasswordCredentials = from_str(json).unwrap();
215                assert_eq!(s.password_hash_b64, "ha$h");
216            }
217
218            #[test]
219            fn serialize_struct_camelcase_to_wire() {
220                let s = SendPasswordCredentials {
221                    password_hash_b64: "ha$h".into(),
222                };
223                let json = to_string(&s).unwrap();
224                assert_eq!(json, r#"{"passwordHashB64":"ha$h"}"#);
225            }
226
227            #[test]
228            fn roundtrip_struct_camel_in_to_camel_out() {
229                let in_json = r#"{ "passwordHashB64": "ha$h" }"#;
230                let parsed: SendPasswordCredentials = from_str(in_json).unwrap();
231                let out_json = to_string(&parsed).unwrap();
232                assert_eq!(out_json, r#"{"passwordHashB64":"ha$h"}"#);
233            }
234
235            #[test]
236            fn deserialize_enum_variant_from_json() {
237                let json = r#"{"passwordHashB64":"ha$h"}"#;
238                let creds: SendAccessCredentials = from_str(json).unwrap();
239                match creds {
240                    SendAccessCredentials::Password(password_creds) => {
241                        assert_eq!(password_creds.password_hash_b64, "ha$h");
242                    }
243                    _ => panic!("Expected Password variant"),
244                }
245            }
246
247            #[test]
248            fn serialize_enum_variant_to_json() {
249                let creds = SendAccessCredentials::Password(SendPasswordCredentials {
250                    password_hash_b64: "ha$h".into(),
251                });
252                let json = to_string(&creds).unwrap();
253                assert_eq!(json, r#"{"passwordHashB64":"ha$h"}"#);
254            }
255
256            #[test]
257            fn roundtrip_enum_variant_through_json() {
258                let in_json = r#"{"passwordHashB64":"ha$h"}"#;
259                let creds: SendAccessCredentials = from_str(in_json).unwrap();
260                let out_json = to_string(&creds).unwrap();
261                assert_eq!(out_json, r#"{"passwordHashB64":"ha$h"}"#);
262            }
263        }
264
265        mod send_access_email_credentials_tests {
266            use serde_json::{from_str, to_string};
267
268            use super::*;
269
270            #[test]
271            fn deserialize_struct_camelcase_from_ts() {
272                let json = r#"{ "email": "[email protected]" }"#;
273                let s: SendEmailCredentials = from_str(json).unwrap();
274                assert_eq!(s.email, "[email protected]");
275            }
276
277            #[test]
278            fn serialize_struct_camelcase_to_wire() {
279                let s = SendEmailCredentials {
280                    email: "[email protected]".into(),
281                };
282                let json = to_string(&s).unwrap();
283                assert_eq!(json, r#"{"email":"[email protected]"}"#);
284            }
285
286            #[test]
287            fn roundtrip_struct_camel_in_to_camel_out() {
288                let in_json = r#"{ "email": "[email protected]" }"#;
289                let parsed: SendEmailCredentials = from_str(in_json).unwrap();
290                let out_json = to_string(&parsed).unwrap();
291                assert_eq!(out_json, r#"{"email":"[email protected]"}"#);
292            }
293
294            #[test]
295            fn deserialize_enum_variant_from_json() {
296                let json = r#"{"email":"[email protected]"}"#;
297                let creds: SendAccessCredentials = from_str(json).unwrap();
298                match creds {
299                    SendAccessCredentials::Email(email_creds) => {
300                        assert_eq!(email_creds.email, "[email protected]");
301                    }
302                    _ => panic!("Expected Email variant"),
303                }
304            }
305
306            #[test]
307            fn serialize_enum_variant_to_json() {
308                let creds = SendAccessCredentials::Email(SendEmailCredentials {
309                    email: "[email protected]".into(),
310                });
311                let json = to_string(&creds).unwrap();
312                assert_eq!(json, r#"{"email":"[email protected]"}"#);
313            }
314
315            #[test]
316            fn roundtrip_enum_variant_through_json() {
317                let in_json = r#"{"email":"[email protected]"}"#;
318                let creds: SendAccessCredentials = from_str(in_json).unwrap();
319                let out_json = to_string(&creds).unwrap();
320                assert_eq!(out_json, r#"{"email":"[email protected]"}"#);
321            }
322        }
323
324        mod send_access_email_otp_credentials_tests {
325            use serde_json::{from_str, to_string};
326
327            use super::*;
328
329            #[test]
330            fn deserialize_struct_camelcase_from_ts() {
331                let json = r#"{ "email": "[email protected]", "otp": "123456" }"#;
332                let s: SendEmailOtpCredentials = from_str(json).unwrap();
333                assert_eq!(s.email, "[email protected]");
334                assert_eq!(s.otp, "123456");
335            }
336
337            #[test]
338            fn serialize_struct_camelcase_to_wire() {
339                let s = SendEmailOtpCredentials {
340                    email: "[email protected]".into(),
341                    otp: "123456".into(),
342                };
343                let json = to_string(&s).unwrap();
344                assert_eq!(json, r#"{"email":"[email protected]","otp":"123456"}"#);
345            }
346
347            #[test]
348            fn roundtrip_struct_camel_in_to_camel_out() {
349                let in_json = r#"{ "email": "[email protected]", "otp": "123456" }"#;
350                let parsed: SendEmailOtpCredentials = from_str(in_json).unwrap();
351                let out_json = to_string(&parsed).unwrap();
352                assert_eq!(out_json, r#"{"email":"[email protected]","otp":"123456"}"#);
353            }
354
355            #[test]
356            fn deserialize_enum_variant_from_json() {
357                let json = r#"{"email":"[email protected]","otp":"123456"}"#;
358                let creds: SendAccessCredentials = from_str(json).unwrap();
359                match creds {
360                    SendAccessCredentials::EmailOtp(otp_creds) => {
361                        assert_eq!(otp_creds.email, "[email protected]");
362                        assert_eq!(otp_creds.otp, "123456");
363                    }
364                    _ => panic!("Expected EmailOtp variant"),
365                }
366            }
367
368            #[test]
369            fn serialize_enum_variant_to_json() {
370                let creds = SendAccessCredentials::EmailOtp(SendEmailOtpCredentials {
371                    email: "[email protected]".into(),
372                    otp: "123456".into(),
373                });
374                let json = to_string(&creds).unwrap();
375                assert_eq!(json, r#"{"email":"[email protected]","otp":"123456"}"#);
376            }
377
378            #[test]
379            fn roundtrip_enum_variant_through_json() {
380                let in_json = r#"{"email":"[email protected]","otp":"123456"}"#;
381                let creds: SendAccessCredentials = from_str(in_json).unwrap();
382                let out_json = to_string(&creds).unwrap();
383                assert_eq!(out_json, r#"{"email":"[email protected]","otp":"123456"}"#);
384            }
385        }
386    }
387}