bitwarden_auth/send_access/
access_token_request.rs

1#[cfg(feature = "wasm")]
2use tsify::Tsify;
3
4/// Credentials for sending password secured access requests.
5/// Clone auto implements the standard lib's Clone trait, allowing us to create copies of this
6/// struct.
7#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
8#[serde(rename_all = "camelCase", deny_unknown_fields)]
9#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
10pub struct SendPasswordCredentials {
11    /// A Base64-encoded hash of the password protecting the send.
12    pub password_hash_b64: String,
13}
14
15/// Credentials for sending an OTP to the user's email address.
16/// This is used when the send requires email verification with an OTP.
17#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
18#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
19pub struct SendEmailCredentials {
20    /// The email address to which the OTP will be sent.
21    pub email: String,
22}
23
24/// Credentials for getting a send access token using an email and OTP.
25#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
26#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
27pub struct SendEmailOtpCredentials {
28    /// The email address to which the OTP will be sent.
29    pub email: String,
30    /// The one-time password (OTP) that the user has received via email.
31    pub otp: String,
32}
33
34/// The credentials used for send access requests.
35#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
36#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
37// Use untagged so that each variant can be serialized without a type tag.
38// For example, this allows us to serialize the password credentials as just
39// {"password_hash_b64": "value"} instead of {"type": "password", "password_hash_b64": "value"}.
40#[serde(untagged)]
41pub enum SendAccessCredentials {
42    #[allow(missing_docs)]
43    Password(SendPasswordCredentials),
44    #[allow(missing_docs)]
45    Email(SendEmailCredentials),
46    #[allow(missing_docs)]
47    EmailOtp(SendEmailOtpCredentials),
48}
49
50/// A request structure for requesting a send access token from the API.
51#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
52#[serde(rename_all = "camelCase", deny_unknown_fields)]
53#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
54pub struct SendAccessTokenRequest {
55    /// The id of the send for which the access token is requested.
56    pub send_id: String,
57
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    #[cfg_attr(feature = "wasm", tsify(optional))]
60    /// The optional send access credentials.
61    pub send_access_credentials: Option<SendAccessCredentials>,
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    mod send_access_token_request_tests {
69        use serde_json::{from_str, to_string};
70
71        use super::*;
72
73        #[test]
74        fn deserialize_camelcase_request() {
75            let json = r#"
76        {
77          "sendId": "abc123",
78          "sendAccessCredentials": { "passwordHashB64": "ha$h" }
79        }"#;
80
81            let req: SendAccessTokenRequest = from_str(json).unwrap();
82            assert_eq!(req.send_id, "abc123");
83
84            let creds = req.send_access_credentials.expect("expected Some");
85            match creds {
86                SendAccessCredentials::Password(p) => assert_eq!(p.password_hash_b64, "ha$h"),
87                _ => panic!("expected Password variant"),
88            }
89        }
90
91        #[test]
92        fn serialize_camelcase_request_with_credentials() {
93            let req = SendAccessTokenRequest {
94                send_id: "abc123".into(),
95                send_access_credentials: Some(SendAccessCredentials::Password(
96                    SendPasswordCredentials {
97                        password_hash_b64: "ha$h".into(),
98                    },
99                )),
100            };
101            let json = to_string(&req).unwrap();
102            assert_eq!(
103                json,
104                r#"{"sendId":"abc123","sendAccessCredentials":{"passwordHashB64":"ha$h"}}"#
105            );
106        }
107
108        #[test]
109        fn serialize_omits_optional_credentials_when_none() {
110            let req = SendAccessTokenRequest {
111                send_id: "abc123".into(),
112                send_access_credentials: None,
113            };
114            let json = to_string(&req).unwrap();
115            assert_eq!(json, r#"{"sendId":"abc123"}"#);
116        }
117
118        #[test]
119        fn roundtrip_camel_in_to_camel_out() {
120            let in_json = r#"
121        {
122          "sendId": "abc123",
123          "sendAccessCredentials": { "passwordHashB64": "ha$h" }
124        }"#;
125
126            let req: SendAccessTokenRequest = from_str(in_json).unwrap();
127            let out_json = to_string(&req).unwrap();
128            assert_eq!(
129                out_json,
130                r#"{"sendId":"abc123","sendAccessCredentials":{"passwordHashB64":"ha$h"}}"#
131            );
132        }
133
134        #[test]
135        fn snakecase_top_level_keys_are_rejected() {
136            let json = r#"
137        {
138          "send_id": "abc123",
139          "sendAccessCredentials": { "passwordHashB64": "ha$h" }
140        }"#;
141            let err = from_str::<SendAccessTokenRequest>(json).unwrap_err();
142            let msg = err.to_string();
143            assert!(
144                msg.contains("unknown field") && msg.contains("send_id"),
145                "unexpected: {msg}"
146            );
147        }
148
149        #[test]
150        fn extra_top_level_key_is_rejected() {
151            let json = r#"
152        {
153          "sendId": "abc123",
154          "sendAccessCredentials": { "passwordHashB64": "ha$h" },
155          "extra": "nope"
156        }"#;
157            let err = from_str::<SendAccessTokenRequest>(json).unwrap_err();
158            let msg = err.to_string();
159            assert!(
160                msg.contains("unknown field") && msg.contains("extra"),
161                "unexpected: {msg}"
162            );
163        }
164
165        #[test]
166        fn snakecase_nested_keys_are_rejected() {
167            let json = r#"
168    {
169      "sendId": "abc123",
170      "sendAccessCredentials": { "password_hash_b64": "ha$h" }
171    }"#;
172
173            let err = serde_json::from_str::<SendAccessTokenRequest>(json).unwrap_err();
174            let msg = err.to_string();
175            assert!(
176                msg.contains("did not match any variant"),
177                "unexpected: {msg}"
178            );
179        }
180
181        #[test]
182        fn extra_nested_key_is_rejected() {
183            let json = r#"
184        {
185          "sendId": "abc123",
186          "sendAccessCredentials": {
187            "passwordHashB64": "ha$h",
188            "extra": "nope"
189          }
190        }"#;
191            let err = from_str::<SendAccessTokenRequest>(json).unwrap_err();
192            let msg = err.to_string();
193            assert!(
194                msg.contains("did not match any variant"),
195                "unexpected: {msg}"
196            );
197        }
198    }
199
200    mod send_access_credentials_tests {
201        use super::*;
202
203        mod send_access_password_credentials_tests {
204            use serde_json::{from_str, to_string};
205
206            use super::*;
207            #[test]
208            fn deserializes_camelcase_from_ts() {
209                let json = r#"{ "passwordHashB64": "ha$h" }"#;
210                let s: SendPasswordCredentials = from_str(json).unwrap();
211                assert_eq!(s.password_hash_b64, "ha$h");
212            }
213
214            #[test]
215            fn serializes_camelcase_to_wire() {
216                let s = SendPasswordCredentials {
217                    password_hash_b64: "ha$h".into(),
218                };
219                let json = to_string(&s).unwrap();
220                assert_eq!(json, r#"{"passwordHashB64":"ha$h"}"#);
221            }
222
223            #[test]
224            fn roundtrip_camel_in_to_camel_out() {
225                let in_json = r#"{ "passwordHashB64": "ha$h" }"#;
226                let parsed: SendPasswordCredentials = from_str(in_json).unwrap();
227                let out_json = to_string(&parsed).unwrap();
228                assert_eq!(out_json, r#"{"passwordHashB64":"ha$h"}"#);
229            }
230        }
231
232        #[test]
233        fn serialize_email_credentials() {
234            let creds = SendAccessCredentials::Email(SendEmailCredentials {
235                email: "[email protected]".into(),
236            });
237            let json = serde_json::to_string(&creds).unwrap();
238            assert_eq!(json, r#"{"email":"[email protected]"}"#);
239        }
240
241        #[test]
242        fn serialize_email_otp_credentials() {
243            let creds = SendAccessCredentials::EmailOtp(SendEmailOtpCredentials {
244                email: "[email protected]".into(),
245                otp: "123456".into(),
246            });
247            let json = serde_json::to_string(&creds).unwrap();
248            assert_eq!(json, r#"{"email":"[email protected]","otp":"123456"}"#);
249        }
250    }
251}