@@ -129,7 +129,7 @@ async fn login_sync_cache_round_trip() {
129129 let prelogin = client. prelogin ( EMAIL ) . await . unwrap ( ) ;
130130 let params = prelogin. into_kdf_params ( ) . unwrap ( ) ;
131131 let token = client
132- . login_password ( EMAIL , PASSWORD . as_bytes ( ) , params)
132+ . login_password ( EMAIL , PASSWORD . as_bytes ( ) , params, None )
133133 . await
134134 . unwrap ( ) ;
135135 assert_eq ! ( token. access_token, "test-access-token" ) ;
@@ -180,7 +180,7 @@ async fn wrong_password_surfaces_server_status() {
180180
181181 let mut client = BitwardenClient :: new ( urls, Uuid :: new_v4 ( ) , "vault-test" ) . unwrap ( ) ;
182182 let err = client
183- . login_password ( EMAIL , b"wrong" , pbkdf2_params ( ) )
183+ . login_password ( EMAIL , b"wrong" , pbkdf2_params ( ) , None )
184184 . await
185185 . unwrap_err ( ) ;
186186 assert ! ( matches!(
@@ -220,7 +220,7 @@ async fn delete_cipher_sends_authorized_delete() {
220220 let mut client = BitwardenClient :: new ( urls, Uuid :: new_v4 ( ) , "vault-test" ) . unwrap ( ) ;
221221 // Prime token via login_password (uses the token mock above).
222222 client
223- . login_password ( EMAIL , PASSWORD . as_bytes ( ) , pbkdf2_params ( ) )
223+ . login_password ( EMAIL , PASSWORD . as_bytes ( ) , pbkdf2_params ( ) , None )
224224 . await
225225 . unwrap ( ) ;
226226
@@ -286,7 +286,7 @@ async fn create_and_update_cipher_send_authorized_requests() {
286286
287287 let mut client = BitwardenClient :: new ( urls, Uuid :: new_v4 ( ) , "vault-test" ) . unwrap ( ) ;
288288 client
289- . login_password ( EMAIL , PASSWORD . as_bytes ( ) , pbkdf2_params ( ) )
289+ . login_password ( EMAIL , PASSWORD . as_bytes ( ) , pbkdf2_params ( ) , None )
290290 . await
291291 . unwrap ( ) ;
292292
@@ -343,7 +343,7 @@ async fn create_secure_note_carries_securenote_marker() {
343343
344344 let mut client = BitwardenClient :: new ( urls, Uuid :: new_v4 ( ) , "vault-test" ) . unwrap ( ) ;
345345 client
346- . login_password ( EMAIL , PASSWORD . as_bytes ( ) , pbkdf2_params ( ) )
346+ . login_password ( EMAIL , PASSWORD . as_bytes ( ) , pbkdf2_params ( ) , None )
347347 . await
348348 . unwrap ( ) ;
349349
@@ -407,6 +407,63 @@ async fn login_api_key_bad_key_surfaces_server_status() {
407407 ) ) ;
408408}
409409
410+ #[ tokio:: test]
411+ #[ ignore = "links ring into a test binary; see file preamble" ]
412+ async fn login_password_two_factor_challenge_then_resubmit ( ) {
413+ let server = MockServer :: start ( ) . await ;
414+ let urls = BaseUrls :: self_hosted ( & server. uri ( ) ) . unwrap ( ) ;
415+
416+ // First attempt → 400 with the 2FA-required shape. `up_to_n_times(1)` so it
417+ // matches only the first request; the coded resubmit falls through to the
418+ // success mock below.
419+ Mock :: given ( method ( "POST" ) )
420+ . and ( path ( "/identity/connect/token" ) )
421+ . and ( body_string_contains ( "grant_type=password" ) )
422+ . respond_with ( ResponseTemplate :: new ( 400 ) . set_body_json ( json ! ( {
423+ "TwoFactorProviders" : [ 0 ] ,
424+ "TwoFactorProviders2" : { "0" : { } }
425+ } ) ) )
426+ . up_to_n_times ( 1 )
427+ . mount ( & server)
428+ . await ;
429+
430+ // Resubmit carrying the authenticator code → 200 + token.
431+ Mock :: given ( method ( "POST" ) )
432+ . and ( path ( "/identity/connect/token" ) )
433+ . and ( body_string_contains ( "twoFactorToken=123456" ) )
434+ . and ( body_string_contains ( "twoFactorProvider=0" ) )
435+ . respond_with ( ResponseTemplate :: new ( 200 ) . set_body_json ( json ! ( {
436+ "access_token" : "tfa-token" ,
437+ "expires_in" : 3600 ,
438+ "token_type" : "Bearer" ,
439+ "Key" : "2.dGVzdA==|dGVzdA==|dGVzdA==" ,
440+ } ) ) )
441+ . mount ( & server)
442+ . await ;
443+
444+ let mut client = BitwardenClient :: new ( urls, Uuid :: new_v4 ( ) , "vault-test" ) . unwrap ( ) ;
445+ let err = client
446+ . login_password ( EMAIL , PASSWORD . as_bytes ( ) , pbkdf2_params ( ) , None )
447+ . await
448+ . unwrap_err ( ) ;
449+ assert ! (
450+ matches!( err, vault_api:: Error :: TwoFactorRequired ( _) ) ,
451+ "first attempt must 2FA-challenge: {err:?}"
452+ ) ;
453+
454+ let tf = vault_api:: TwoFactor {
455+ provider : 0 ,
456+ token : "123456" . to_owned ( ) ,
457+ remember : false ,
458+ } ;
459+ let token = client
460+ . login_password ( EMAIL , PASSWORD . as_bytes ( ) , pbkdf2_params ( ) , Some ( & tf) )
461+ . await
462+ . unwrap ( ) ;
463+ assert_eq ! ( token. access_token, "tfa-token" ) ;
464+ assert ! ( client. is_authenticated( ) ) ;
465+ }
466+
410467fn urlencoded ( s : & str ) -> String {
411468 // wiremock body_string_contains needs the literal bytes that appear in
412469 // the form-encoded request body. reqwest's serde_urlencoded percent-
0 commit comments