bitwarden_auth/login/login_via_password/
password_prelogin.rs1use 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#[bitwarden_error(flat)]
12#[derive(Debug, Error)]
13pub enum PasswordPreloginError {
14 #[error(transparent)]
16 Api(#[from] ApiError),
17
18 #[error("Unknown password prelogin error: {0}")]
22 Unknown(String),
23}
24
25impl 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 pub async fn get_password_prelogin(
39 &self,
40 email: String,
41 ) -> Result<PasswordPreloginResponse, PasswordPreloginError> {
42 let request_model = PasswordPreloginRequestModel::new(email.clone());
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((
52 response,
53 email.as_str(),
54 ))?)
55 }
56}
57
58#[cfg(test)]
59mod tests {
60 use std::num::NonZeroU32;
61
62 use bitwarden_api_api::ResponseContent;
63 use bitwarden_api_identity::models::KdfType;
64 use bitwarden_core::{ClientSettings, DeviceType};
65 use bitwarden_crypto::Kdf;
66 use bitwarden_test::start_api_mock;
67 use wiremock::{Mock, ResponseTemplate, matchers};
68
69 use super::*;
70
71 const TEST_EMAIL: &str = "[email protected]";
72 const TEST_SALT_PBKDF2: &str = "test-salt-value";
73 const TEST_SALT_ARGON2: &str = "argon2-salt-value";
74
75 fn make_login_client(mock_server: &wiremock::MockServer) -> LoginClient {
76 let settings = ClientSettings {
77 identity_url: format!("http://{}/identity", mock_server.address()),
78 api_url: format!("http://{}/api", mock_server.address()),
79 user_agent: "Bitwarden Rust-SDK [TEST]".into(),
80 device_type: DeviceType::SDK,
81 device_identifier: None,
82 bitwarden_client_version: None,
83 bitwarden_package_type: None,
84 };
85 LoginClient::new(settings)
86 }
87
88 fn mock_default_pbkdf2_iterations() -> NonZeroU32 {
89 let Kdf::PBKDF2 { iterations } = Kdf::default_pbkdf2() else {
90 panic!("Expected PBKDF2 KDF");
91 };
92 iterations
93 }
94
95 fn mock_default_argon2_params() -> (NonZeroU32, NonZeroU32, NonZeroU32) {
96 let Kdf::Argon2id {
97 iterations,
98 memory,
99 parallelism,
100 } = Kdf::default_argon2()
101 else {
102 panic!("Expected Argon2 KDF");
103 };
104 (iterations, memory, parallelism)
105 }
106
107 #[tokio::test]
108 async fn test_get_password_prelogin_pbkdf2_success() {
109 let raw_success = serde_json::json!({
111 "kdfSettings": {
112 "kdfType": KdfType::PBKDF2_SHA256.as_i64(),
113 "iterations": mock_default_pbkdf2_iterations().get()
114 },
115 "salt": TEST_SALT_PBKDF2
116 });
117
118 let mock = Mock::given(matchers::method("POST"))
119 .and(matchers::path("identity/accounts/prelogin/password"))
120 .and(matchers::header(
121 reqwest::header::CONTENT_TYPE.as_str(),
122 "application/json",
123 ))
124 .respond_with(ResponseTemplate::new(200).set_body_json(raw_success));
125
126 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
127 let login_client = make_login_client(&mock_server);
128
129 let result = login_client
130 .get_password_prelogin(TEST_EMAIL.to_string())
131 .await
132 .unwrap();
133
134 assert_eq!(result.salt, TEST_SALT_PBKDF2);
135 match result.kdf {
136 Kdf::PBKDF2 { iterations } => {
137 assert_eq!(iterations, mock_default_pbkdf2_iterations());
138 }
139 _ => panic!("Expected PBKDF2 KDF type"),
140 }
141 }
142
143 #[tokio::test]
144 async fn test_get_password_prelogin_argon2id_success() {
145 let (default_iterations, default_memory, default_parallelism) =
146 mock_default_argon2_params();
147
148 let raw_success = serde_json::json!({
150 "kdfSettings": {
151 "kdfType": KdfType::Argon2id.as_i64(),
152 "iterations": default_iterations.get(),
153 "memory": default_memory.get(),
154 "parallelism": default_parallelism.get(),
155 },
156 "salt": TEST_SALT_ARGON2
157 });
158
159 let mock = Mock::given(matchers::method("POST"))
160 .and(matchers::path("identity/accounts/prelogin/password"))
161 .and(matchers::header(
162 reqwest::header::CONTENT_TYPE.as_str(),
163 "application/json",
164 ))
165 .respond_with(ResponseTemplate::new(200).set_body_json(raw_success));
166
167 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
168 let login_client = make_login_client(&mock_server);
169
170 let result = login_client
171 .get_password_prelogin(TEST_EMAIL.to_string())
172 .await
173 .unwrap();
174
175 assert_eq!(result.salt, TEST_SALT_ARGON2);
176 match result.kdf {
177 Kdf::Argon2id {
178 iterations,
179 memory,
180 parallelism,
181 } => {
182 assert_eq!(iterations, default_iterations);
183 assert_eq!(memory, default_memory);
184 assert_eq!(parallelism, default_parallelism);
185 }
186 _ => panic!("Expected Argon2id KDF type"),
187 }
188 }
189
190 #[tokio::test]
191 async fn test_get_password_prelogin_missing_kdf_settings() {
192 let raw_response = serde_json::json!({
194 "salt": TEST_SALT_PBKDF2
195 });
196
197 let mock = Mock::given(matchers::method("POST"))
198 .and(matchers::path("identity/accounts/prelogin/password"))
199 .respond_with(ResponseTemplate::new(200).set_body_json(raw_response));
200
201 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
202 let login_client = make_login_client(&mock_server);
203
204 let result = login_client
205 .get_password_prelogin(TEST_EMAIL.to_string())
206 .await;
207
208 assert!(result.is_err());
209 match result.unwrap_err() {
210 PasswordPreloginError::Unknown(err) => {
211 assert_eq!(
212 err,
213 "The response received was missing a required field: response.kdf_settings"
214 );
215 }
216 other => panic!("Expected MissingField error, got {:?}", other),
217 }
218 }
219
220 #[tokio::test]
221 async fn test_get_password_prelogin_missing_salt() {
222 let raw_response = serde_json::json!({
224 "kdfSettings": {
225 "kdfType": KdfType::PBKDF2_SHA256.as_i64(),
226 "iterations": mock_default_pbkdf2_iterations().get(),
227 }
228 });
229
230 let mock = Mock::given(matchers::method("POST"))
231 .and(matchers::path("/identity/accounts/prelogin/password"))
232 .respond_with(ResponseTemplate::new(200).set_body_json(raw_response));
233
234 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
235 let login_client = make_login_client(&mock_server);
236
237 let result = login_client
238 .get_password_prelogin(TEST_EMAIL.to_string())
239 .await;
240
241 assert_eq!(result.unwrap().salt, TEST_EMAIL.to_string());
242 }
243
244 #[tokio::test]
245 async fn test_get_password_prelogin_email_fallback_case_insensitive() {
246 let raw_response = serde_json::json!({
248 "kdfSettings": {
249 "kdfType": KdfType::PBKDF2_SHA256.as_i64(),
250 "iterations": mock_default_pbkdf2_iterations().get(),
251 }
252 });
253
254 let case_variant_email = "[email protected]";
255
256 let mock = Mock::given(matchers::method("POST"))
257 .and(matchers::path("/identity/accounts/prelogin/password"))
258 .respond_with(ResponseTemplate::new(200).set_body_json(raw_response));
259
260 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
261 let login_client = make_login_client(&mock_server);
262
263 let result = login_client
264 .get_password_prelogin(case_variant_email.to_string())
265 .await;
266
267 assert_eq!(result.unwrap().salt, TEST_EMAIL.to_string());
268 }
269
270 #[tokio::test]
271 async fn test_get_password_prelogin_api_error() {
272 let mock = Mock::given(matchers::method("POST"))
274 .and(matchers::path("/identity/accounts/prelogin/password"))
275 .respond_with(ResponseTemplate::new(500));
276
277 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
278 let login_client = make_login_client(&mock_server);
279
280 let result = login_client
281 .get_password_prelogin(TEST_EMAIL.to_string())
282 .await;
283
284 assert!(result.is_err());
285 match result.unwrap_err() {
286 PasswordPreloginError::Api(bitwarden_core::ApiError::Response(ResponseContent {
287 status,
288 message: _,
289 })) => {
290 assert_eq!(status, reqwest::StatusCode::INTERNAL_SERVER_ERROR);
291 }
292 other => panic!("Expected Api Response error, got {:?}", other),
293 }
294 }
295}