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 ;
@@ -370,7 +371,14 @@ public virtual async Task<SignInResult> CheckPasswordSignInAsync(TUser user, str
370
371
// Only reset the lockout when not in quirks mode if either TFA is not enabled or the client is remembered for TFA.
371
372
if ( alwaysLockout || ! await IsTwoFactorEnabledAsync ( user ) || await IsTwoFactorClientRememberedAsync ( user ) )
372
373
{
373
- await ResetLockout ( user ) ;
374
+ var resetLockoutResult = await ResetLockoutWithResult ( user ) ;
375
+ if ( ! resetLockoutResult . Succeeded )
376
+ {
377
+ // ResetLockout got an unsuccessful result that could be caused by concurrency failures indicating an
378
+ // attacker could be trying to bypass the MaxFailedAccessAttempts limit. Return the same failure we do
379
+ // when failing to increment the lockout to avoid giving an attacker extra guesses at the password.
380
+ return SignInResult . Failed ;
381
+ }
374
382
}
375
383
376
384
return SignInResult . Success ;
@@ -380,7 +388,13 @@ public virtual async Task<SignInResult> CheckPasswordSignInAsync(TUser user, str
380
388
if ( UserManager . SupportsUserLockout && lockoutOnFailure )
381
389
{
382
390
// If lockout is requested, increment access failed count which might lock out the user
383
- await UserManager . AccessFailedAsync ( user ) ;
391
+ var incrementLockoutResult = await UserManager . AccessFailedAsync ( user ) ?? IdentityResult . Success ;
392
+ if ( ! incrementLockoutResult . Succeeded )
393
+ {
394
+ // Return the same failure we do when resetting the lockout fails after a correct password.
395
+ return SignInResult . Failed ;
396
+ }
397
+
384
398
if ( await UserManager . IsLockedOutAsync ( user ) )
385
399
{
386
400
return await LockedOut ( user ) ;
@@ -449,18 +463,23 @@ public virtual async Task<SignInResult> TwoFactorRecoveryCodeSignInAsync(string
449
463
var result = await UserManager . RedeemTwoFactorRecoveryCodeAsync ( user , recoveryCode ) ;
450
464
if ( result . Succeeded )
451
465
{
452
- await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent : false , rememberClient : false ) ;
453
- return SignInResult . Success ;
466
+ return await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent : false , rememberClient : false ) ;
454
467
}
455
468
456
469
// We don't protect against brute force attacks since codes are expected to be random.
457
470
return SignInResult . Failed ;
458
471
}
459
472
460
- private async Task DoTwoFactorSignInAsync ( TUser user , TwoFactorAuthenticationInfo twoFactorInfo , bool isPersistent , bool rememberClient )
473
+ private async Task < SignInResult > DoTwoFactorSignInAsync ( TUser user , TwoFactorAuthenticationInfo twoFactorInfo , bool isPersistent , bool rememberClient )
461
474
{
462
- // When token is verified correctly, clear the access failed count used for lockout
463
- await ResetLockout ( user ) ;
475
+ var resetLockoutResult = await ResetLockoutWithResult ( user ) ;
476
+ if ( ! resetLockoutResult . Succeeded )
477
+ {
478
+ // ResetLockout got an unsuccessful result that could be caused by concurrency failures indicating an
479
+ // attacker could be trying to bypass the MaxFailedAccessAttempts limit. Return the same failure we do
480
+ // when failing to increment the lockout to avoid giving an attacker extra guesses at the two factor code.
481
+ return SignInResult . Failed ;
482
+ }
464
483
465
484
var claims = new List < Claim > ( ) ;
466
485
claims . Add ( new Claim ( "amr" , "mfa" ) ) ;
@@ -478,6 +497,7 @@ private async Task DoTwoFactorSignInAsync(TUser user, TwoFactorAuthenticationInf
478
497
await RememberTwoFactorClientAsync ( user ) ;
479
498
}
480
499
await SignInWithClaimsAsync ( user , isPersistent , claims ) ;
500
+ return SignInResult . Success ;
481
501
}
482
502
483
503
/// <summary>
@@ -510,13 +530,18 @@ public virtual async Task<SignInResult> TwoFactorAuthenticatorSignInAsync(string
510
530
511
531
if ( await UserManager . VerifyTwoFactorTokenAsync ( user , Options . Tokens . AuthenticatorTokenProvider , code ) )
512
532
{
513
- await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent , rememberClient ) ;
514
- return SignInResult . Success ;
533
+ return await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent , rememberClient ) ;
515
534
}
516
535
// If the token is incorrect, record the failure which also may cause the user to be locked out
517
536
if ( UserManager . SupportsUserLockout )
518
537
{
519
- await UserManager . AccessFailedAsync ( user ) ;
538
+ var incrementLockoutResult = await UserManager . AccessFailedAsync ( user ) ?? IdentityResult . Success ;
539
+ if ( ! incrementLockoutResult . Succeeded )
540
+ {
541
+ // Return the same failure we do when resetting the lockout fails after a correct two factor code.
542
+ // This is currently redundant, but it's here in case the code gets copied elsewhere.
543
+ return SignInResult . Failed ;
544
+ }
520
545
}
521
546
return SignInResult . Failed ;
522
547
}
@@ -551,13 +576,18 @@ public virtual async Task<SignInResult> TwoFactorSignInAsync(string provider, st
551
576
}
552
577
if ( await UserManager . VerifyTwoFactorTokenAsync ( user , provider , code ) )
553
578
{
554
- await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent , rememberClient ) ;
555
- return SignInResult . Success ;
579
+ return await DoTwoFactorSignInAsync ( user , twoFactorInfo , isPersistent , rememberClient ) ;
556
580
}
557
581
// If the token is incorrect, record the failure which also may cause the user to be locked out
558
582
if ( UserManager . SupportsUserLockout )
559
583
{
560
- await UserManager . AccessFailedAsync ( user ) ;
584
+ var incrementLockoutResult = await UserManager . AccessFailedAsync ( user ) ?? IdentityResult . Success ;
585
+ if ( ! incrementLockoutResult . Succeeded )
586
+ {
587
+ // Return the same failure we do when resetting the lockout fails after a correct two factor code.
588
+ // This is currently redundant, but it's here in case the code gets copied elsewhere.
589
+ return SignInResult . Failed ;
590
+ }
561
591
}
562
592
return SignInResult . Failed ;
563
593
}
@@ -848,13 +878,77 @@ protected virtual Task<SignInResult> LockedOut(TUser user)
848
878
/// </summary>
849
879
/// <param name="user">The user</param>
850
880
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/> of the operation.</returns>
851
- protected virtual Task ResetLockout ( TUser user )
881
+ protected virtual async Task ResetLockout ( TUser user )
852
882
{
853
883
if ( UserManager . SupportsUserLockout )
854
884
{
855
- return UserManager . ResetAccessFailedCountAsync ( user ) ;
885
+ // The IdentityResult should not be null according to the annotations, but our own tests return null and I'm trying to limit breakages.
886
+ var result = await UserManager . ResetAccessFailedCountAsync ( user ) ?? IdentityResult . Success ;
887
+
888
+ if ( ! result . Succeeded )
889
+ {
890
+ throw new IdentityResultException ( result ) ;
891
+ }
892
+ }
893
+ }
894
+
895
+ private async Task < IdentityResult > ResetLockoutWithResult ( TUser user )
896
+ {
897
+ // Avoid relying on throwing an exception if we're not in a derived class.
898
+ if ( GetType ( ) == typeof ( SignInManager < TUser > ) )
899
+ {
900
+ if ( ! UserManager . SupportsUserLockout )
901
+ {
902
+ return IdentityResult . Success ;
903
+ }
904
+
905
+ return await UserManager . ResetAccessFailedCountAsync ( user ) ?? IdentityResult . Success ;
906
+ }
907
+
908
+ try
909
+ {
910
+ var resetLockoutTask = ResetLockout ( user ) ;
911
+
912
+ if ( resetLockoutTask is Task < IdentityResult > resultTask )
913
+ {
914
+ return await resultTask ?? IdentityResult . Success ;
915
+ }
916
+
917
+ await resetLockoutTask ;
918
+ return IdentityResult . Success ;
919
+ }
920
+ catch ( IdentityResultException ex )
921
+ {
922
+ return ex . IdentityResult ;
923
+ }
924
+ }
925
+
926
+ private sealed class IdentityResultException : Exception
927
+ {
928
+ internal IdentityResultException ( IdentityResult result ) : base ( )
929
+ {
930
+ IdentityResult = result ;
931
+ }
932
+
933
+ internal IdentityResult IdentityResult { get ; set ; }
934
+
935
+ public override string Message
936
+ {
937
+ get
938
+ {
939
+ var sb = new StringBuilder ( "ResetLockout failed." ) ;
940
+
941
+ foreach ( var error in IdentityResult . Errors )
942
+ {
943
+ sb . AppendLine ( ) ;
944
+ sb . Append ( error . Code ) ;
945
+ sb . Append ( ": " ) ;
946
+ sb . Append ( error . Description ) ;
947
+ }
948
+
949
+ return sb . ToString ( ) ;
950
+ }
856
951
}
857
- return Task . CompletedTask ;
858
952
}
859
953
860
954
internal sealed class TwoFactorAuthenticationInfo
0 commit comments