44using System . Diagnostics . CodeAnalysis ;
55using System . Linq ;
66using System . Security . Claims ;
7+ using System . Text ;
78using Microsoft . AspNetCore . Authentication ;
89using Microsoft . AspNetCore . Http ;
910using Microsoft . Extensions . Logging ;
@@ -388,7 +389,14 @@ public virtual async Task<SignInResult> CheckPasswordSignInAsync(TUser user, str
388389 // Only reset the lockout when not in quirks mode if either TFA is not enabled or the client is remembered for TFA.
389390 if ( alwaysLockout || ! await IsTfaEnabled ( user ) || await IsTwoFactorClientRememberedAsync ( user ) )
390391 {
391- await ResetLockout ( user ) ;
392+ var resetLockoutResult = await ResetLockoutWithResult ( user ) ;
393+ if ( ! resetLockoutResult . Succeeded )
394+ {
395+ // ResetLockout got an unsuccessful result that could be caused by concurrency failures indicating an
396+ // attacker could be trying to bypass the MaxFailedAccessAttempts limit. Return the same failure we do
397+ // when failing to increment the lockout to avoid giving an attacker extra guesses at the password.
398+ return SignInResult . Failed ;
399+ }
392400 }
393401
394402 return SignInResult . Success ;
@@ -398,7 +406,13 @@ public virtual async Task<SignInResult> CheckPasswordSignInAsync(TUser user, str
398406 if ( UserManager . SupportsUserLockout && lockoutOnFailure )
399407 {
400408 // If lockout is requested, increment access failed count which might lock out the user
401- await UserManager . AccessFailedAsync ( user ) ;
409+ var incrementLockoutResult = await UserManager . AccessFailedAsync ( user ) ?? IdentityResult . Success ;
410+ if ( ! incrementLockoutResult . Succeeded )
411+ {
412+ // Return the same failure we do when resetting the lockout fails after a correct password.
413+ return SignInResult . Failed ;
414+ }
415+
402416 if ( await UserManager . IsLockedOutAsync ( user ) )
403417 {
404418 return await LockedOut ( user ) ;
@@ -467,18 +481,23 @@ public virtual async Task<SignInResult> TwoFactorRecoveryCodeSignInAsync(string
467481 var result = await UserManager . RedeemTwoFactorRecoveryCodeAsync ( user , recoveryCode ) ;
468482 if ( result . Succeeded )
469483 {
470- await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent : false , rememberClient : false ) ;
471- return SignInResult . Success ;
484+ return await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent : false , rememberClient : false ) ;
472485 }
473486
474487 // We don't protect against brute force attacks since codes are expected to be random.
475488 return SignInResult . Failed ;
476489 }
477490
478- private async Task DoTwoFactorSignInAsync ( TUser user , TwoFactorAuthenticationInfo twoFactorInfo , bool isPersistent , bool rememberClient )
491+ private async Task < SignInResult > DoTwoFactorSignInAsync ( TUser user , TwoFactorAuthenticationInfo twoFactorInfo , bool isPersistent , bool rememberClient )
479492 {
480- // When token is verified correctly, clear the access failed count used for lockout
481- await ResetLockout ( user ) ;
493+ var resetLockoutResult = await ResetLockoutWithResult ( user ) ;
494+ if ( ! resetLockoutResult . Succeeded )
495+ {
496+ // ResetLockout got an unsuccessful result that could be caused by concurrency failures indicating an
497+ // attacker could be trying to bypass the MaxFailedAccessAttempts limit. Return the same failure we do
498+ // when failing to increment the lockout to avoid giving an attacker extra guesses at the two factor code.
499+ return SignInResult . Failed ;
500+ }
482501
483502 var claims = new List < Claim > ( ) ;
484503 claims . Add ( new Claim ( "amr" , "mfa" ) ) ;
@@ -496,6 +515,7 @@ private async Task DoTwoFactorSignInAsync(TUser user, TwoFactorAuthenticationInf
496515 await RememberTwoFactorClientAsync ( user ) ;
497516 }
498517 await SignInWithClaimsAsync ( user , isPersistent , claims ) ;
518+ return SignInResult . Success ;
499519 }
500520
501521 /// <summary>
@@ -528,13 +548,18 @@ public virtual async Task<SignInResult> TwoFactorAuthenticatorSignInAsync(string
528548
529549 if ( await UserManager . VerifyTwoFactorTokenAsync ( user , Options . Tokens . AuthenticatorTokenProvider , code ) )
530550 {
531- await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent , rememberClient ) ;
532- return SignInResult . Success ;
551+ return await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent , rememberClient ) ;
533552 }
534553 // If the token is incorrect, record the failure which also may cause the user to be locked out
535554 if ( UserManager . SupportsUserLockout )
536555 {
537- await UserManager . AccessFailedAsync ( user ) ;
556+ var incrementLockoutResult = await UserManager . AccessFailedAsync ( user ) ?? IdentityResult . Success ;
557+ if ( ! incrementLockoutResult . Succeeded )
558+ {
559+ // Return the same failure we do when resetting the lockout fails after a correct two factor code.
560+ // This is currently redundant, but it's here in case the code gets copied elsewhere.
561+ return SignInResult . Failed ;
562+ }
538563 }
539564 return SignInResult . Failed ;
540565 }
@@ -569,13 +594,18 @@ public virtual async Task<SignInResult> TwoFactorSignInAsync(string provider, st
569594 }
570595 if ( await UserManager . VerifyTwoFactorTokenAsync ( user , provider , code ) )
571596 {
572- await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent , rememberClient ) ;
573- return SignInResult . Success ;
597+ return await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent , rememberClient ) ;
574598 }
575599 // If the token is incorrect, record the failure which also may cause the user to be locked out
576600 if ( UserManager . SupportsUserLockout )
577601 {
578- await UserManager . AccessFailedAsync ( user ) ;
602+ var incrementLockoutResult = await UserManager . AccessFailedAsync ( user ) ?? IdentityResult . Success ;
603+ if ( ! incrementLockoutResult . Succeeded )
604+ {
605+ // Return the same failure we do when resetting the lockout fails after a correct two factor code.
606+ // This is currently redundant, but it's here in case the code gets copied elsewhere.
607+ return SignInResult . Failed ;
608+ }
579609 }
580610 return SignInResult . Failed ;
581611 }
@@ -867,13 +897,77 @@ protected virtual Task<SignInResult> LockedOut(TUser user)
867897 /// </summary>
868898 /// <param name="user">The user</param>
869899 /// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/> of the operation.</returns>
870- protected virtual Task ResetLockout ( TUser user )
900+ protected virtual async Task ResetLockout ( TUser user )
871901 {
872902 if ( UserManager . SupportsUserLockout )
873903 {
874- return UserManager . ResetAccessFailedCountAsync ( user ) ;
904+ // The IdentityResult should not be null according to the annotations, but our own tests return null and I'm trying to limit breakages.
905+ var result = await UserManager . ResetAccessFailedCountAsync ( user ) ?? IdentityResult . Success ;
906+
907+ if ( ! result . Succeeded )
908+ {
909+ throw new IdentityResultException ( result ) ;
910+ }
911+ }
912+ }
913+
914+ private async Task < IdentityResult > ResetLockoutWithResult ( TUser user )
915+ {
916+ // Avoid relying on throwing an exception if we're not in a derived class.
917+ if ( GetType ( ) == typeof ( SignInManager < TUser > ) )
918+ {
919+ if ( ! UserManager . SupportsUserLockout )
920+ {
921+ return IdentityResult . Success ;
922+ }
923+
924+ return await UserManager . ResetAccessFailedCountAsync ( user ) ?? IdentityResult . Success ;
925+ }
926+
927+ try
928+ {
929+ var resetLockoutTask = ResetLockout ( user ) ;
930+
931+ if ( resetLockoutTask is Task < IdentityResult > resultTask )
932+ {
933+ return await resultTask ?? IdentityResult . Success ;
934+ }
935+
936+ await resetLockoutTask ;
937+ return IdentityResult . Success ;
938+ }
939+ catch ( IdentityResultException ex )
940+ {
941+ return ex . IdentityResult ;
942+ }
943+ }
944+
945+ private sealed class IdentityResultException : Exception
946+ {
947+ internal IdentityResultException ( IdentityResult result ) : base ( )
948+ {
949+ IdentityResult = result ;
950+ }
951+
952+ internal IdentityResult IdentityResult { get ; set ; }
953+
954+ public override string Message
955+ {
956+ get
957+ {
958+ var sb = new StringBuilder ( "ResetLockout failed." ) ;
959+
960+ foreach ( var error in IdentityResult . Errors )
961+ {
962+ sb . AppendLine ( ) ;
963+ sb . Append ( error . Code ) ;
964+ sb . Append ( ": " ) ;
965+ sb . Append ( error . Description ) ;
966+ }
967+
968+ return sb . ToString ( ) ;
969+ }
875970 }
876- return Task . CompletedTask ;
877971 }
878972
879973 internal sealed class TwoFactorAuthenticationInfo
0 commit comments