bitwarden_auth/send_access/api/
token_request_payload.rs

1use serde::{Deserialize, Serialize};
2
3use crate::{
4    api::enums::{GrantType, Scope},
5    send_access::{SendAccessCredentials, SendAccessTokenRequest},
6};
7
8/// Represents the shape of the credentials used in the send access token payload.
9#[derive(Serialize, Debug)]
10// untagged allows for different variants to be serialized without a type tag
11// example: { "password_hash_b64": "example_hash" } instead of { "Password": { "password_hash_b64":
12// "example_hash" } }
13#[serde(untagged)]
14pub(crate) enum SendAccessTokenPayloadCredentials {
15    // Uses inline variant syntax for these as we don't need to reference them as independent
16    // types elsewhere.
17    #[allow(missing_docs)]
18    Password { password_hash_b64: String },
19    #[allow(missing_docs)]
20    Email { email: String },
21    #[allow(missing_docs)]
22    EmailOtp { email: String, otp: String },
23    /// Represents an anonymous request, which does not require credentials.
24    Anonymous,
25}
26
27impl From<Option<SendAccessCredentials>> for SendAccessTokenPayloadCredentials {
28    fn from(credentials: Option<SendAccessCredentials>) -> Self {
29        match credentials {
30            Some(SendAccessCredentials::Password(credentials)) => {
31                SendAccessTokenPayloadCredentials::Password {
32                    password_hash_b64: credentials.password_hash_b64,
33                }
34            }
35            Some(SendAccessCredentials::Email(credentials)) => {
36                SendAccessTokenPayloadCredentials::Email {
37                    email: credentials.email,
38                }
39            }
40            Some(SendAccessCredentials::EmailOtp(credentials)) => {
41                SendAccessTokenPayloadCredentials::EmailOtp {
42                    email: credentials.email,
43                    otp: credentials.otp,
44                }
45            }
46            None => SendAccessTokenPayloadCredentials::Anonymous,
47        }
48    }
49}
50
51/// Enum representing the type of client requesting a send access token.
52/// Eventually, this could / should be merged with the existing `ClientType` enum
53#[derive(Serialize, Deserialize, Debug)]
54pub(crate) enum SendAccessClientType {
55    /// Represents a Send client.
56    /// This is a standalone client that lives within the BW web app, but has no context of a BW
57    /// user.
58    #[serde(rename = "send")]
59    Send,
60}
61
62/// Represents the actual request payload for requesting a send access token.
63/// It converts the `SendAccessTokenRequest` into a format suitable for sending to the API.
64#[derive(Serialize, Debug)]
65pub(crate) struct SendAccessTokenRequestPayload {
66    // Standard OAuth2 fields
67    /// The client ID for the send access client.
68    pub(crate) client_id: SendAccessClientType,
69
70    /// The grant type for the send access token request.
71    /// SendAccess is a custom grant type for send access tokens.
72    /// It is used to differentiate send access requests from other OAuth2 flows.
73    pub(crate) grant_type: GrantType,
74
75    /// The scope for the send access token request.
76    /// This is set to "api.send" to indicate that the token is for send access.
77    /// It allows the token to be used for accessing send-related resources.
78    pub(crate) scope: Scope,
79
80    // Custom fields
81    /// The ID of the send for which the access token is being requested.
82    pub(crate) send_id: String,
83
84    /// The credentials used for the send access request.
85    /// This can be password, email, email OTP, or anonymous.
86    // Flatten allows us to serialize the variant directly into the payload without a wrapper
87    // example: { "password_hash_b64": "example_hash" } instead of { "variant": {
88    // "password_hash_b64": "example_hash" } }
89    #[serde(flatten)]
90    pub(crate) credentials: SendAccessTokenPayloadCredentials,
91}
92
93const SEND_ACCESS_CLIENT_ID: SendAccessClientType = SendAccessClientType::Send;
94const SEND_ACCESS_GRANT_TYPE: GrantType = GrantType::SendAccess;
95const SEND_ACCESS_SCOPE: Scope = Scope::ApiSendAccess;
96
97/// Implement a way to convert from our request model to the payload model
98impl From<SendAccessTokenRequest> for SendAccessTokenRequestPayload {
99    fn from(request: SendAccessTokenRequest) -> Self {
100        // Returns a new instance of `SendAccessTokenPayload` based on the provided
101        // `SendAccessTokenRequest`. It extracts the necessary fields from the request and
102        // matches on the credentials to determine the variant
103        SendAccessTokenRequestPayload {
104            client_id: SEND_ACCESS_CLIENT_ID,
105            grant_type: SEND_ACCESS_GRANT_TYPE,
106            scope: SEND_ACCESS_SCOPE,
107            send_id: request.send_id,
108            credentials: request.send_access_credentials.into(),
109        }
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use serde_json;
116
117    use super::*;
118
119    /// Unit tests for `SendAccessTokenPayload` serialization
120    mod send_access_token_payload_tests {
121        use super::*;
122        #[test]
123        fn test_serialize_send_access_token_password_payload() {
124            let payload = SendAccessTokenRequestPayload {
125                client_id: SendAccessClientType::Send,
126                grant_type: GrantType::SendAccess,
127                scope: Scope::ApiSendAccess,
128                send_id: "example_send_id".into(),
129                credentials: SendAccessTokenPayloadCredentials::Password {
130                    password_hash_b64: "example_hash".into(),
131                },
132            };
133
134            let serialized = serde_json::to_string_pretty(&payload).unwrap();
135
136            // Parse both sides to JSON values and compare structurally.
137            let got: serde_json::Value = serde_json::from_str(&serialized).unwrap();
138            let want = serde_json::json!({
139                "client_id": "send",
140                "grant_type": "send_access",
141                "scope": "api.send.access",
142                "send_id": "example_send_id",
143                "password_hash_b64": "example_hash"
144            });
145
146            assert_eq!(got, want);
147        }
148
149        #[test]
150        fn test_serialize_send_access_token_email_payload() {
151            let payload = SendAccessTokenRequestPayload {
152                client_id: SendAccessClientType::Send,
153                grant_type: GrantType::SendAccess,
154                scope: Scope::ApiSendAccess,
155                send_id: "example_send_id".into(),
156                credentials: SendAccessTokenPayloadCredentials::Email {
157                    email: "example_email".into(),
158                },
159            };
160
161            let serialized = serde_json::to_string_pretty(&payload).unwrap();
162
163            // Parse both sides to JSON values and compare structurally.
164            let got: serde_json::Value = serde_json::from_str(&serialized).unwrap();
165            let want = serde_json::json!({
166                "client_id": "send",
167                "grant_type": "send_access",
168                "scope": "api.send.access",
169                "send_id": "example_send_id",
170                "email": "example_email"
171            });
172
173            assert_eq!(got, want);
174        }
175
176        #[test]
177        fn test_serialize_send_access_token_email_otp_payload() {
178            let payload = SendAccessTokenRequestPayload {
179                client_id: SendAccessClientType::Send,
180                grant_type: GrantType::SendAccess,
181                scope: Scope::ApiSendAccess,
182                send_id: "example_send_id".into(),
183                credentials: SendAccessTokenPayloadCredentials::EmailOtp {
184                    email: "example_email".into(),
185                    otp: "example_otp".into(),
186                },
187            };
188            let serialized = serde_json::to_string_pretty(&payload).unwrap();
189            // Parse both sides to JSON values and compare structurally.
190            let got: serde_json::Value = serde_json::from_str(&serialized).unwrap();
191            let want = serde_json::json!({
192                "client_id": "send",
193                "grant_type": "send_access",
194                "scope": "api.send.access",
195                "send_id": "example_send_id",
196                "email": "example_email",
197                "otp": "example_otp"
198            });
199
200            assert_eq!(got, want);
201        }
202    }
203}