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