4
4
using System . Diagnostics . CodeAnalysis ;
5
5
using System . Linq ;
6
6
using System . Security . Claims ;
7
+ using System . Text ;
7
8
using Microsoft . AspNetCore . Authentication ;
8
9
using Microsoft . AspNetCore . Http ;
9
10
using Microsoft . Extensions . Logging ;
@@ -388,7 +389,14 @@ public virtual async Task<SignInResult> CheckPasswordSignInAsync(TUser user, str
388
389
// Only reset the lockout when not in quirks mode if either TFA is not enabled or the client is remembered for TFA.
389
390
if ( alwaysLockout || ! await IsTfaEnabled ( user ) || await IsTwoFactorClientRememberedAsync ( user ) )
390
391
{
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
+ }
392
400
}
393
401
394
402
return SignInResult . Success ;
@@ -398,7 +406,13 @@ public virtual async Task<SignInResult> CheckPasswordSignInAsync(TUser user, str
398
406
if ( UserManager . SupportsUserLockout && lockoutOnFailure )
399
407
{
400
408
// 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
+
402
416
if ( await UserManager . IsLockedOutAsync ( user ) )
403
417
{
404
418
return await LockedOut ( user ) ;
@@ -467,18 +481,23 @@ public virtual async Task<SignInResult> TwoFactorRecoveryCodeSignInAsync(string
467
481
var result = await UserManager . RedeemTwoFactorRecoveryCodeAsync ( user , recoveryCode ) ;
468
482
if ( result . Succeeded )
469
483
{
470
- await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent : false , rememberClient : false ) ;
471
- return SignInResult . Success ;
484
+ return await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent : false , rememberClient : false ) ;
472
485
}
473
486
474
487
// We don't protect against brute force attacks since codes are expected to be random.
475
488
return SignInResult . Failed ;
476
489
}
477
490
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 )
479
492
{
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
+ }
482
501
483
502
var claims = new List < Claim > ( ) ;
484
503
claims . Add ( new Claim ( "amr" , "mfa" ) ) ;
@@ -496,6 +515,7 @@ private async Task DoTwoFactorSignInAsync(TUser user, TwoFactorAuthenticationInf
496
515
await RememberTwoFactorClientAsync ( user ) ;
497
516
}
498
517
await SignInWithClaimsAsync ( user , isPersistent , claims ) ;
518
+ return SignInResult . Success ;
499
519
}
500
520
501
521
/// <summary>
@@ -528,13 +548,18 @@ public virtual async Task<SignInResult> TwoFactorAuthenticatorSignInAsync(string
528
548
529
549
if ( await UserManager . VerifyTwoFactorTokenAsync ( user , Options . Tokens . AuthenticatorTokenProvider , code ) )
530
550
{
531
- await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent , rememberClient ) ;
532
- return SignInResult . Success ;
551
+ return await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent , rememberClient ) ;
533
552
}
534
553
// If the token is incorrect, record the failure which also may cause the user to be locked out
535
554
if ( UserManager . SupportsUserLockout )
536
555
{
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
+ }
538
563
}
539
564
return SignInResult . Failed ;
540
565
}
@@ -569,13 +594,18 @@ 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
600
if ( UserManager . SupportsUserLockout )
577
601
{
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
+ }
579
609
}
580
610
return SignInResult . Failed ;
581
611
}
@@ -867,13 +897,77 @@ protected virtual Task<SignInResult> LockedOut(TUser user)
867
897
/// </summary>
868
898
/// <param name="user">The user</param>
869
899
/// <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 )
871
901
{
872
902
if ( UserManager . SupportsUserLockout )
873
903
{
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
+ }
875
970
}
876
- return Task . CompletedTask ;
877
971
}
878
972
879
973
internal sealed class TwoFactorAuthenticationInfo
0 commit comments