6
6
using System . Diagnostics . CodeAnalysis ;
7
7
using System . Linq ;
8
8
using System . Security . Claims ;
9
+ using System . Text ;
9
10
using System . Threading . Tasks ;
10
11
using Microsoft . AspNetCore . Authentication ;
11
12
using Microsoft . AspNetCore . Http ;
@@ -391,7 +392,14 @@ public virtual async Task<SignInResult> CheckPasswordSignInAsync(TUser user, str
391
392
// Only reset the lockout when not in quirks mode if either TFA is not enabled or the client is remembered for TFA.
392
393
if ( alwaysLockout || ! await IsTfaEnabled ( user ) || await IsTwoFactorClientRememberedAsync ( user ) )
393
394
{
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
+ }
395
403
}
396
404
397
405
return SignInResult . Success ;
@@ -401,7 +409,13 @@ public virtual async Task<SignInResult> CheckPasswordSignInAsync(TUser user, str
401
409
if ( UserManager . SupportsUserLockout && lockoutOnFailure )
402
410
{
403
411
// 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
+
405
419
if ( await UserManager . IsLockedOutAsync ( user ) )
406
420
{
407
421
return await LockedOut ( user ) ;
@@ -470,18 +484,23 @@ public virtual async Task<SignInResult> TwoFactorRecoveryCodeSignInAsync(string
470
484
var result = await UserManager . RedeemTwoFactorRecoveryCodeAsync ( user , recoveryCode ) ;
471
485
if ( result . Succeeded )
472
486
{
473
- await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent : false , rememberClient : false ) ;
474
- return SignInResult . Success ;
487
+ return await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent : false , rememberClient : false ) ;
475
488
}
476
489
477
490
// We don't protect against brute force attacks since codes are expected to be random.
478
491
return SignInResult . Failed ;
479
492
}
480
493
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 )
482
495
{
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
+ }
485
504
486
505
var claims = new List < Claim > ( ) ;
487
506
claims . Add ( new Claim ( "amr" , "mfa" ) ) ;
@@ -499,6 +518,7 @@ private async Task DoTwoFactorSignInAsync(TUser user, TwoFactorAuthenticationInf
499
518
await RememberTwoFactorClientAsync ( user ) ;
500
519
}
501
520
await SignInWithClaimsAsync ( user , isPersistent , claims ) ;
521
+ return SignInResult . Success ;
502
522
}
503
523
504
524
/// <summary>
@@ -531,11 +551,16 @@ public virtual async Task<SignInResult> TwoFactorAuthenticatorSignInAsync(string
531
551
532
552
if ( await UserManager . VerifyTwoFactorTokenAsync ( user , Options . Tokens . AuthenticatorTokenProvider , code ) )
533
553
{
534
- await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent , rememberClient ) ;
535
- return SignInResult . Success ;
554
+ return await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent , rememberClient ) ;
536
555
}
537
556
// 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
+ }
539
564
return SignInResult . Failed ;
540
565
}
541
566
@@ -569,11 +594,16 @@ public virtual async Task<SignInResult> TwoFactorSignInAsync(string provider, st
569
594
}
570
595
if ( await UserManager . VerifyTwoFactorTokenAsync ( user , provider , code ) )
571
596
{
572
- await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent , rememberClient ) ;
573
- return SignInResult . Success ;
597
+ return await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent , rememberClient ) ;
574
598
}
575
599
// 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
+ }
577
607
return SignInResult . Failed ;
578
608
}
579
609
@@ -864,13 +894,77 @@ protected virtual async Task<SignInResult> PreSignInCheck(TUser user)
864
894
/// </summary>
865
895
/// <param name="user">The user</param>
866
896
/// <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 )
868
898
{
869
899
if ( UserManager . SupportsUserLockout )
870
900
{
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
+ }
872
967
}
873
- return Task . CompletedTask ;
874
968
}
875
969
876
970
internal class TwoFactorAuthenticationInfo
0 commit comments