66using System . Diagnostics . CodeAnalysis ;
77using System . Linq ;
88using System . Security . Claims ;
9+ using System . Text ;
910using System . Threading . Tasks ;
1011using Microsoft . AspNetCore . Authentication ;
1112using Microsoft . AspNetCore . Http ;
@@ -391,7 +392,14 @@ public virtual async Task<SignInResult> CheckPasswordSignInAsync(TUser user, str
391392 // Only reset the lockout when not in quirks mode if either TFA is not enabled or the client is remembered for TFA.
392393 if ( alwaysLockout || ! await IsTfaEnabled ( user ) || await IsTwoFactorClientRememberedAsync ( user ) )
393394 {
394- await ResetLockout ( user ) ;
395+ var resetLockoutResult = await ResetLockoutWithResult ( user ) ;
396+ if ( ! resetLockoutResult . Succeeded )
397+ {
398+ // ResetLockout got an unsuccessful result that could be caused by concurrency failures indicating an
399+ // attacker could be trying to bypass the MaxFailedAccessAttempts limit. Return the same failure we do
400+ // when failing to increment the lockout to avoid giving an attacker extra guesses at the password.
401+ return SignInResult . Failed ;
402+ }
395403 }
396404
397405 return SignInResult . Success ;
@@ -401,7 +409,13 @@ public virtual async Task<SignInResult> CheckPasswordSignInAsync(TUser user, str
401409 if ( UserManager . SupportsUserLockout && lockoutOnFailure )
402410 {
403411 // If lockout is requested, increment access failed count which might lock out the user
404- await UserManager . AccessFailedAsync ( user ) ;
412+ var incrementLockoutResult = await UserManager . AccessFailedAsync ( user ) ?? IdentityResult . Success ;
413+ if ( ! incrementLockoutResult . Succeeded )
414+ {
415+ // Return the same failure we do when resetting the lockout fails after a correct password.
416+ return SignInResult . Failed ;
417+ }
418+
405419 if ( await UserManager . IsLockedOutAsync ( user ) )
406420 {
407421 return await LockedOut ( user ) ;
@@ -470,18 +484,23 @@ public virtual async Task<SignInResult> TwoFactorRecoveryCodeSignInAsync(string
470484 var result = await UserManager . RedeemTwoFactorRecoveryCodeAsync ( user , recoveryCode ) ;
471485 if ( result . Succeeded )
472486 {
473- await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent : false , rememberClient : false ) ;
474- return SignInResult . Success ;
487+ return await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent : false , rememberClient : false ) ;
475488 }
476489
477490 // We don't protect against brute force attacks since codes are expected to be random.
478491 return SignInResult . Failed ;
479492 }
480493
481- private async Task DoTwoFactorSignInAsync ( TUser user , TwoFactorAuthenticationInfo twoFactorInfo , bool isPersistent , bool rememberClient )
494+ private async Task < SignInResult > DoTwoFactorSignInAsync ( TUser user , TwoFactorAuthenticationInfo twoFactorInfo , bool isPersistent , bool rememberClient )
482495 {
483- // When token is verified correctly, clear the access failed count used for lockout
484- await ResetLockout ( user ) ;
496+ var resetLockoutResult = await ResetLockoutWithResult ( user ) ;
497+ if ( ! resetLockoutResult . Succeeded )
498+ {
499+ // ResetLockout got an unsuccessful result that could be caused by concurrency failures indicating an
500+ // attacker could be trying to bypass the MaxFailedAccessAttempts limit. Return the same failure we do
501+ // when failing to increment the lockout to avoid giving an attacker extra guesses at the two factor code.
502+ return SignInResult . Failed ;
503+ }
485504
486505 var claims = new List < Claim > ( ) ;
487506 claims . Add ( new Claim ( "amr" , "mfa" ) ) ;
@@ -499,6 +518,7 @@ private async Task DoTwoFactorSignInAsync(TUser user, TwoFactorAuthenticationInf
499518 await RememberTwoFactorClientAsync ( user ) ;
500519 }
501520 await SignInWithClaimsAsync ( user , isPersistent , claims ) ;
521+ return SignInResult . Success ;
502522 }
503523
504524 /// <summary>
@@ -531,11 +551,16 @@ public virtual async Task<SignInResult> TwoFactorAuthenticatorSignInAsync(string
531551
532552 if ( await UserManager . VerifyTwoFactorTokenAsync ( user , Options . Tokens . AuthenticatorTokenProvider , code ) )
533553 {
534- await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent , rememberClient ) ;
535- return SignInResult . Success ;
554+ return await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent , rememberClient ) ;
536555 }
537556 // If the token is incorrect, record the failure which also may cause the user to be locked out
538- await UserManager . AccessFailedAsync ( user ) ;
557+ var incrementLockoutResult = await UserManager . AccessFailedAsync ( user ) ?? IdentityResult . Success ;
558+ if ( ! incrementLockoutResult . Succeeded )
559+ {
560+ // Return the same failure we do when resetting the lockout fails after a correct two factor code.
561+ // This is currently redundant, but it's here in case the code gets copied elsewhere.
562+ return SignInResult . Failed ;
563+ }
539564 return SignInResult . Failed ;
540565 }
541566
@@ -569,11 +594,16 @@ 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
576- await UserManager . AccessFailedAsync ( user ) ;
600+ var incrementLockoutResult = await UserManager . AccessFailedAsync ( user ) ?? IdentityResult . Success ;
601+ if ( ! incrementLockoutResult . Succeeded )
602+ {
603+ // Return the same failure we do when resetting the lockout fails after a correct two factor code.
604+ // This is currently redundant, but it's here in case the code gets copied elsewhere.
605+ return SignInResult . Failed ;
606+ }
577607 return SignInResult . Failed ;
578608 }
579609
@@ -864,13 +894,77 @@ protected virtual async Task<SignInResult> PreSignInCheck(TUser user)
864894 /// </summary>
865895 /// <param name="user">The user</param>
866896 /// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/> of the operation.</returns>
867- protected virtual Task ResetLockout ( TUser user )
897+ protected virtual async Task ResetLockout ( TUser user )
868898 {
869899 if ( UserManager . SupportsUserLockout )
870900 {
871- return UserManager . ResetAccessFailedCountAsync ( user ) ;
901+ // The IdentityResult should not be null according to the annotations, but our own tests return null and I'm trying to limit breakages.
902+ var result = await UserManager . ResetAccessFailedCountAsync ( user ) ?? IdentityResult . Success ;
903+
904+ if ( ! result . Succeeded )
905+ {
906+ throw new IdentityResultException ( result ) ;
907+ }
908+ }
909+ }
910+
911+ private async Task < IdentityResult > ResetLockoutWithResult ( TUser user )
912+ {
913+ // Avoid relying on throwing an exception if we're not in a derived class.
914+ if ( GetType ( ) == typeof ( SignInManager < TUser > ) )
915+ {
916+ if ( ! UserManager . SupportsUserLockout )
917+ {
918+ return IdentityResult . Success ;
919+ }
920+
921+ return await UserManager . ResetAccessFailedCountAsync ( user ) ?? IdentityResult . Success ;
922+ }
923+
924+ try
925+ {
926+ var resetLockoutTask = ResetLockout ( user ) ;
927+
928+ if ( resetLockoutTask is Task < IdentityResult > resultTask )
929+ {
930+ return await resultTask ?? IdentityResult . Success ;
931+ }
932+
933+ await resetLockoutTask ;
934+ return IdentityResult . Success ;
935+ }
936+ catch ( IdentityResultException ex )
937+ {
938+ return ex . IdentityResult ;
939+ }
940+ }
941+
942+ private sealed class IdentityResultException : Exception
943+ {
944+ internal IdentityResultException ( IdentityResult result ) : base ( )
945+ {
946+ IdentityResult = result ;
947+ }
948+
949+ internal IdentityResult IdentityResult { get ; set ; }
950+
951+ public override string Message
952+ {
953+ get
954+ {
955+ var sb = new StringBuilder ( "ResetLockout failed." ) ;
956+
957+ foreach ( var error in IdentityResult . Errors )
958+ {
959+ sb . AppendLine ( ) ;
960+ sb . Append ( error . Code ) ;
961+ sb . Append ( ": " ) ;
962+ sb . Append ( error . Description ) ;
963+ }
964+
965+ return sb . ToString ( ) ;
966+ }
872967 }
873- return Task . CompletedTask ;
874968 }
875969
876970 internal class TwoFactorAuthenticationInfo
0 commit comments