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