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