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);
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 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 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 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 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 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}