Skip to main content

bitwarden_auth/login/login_via_password/
password_prelogin.rs

1use bitwarden_api_identity::models::PasswordPreloginRequestModel;
2use bitwarden_core::ApiError;
3use bitwarden_error::bitwarden_error;
4use thiserror::Error;
5#[cfg(feature = "wasm")]
6use wasm_bindgen::prelude::*;
7
8use crate::login::{LoginClient, login_via_password::PasswordPreloginResponse};
9
10/// Error type for password prelogin operations
11#[bitwarden_error(flat)]
12#[derive(Debug, Error)]
13pub enum PasswordPreloginError {
14    /// API error occurred during the prelogin request
15    #[error(transparent)]
16    Api(#[from] ApiError),
17
18    /// An unknown error occurred
19    /// This variant ensures the SDK can handle new error types introduced by the server
20    /// without breaking existing client code.
21    #[error("Unknown password prelogin error: {0}")]
22    Unknown(String),
23}
24
25/// Converts MissingFieldError into PasswordPreloginError::Unknown
26/// We need this because we use the !require macro which returns MissingFieldError
27/// to enforce that salt and kdf_settings are present in the response.
28impl From<bitwarden_core::MissingFieldError> for PasswordPreloginError {
29    fn from(err: bitwarden_core::MissingFieldError) -> Self {
30        PasswordPreloginError::Unknown(err.to_string())
31    }
32}
33
34#[cfg_attr(feature = "wasm", wasm_bindgen)]
35impl LoginClient {
36    /// Retrieves the data required before authenticating with a password.
37    /// This includes the user's KDF configuration needed to properly derive the master key.
38    pub async fn get_password_prelogin(
39        &self,
40        email: String,
41    ) -> Result<PasswordPreloginResponse, PasswordPreloginError> {
42        let request_model = PasswordPreloginRequestModel::new(email);
43        let api_configs = self.client.internal.get_api_configurations();
44        let response = api_configs
45            .identity_client
46            .accounts_api()
47            .post_password_prelogin(Some(request_model))
48            .await
49            .map_err(ApiError::from)?;
50
51        Ok(PasswordPreloginResponse::try_from(response)?)
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use std::num::NonZeroU32;
58
59    use bitwarden_api_identity::models::KdfType;
60    use bitwarden_core::{ClientSettings, DeviceType};
61    use bitwarden_crypto::Kdf;
62    use bitwarden_test::start_api_mock;
63    use wiremock::{Mock, ResponseTemplate, matchers};
64
65    use super::*;
66
67    const TEST_EMAIL: &str = "[email protected]";
68    const TEST_SALT_PBKDF2: &str = "test-salt-value";
69    const TEST_SALT_ARGON2: &str = "argon2-salt-value";
70
71    fn make_login_client(mock_server: &wiremock::MockServer) -> LoginClient {
72        let settings = ClientSettings {
73            identity_url: format!("http://{}/identity", mock_server.address()),
74            api_url: format!("http://{}/api", mock_server.address()),
75            user_agent: "Bitwarden Rust-SDK [TEST]".into(),
76            device_type: DeviceType::SDK,
77            device_identifier: None,
78            bitwarden_client_version: None,
79            bitwarden_package_type: None,
80        };
81        LoginClient::new(settings)
82    }
83
84    fn mock_default_pbkdf2_iterations() -> NonZeroU32 {
85        let Kdf::PBKDF2 { iterations } = Kdf::default_pbkdf2() else {
86            panic!("Expected PBKDF2 KDF");
87        };
88        iterations
89    }
90
91    fn mock_default_argon2_params() -> (NonZeroU32, NonZeroU32, NonZeroU32) {
92        let Kdf::Argon2id {
93            iterations,
94            memory,
95            parallelism,
96        } = Kdf::default_argon2()
97        else {
98            panic!("Expected Argon2 KDF");
99        };
100        (iterations, memory, parallelism)
101    }
102
103    #[tokio::test]
104    async fn test_get_password_prelogin_pbkdf2_success() {
105        // Create a mock success response with PBKDF2
106        let raw_success = serde_json::json!({
107            "kdfSettings": {
108                "kdfType": KdfType::PBKDF2_SHA256.as_i64(),
109                "iterations": mock_default_pbkdf2_iterations().get()
110            },
111            "salt": TEST_SALT_PBKDF2
112        });
113
114        let mock = Mock::given(matchers::method("POST"))
115            .and(matchers::path("identity/accounts/prelogin/password"))
116            .and(matchers::header(
117                reqwest::header::CONTENT_TYPE.as_str(),
118                "application/json",
119            ))
120            .respond_with(ResponseTemplate::new(200).set_body_json(raw_success));
121
122        let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
123        let login_client = make_login_client(&mock_server);
124
125        let result = login_client
126            .get_password_prelogin(TEST_EMAIL.to_string())
127            .await
128            .unwrap();
129
130        assert_eq!(result.salt, TEST_SALT_PBKDF2);
131        match result.kdf {
132            Kdf::PBKDF2 { iterations } => {
133                assert_eq!(iterations, mock_default_pbkdf2_iterations());
134            }
135            _ => panic!("Expected PBKDF2 KDF type"),
136        }
137    }
138
139    #[tokio::test]
140    async fn test_get_password_prelogin_argon2id_success() {
141        let (default_iterations, default_memory, default_parallelism) =
142            mock_default_argon2_params();
143
144        // Create a mock success response with Argon2id
145        let raw_success = serde_json::json!({
146            "kdfSettings": {
147                "kdfType": KdfType::Argon2id.as_i64(),
148                "iterations": default_iterations.get(),
149                "memory": default_memory.get(),
150                "parallelism": default_parallelism.get(),
151            },
152            "salt": TEST_SALT_ARGON2
153        });
154
155        let mock = Mock::given(matchers::method("POST"))
156            .and(matchers::path("identity/accounts/prelogin/password"))
157            .and(matchers::header(
158                reqwest::header::CONTENT_TYPE.as_str(),
159                "application/json",
160            ))
161            .respond_with(ResponseTemplate::new(200).set_body_json(raw_success));
162
163        let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
164        let login_client = make_login_client(&mock_server);
165
166        let result = login_client
167            .get_password_prelogin(TEST_EMAIL.to_string())
168            .await
169            .unwrap();
170
171        assert_eq!(result.salt, TEST_SALT_ARGON2);
172        match result.kdf {
173            Kdf::Argon2id {
174                iterations,
175                memory,
176                parallelism,
177            } => {
178                assert_eq!(iterations, default_iterations);
179                assert_eq!(memory, default_memory);
180                assert_eq!(parallelism, default_parallelism);
181            }
182            _ => panic!("Expected Argon2id KDF type"),
183        }
184    }
185
186    #[tokio::test]
187    async fn test_get_password_prelogin_missing_kdf_settings() {
188        // Create a mock response missing kdf_settings
189        let raw_response = serde_json::json!({
190            "salt": TEST_SALT_PBKDF2
191        });
192
193        let mock = Mock::given(matchers::method("POST"))
194            .and(matchers::path("identity/accounts/prelogin/password"))
195            .respond_with(ResponseTemplate::new(200).set_body_json(raw_response));
196
197        let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
198        let login_client = make_login_client(&mock_server);
199
200        let result = login_client
201            .get_password_prelogin(TEST_EMAIL.to_string())
202            .await;
203
204        assert!(result.is_err());
205        match result.unwrap_err() {
206            PasswordPreloginError::Unknown(err) => {
207                assert_eq!(
208                    err,
209                    "The response received was missing a required field: response.kdf_settings"
210                );
211            }
212            other => panic!("Expected MissingField error, got {:?}", other),
213        }
214    }
215
216    #[tokio::test]
217    async fn test_get_password_prelogin_missing_salt() {
218        // Create a mock response missing salt
219        let raw_response = serde_json::json!({
220            "kdfSettings": {
221                "kdfType": KdfType::PBKDF2_SHA256.as_i64(),
222                "iterations": mock_default_pbkdf2_iterations().get(),
223            }
224        });
225
226        let mock = Mock::given(matchers::method("POST"))
227            .and(matchers::path("/identity/accounts/prelogin/password"))
228            .respond_with(ResponseTemplate::new(200).set_body_json(raw_response));
229
230        let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
231        let login_client = make_login_client(&mock_server);
232
233        let result = login_client
234            .get_password_prelogin(TEST_EMAIL.to_string())
235            .await;
236
237        assert!(result.is_err());
238        match result.unwrap_err() {
239            PasswordPreloginError::Unknown(err) => {
240                assert_eq!(
241                    err,
242                    "The response received was missing a required field: response.salt"
243                );
244            }
245            other => panic!("Expected MissingField error, got {:?}", other),
246        }
247    }
248
249    #[tokio::test]
250    async fn test_get_password_prelogin_api_error() {
251        // Create a mock 500 error
252        let mock = Mock::given(matchers::method("POST"))
253            .and(matchers::path("/identity/accounts/prelogin/password"))
254            .respond_with(ResponseTemplate::new(500));
255
256        let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
257        let login_client = make_login_client(&mock_server);
258
259        let result = login_client
260            .get_password_prelogin(TEST_EMAIL.to_string())
261            .await;
262
263        assert!(result.is_err());
264        match result.unwrap_err() {
265            PasswordPreloginError::Api(bitwarden_core::ApiError::ResponseContent {
266                status,
267                message: _,
268            }) => {
269                assert_eq!(status, reqwest::StatusCode::INTERNAL_SERVER_ERROR);
270            }
271            other => panic!("Expected Api ResponseContent error, got {:?}", other),
272        }
273    }
274}