1use serde_json::json;
8use zeroize::Zeroizing;
9
10use super::{
11 account_key::AccountKey,
12 credentials::Credentials,
13 device::{ClientInfo, reauthorize_device, register_device},
14 error::OnePasswordError,
15 mac::MacSigner,
16 opdata::{AesKey, decode64_loose},
17 rest::RestClient,
18 session::Session,
19 srp::{self, SrpInfo},
20 two_factor::{MfaOutcome, TwoFactorUi, perform_second_factor_authentication},
21 wire::{AuthComplete, LoginInfo, MfaInfo, NewSession},
22};
23
24const MAX_DEVICE_ATTEMPTS: u32 = 2;
27const AUTH_METHODS_ENDPOINT: &str = "v2/auth/methods";
28const PASSWORD_METHOD: &str = "PASSWORD+SK";
29const SSO_METHOD: &str = "OIDC";
30pub(super) const AUTH_START_ENDPOINT: &str = "v3/auth/start";
31pub(super) const AUTH_COMPLETE_ENDPOINT: &str = "v2/auth/complete";
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
35#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
36#[bitwarden_ffi::wasm_record]
37pub struct SignInMethods {
38 pub password: bool,
40 pub sso: bool,
42}
43
44impl SignInMethods {
45 pub(super) fn of(login_info: &LoginInfo) -> SignInMethods {
46 let offers = |kind| {
47 login_info
48 .auth_methods
49 .iter()
50 .any(|method| method.kind == kind)
51 };
52 SignInMethods {
53 password: offers(PASSWORD_METHOD),
54 sso: offers(SSO_METHOD),
55 }
56 }
57}
58
59pub(super) enum LoginOutcome {
62 Success(Box<Session>),
64 BadOtp,
66}
67
68pub(super) async fn fetch_auth_methods(
71 username: &str,
72 rest: &RestClient,
73) -> Result<LoginInfo, OnePasswordError> {
74 rest.post_json(AUTH_METHODS_ENDPOINT, json!({ "email": username }))
75 .await
76 .map_err(|error| match error {
77 OnePasswordError::UnexpectedStatus { status: 400, .. } => {
78 OnePasswordError::InvalidEmail
79 }
80 error => error,
81 })
82}
83
84pub(super) async fn login_attempt(
87 credentials: &Credentials,
88 account_key: &AccountKey,
89 client_info: &ClientInfo,
90 attempt: u32,
91 ui: &dyn TwoFactorUi,
92 rest: &RestClient,
93) -> Result<LoginOutcome, OnePasswordError> {
94 let (session_id, srp_info) =
96 start_new_session(credentials, account_key, client_info, rest).await?;
97
98 let session_rest = rest.with_session_id(&session_id)?;
101
102 let srp_x = Zeroizing::new(srp::compute_x(credentials, account_key, &srp_info)?);
104 let session_key = srp::perform_and_verify_with_x(
105 srp_x.as_slice(),
106 &credentials.username,
107 &account_key.uuid,
108 srp_info.salt(),
109 &session_id,
110 &session_rest,
111 )
112 .await?;
113
114 let mac_rest = session_rest.with_signer(MacSigner::new(&session_key));
117
118 let mfa = verify_session_key(client_info, &session_key, &mac_rest).await?;
120
121 if let Some(mfa) = mfa {
123 let outcome = perform_second_factor_authentication(
124 &mfa,
125 client_info,
126 &session_key,
127 attempt,
128 ui,
129 &mac_rest,
130 )
131 .await?;
132
133 match outcome {
134 MfaOutcome::Verified => {}
135 MfaOutcome::BadOtp => return Ok(LoginOutcome::BadOtp),
136 }
137 }
138
139 Ok(LoginOutcome::Success(Box::new(Session::new(
140 session_key,
141 mac_rest,
142 ))))
143}
144
145async fn start_new_session(
148 credentials: &Credentials,
149 account_key: &AccountKey,
150 client_info: &ClientInfo,
151 rest: &RestClient,
152) -> Result<(String, SrpInfo), OnePasswordError> {
153 let mut device_attempts = 0;
154 loop {
155 let response: NewSession = rest
157 .post_json(
158 AUTH_START_ENDPOINT,
159 json!({
160 "email": credentials.username,
161 "skformat": account_key.format,
162 "skid": account_key.uuid,
163 "deviceUuid": client_info.device_uuid,
164 }),
165 )
166 .await?;
167
168 match response.status.as_str() {
171 "ok" => {
174 if response.key_format.as_deref() != Some(account_key.format.as_str())
175 || response.key_uuid.as_deref() != Some(account_key.uuid.as_str())
176 {
177 return Err(OnePasswordError::BadCredentials);
178 }
179
180 let auth = response.auth.ok_or_else(|| {
181 OnePasswordError::Internal(
182 "missing SRP parameters in the start response".into(),
183 )
184 })?;
185 let srp_info = SrpInfo::new(
186 auth.method,
187 auth.algorithm,
188 auth.iterations,
189 decode64_loose(&auth.salt)?,
190 )?;
191 return Ok((response.session_id, srp_info));
192 }
193 status @ ("device-not-registered" | "device-deleted") => {
196 device_attempts += 1;
197 if device_attempts > MAX_DEVICE_ATTEMPTS {
198 return Err(OnePasswordError::Internal(format!(
199 "the server still reports the device as '{status}' after \
200 {MAX_DEVICE_ATTEMPTS} attempts"
201 )));
202 }
203
204 let session_rest = rest.with_session_id(&response.session_id)?;
205 if status == "device-not-registered" {
206 register_device(client_info, &session_rest).await?;
207 } else {
208 reauthorize_device(client_info, &session_rest).await?;
209 }
210 }
211 other => {
212 return Err(OnePasswordError::Internal(format!(
213 "failed to start a new session, unsupported status '{other}'"
214 )));
215 }
216 }
217 }
218}
219
220async fn verify_session_key(
223 client_info: &ClientInfo,
224 session_key: &AesKey,
225 rest: &RestClient,
226) -> Result<Option<MfaInfo>, OnePasswordError> {
227 let params = json!({
228 "client": client_info.client_id(),
229 "device": client_info.device_body(),
230 });
231 let response: AuthComplete = rest
232 .post_encrypted_json(AUTH_COMPLETE_ENDPOINT, params, session_key)
233 .await?;
234 Ok(response.mfa)
235}
236
237#[cfg(test)]
238mod tests {
239 use bitwarden_api_base::new_http_client;
240 use wiremock::{Mock, MockServer, ResponseTemplate, matchers};
241
242 use super::{
243 super::sign_in::{SignInAddress, SignInDomain},
244 *,
245 };
246
247 fn client(server: &MockServer) -> RestClient {
248 RestClient::new(
249 new_http_client(),
250 format!("http://{}/api", server.address()),
251 "client-id",
252 "user-agent",
253 "op-user-agent",
254 )
255 .expect("valid headers")
256 }
257
258 fn credentials() -> Credentials {
259 Credentials {
260 username: "[email protected]".into(),
261 password: "password".into(),
262 account_key: "A3-RTN9SA-DY9445Y5FF96X6E7B5GPFA95R9".into(),
263 sign_in_address: SignInAddress {
264 subdomain: "my".into(),
265 domain: SignInDomain::Global,
266 },
267 }
268 }
269
270 fn account_key() -> AccountKey {
271 AccountKey::parse(&credentials().account_key).expect("valid account key")
272 }
273
274 fn start_response(status: &str) -> serde_json::Value {
275 json!({"status": status, "sessionID": "SESSION"})
276 }
277
278 fn ok_start_response() -> serde_json::Value {
279 json!({
280 "status": "ok",
281 "sessionID": "SESSION",
282 "accountKeyFormat": "A3",
283 "accountKeyUuid": "RTN9SA",
284 "userAuth": {
285 "method": "SRPg-4096",
286 "alg": "PBES2g-HS256",
287 "iterations": 100000,
288 "salt": "c2FsdHNhbHRzYWx0",
289 },
290 })
291 }
292
293 #[tokio::test]
294 async fn fetches_the_auth_methods() {
295 let server = MockServer::start().await;
296 server
297 .register(
298 Mock::given(matchers::path("/api/v2/auth/methods"))
299 .and(matchers::body_json(json!({"email": "[email protected]"})))
300 .respond_with(
301 ResponseTemplate::new(200)
302 .set_body_json(json!({"authMethods": [{"type": "PASSWORD+SK"}]})),
303 )
304 .expect(1),
305 )
306 .await;
307
308 let info = fetch_auth_methods("[email protected]", &client(&server))
309 .await
310 .expect("methods are listed");
311
312 assert_eq!(info.auth_methods[0].kind, "PASSWORD+SK");
313 server.verify().await;
314 }
315
316 #[tokio::test]
317 async fn a_refused_email_is_invalid() {
318 let server = MockServer::start().await;
319 server
320 .register(
321 Mock::given(matchers::path("/api/v2/auth/methods"))
322 .respond_with(ResponseTemplate::new(400).set_body_json(json!({})))
323 .expect(1),
324 )
325 .await;
326
327 let error = fetch_auth_methods("user@example", &client(&server))
328 .await
329 .expect_err("the email is refused");
330
331 assert!(matches!(error, OnePasswordError::InvalidEmail));
332 server.verify().await;
333 }
334
335 #[tokio::test]
336 async fn other_auth_methods_failures_keep_their_status() {
337 let server = MockServer::start().await;
338 server
339 .register(
340 Mock::given(matchers::path("/api/v2/auth/methods"))
341 .respond_with(ResponseTemplate::new(503))
342 .expect(1),
343 )
344 .await;
345
346 let error = fetch_auth_methods("[email protected]", &client(&server))
347 .await
348 .expect_err("the server is down");
349
350 assert!(matches!(
351 error,
352 OnePasswordError::UnexpectedStatus { status: 503, .. }
353 ));
354 server.verify().await;
355 }
356
357 #[test]
358 fn sign_in_methods_read_the_offered_auth_methods() {
359 let methods = |kinds: &[&str]| {
360 let login_info: LoginInfo = serde_json::from_value(json!({
361 "authMethods": kinds.iter().map(|kind| json!({"type": kind})).collect::<Vec<_>>(),
362 }))
363 .expect("valid login info");
364 SignInMethods::of(&login_info)
365 };
366 let only = |password, sso| SignInMethods { password, sso };
367
368 assert_eq!(methods(&["PASSWORD+SK"]), only(true, false));
369 assert_eq!(methods(&["OIDC"]), only(false, true));
370 assert_eq!(methods(&["PASSWORD+SK", "OIDC"]), only(true, true));
371 assert_eq!(methods(&["PASSKEY"]), only(false, false));
372 }
373
374 #[tokio::test]
375 async fn start_registers_an_unknown_device_then_retries() {
376 let server = MockServer::start().await;
377 server
380 .register(
381 Mock::given(matchers::path("/api/v3/auth/start"))
382 .respond_with(
383 ResponseTemplate::new(200)
384 .set_body_json(start_response("device-not-registered")),
385 )
386 .up_to_n_times(1)
387 .expect(1),
388 )
389 .await;
390 server
391 .register(
392 Mock::given(matchers::path("/api/v1/device"))
393 .and(matchers::method("POST"))
394 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"success": 1})))
395 .expect(1),
396 )
397 .await;
398 server
399 .register(
400 Mock::given(matchers::path("/api/v3/auth/start"))
401 .respond_with(ResponseTemplate::new(200).set_body_json(ok_start_response()))
402 .expect(1),
403 )
404 .await;
405
406 let (session_id, srp_info) = start_new_session(
407 &credentials(),
408 &account_key(),
409 &ClientInfo::for_desktop("device-uuid"),
410 &client(&server),
411 )
412 .await
413 .expect("session starts after registering the device");
414
415 assert_eq!(session_id, "SESSION");
416 assert_eq!(
417 srp_info,
418 SrpInfo::new(
419 "SRPg-4096".into(),
420 "PBES2g-HS256".into(),
421 100000,
422 b"saltsaltsalt".to_vec(),
423 )
424 .expect("supported parameters")
425 );
426 server.verify().await;
427 }
428
429 #[tokio::test]
430 async fn start_gives_up_when_the_device_never_registers() {
431 let server = MockServer::start().await;
432 server
433 .register(
434 Mock::given(matchers::path("/api/v3/auth/start"))
435 .respond_with(
436 ResponseTemplate::new(200)
437 .set_body_json(start_response("device-not-registered")),
438 )
439 .expect(u64::from(MAX_DEVICE_ATTEMPTS) + 1),
440 )
441 .await;
442 server
443 .register(
444 Mock::given(matchers::path("/api/v1/device"))
445 .and(matchers::method("POST"))
446 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"success": 1})))
447 .expect(u64::from(MAX_DEVICE_ATTEMPTS)),
448 )
449 .await;
450
451 let error = start_new_session(
452 &credentials(),
453 &account_key(),
454 &ClientInfo::for_desktop("device-uuid"),
455 &client(&server),
456 )
457 .await
458 .expect_err("gives up instead of registering the device forever");
459
460 assert!(
461 error.to_string().contains("device-not-registered"),
462 "unexpected error: {error}"
463 );
464 server.verify().await;
465 }
466
467 #[tokio::test]
468 async fn start_rejects_a_mismatching_account_key() {
469 let server = MockServer::start().await;
470 server
471 .register(
472 Mock::given(matchers::path("/api/v3/auth/start"))
473 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
474 "status": "ok",
475 "sessionID": "SESSION",
476 "accountKeyFormat": "A3",
477 "accountKeyUuid": "OTHERS",
478 })))
479 .expect(1),
480 )
481 .await;
482
483 let error = start_new_session(
484 &credentials(),
485 &account_key(),
486 &ClientInfo::for_desktop("device-uuid"),
487 &client(&server),
488 )
489 .await
490 .expect_err("the server knows a different Secret Key");
491
492 assert!(matches!(error, OnePasswordError::BadCredentials));
493 server.verify().await;
494 }
495
496 #[tokio::test]
497 async fn start_reports_an_unknown_status() {
498 let server = MockServer::start().await;
499 server
500 .register(
501 Mock::given(matchers::path("/api/v3/auth/start"))
502 .respond_with(
503 ResponseTemplate::new(200).set_body_json(start_response("who-knows")),
504 )
505 .expect(1),
506 )
507 .await;
508
509 let error = start_new_session(
510 &credentials(),
511 &account_key(),
512 &ClientInfo::for_desktop("device-uuid"),
513 &client(&server),
514 )
515 .await
516 .expect_err("unknown status");
517
518 assert!(error.to_string().contains("who-knows"));
519 server.verify().await;
520 }
521}