Skip to content

Commit 00fa5e1

Browse files
wtgodbeStephen Halter
and
Stephen Halter
authored
Merged PR 31116: [internal/release/7.0] Always return SignInResult.Failed if updating AccessFailedCount fails (#49345)
Co-authored-by: Stephen Halter <shalter@microsoft.com>
1 parent 33a8622 commit 00fa5e1

File tree

2 files changed

+340
-16
lines changed

2 files changed

+340
-16
lines changed

src/Identity/Core/src/SignInManager.cs

Lines changed: 110 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Diagnostics.CodeAnalysis;
55
using System.Linq;
66
using System.Security.Claims;
7+
using System.Text;
78
using Microsoft.AspNetCore.Authentication;
89
using Microsoft.AspNetCore.Http;
910
using Microsoft.Extensions.Logging;
@@ -370,7 +371,14 @@ public virtual async Task<SignInResult> CheckPasswordSignInAsync(TUser user, str
370371
// Only reset the lockout when not in quirks mode if either TFA is not enabled or the client is remembered for TFA.
371372
if (alwaysLockout || !await IsTwoFactorEnabledAsync(user) || await IsTwoFactorClientRememberedAsync(user))
372373
{
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+
}
374382
}
375383

376384
return SignInResult.Success;
@@ -380,7 +388,13 @@ public virtual async Task<SignInResult> CheckPasswordSignInAsync(TUser user, str
380388
if (UserManager.SupportsUserLockout && lockoutOnFailure)
381389
{
382390
// 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+
384398
if (await UserManager.IsLockedOutAsync(user))
385399
{
386400
return await LockedOut(user);
@@ -449,18 +463,23 @@ public virtual async Task<SignInResult> TwoFactorRecoveryCodeSignInAsync(string
449463
var result = await UserManager.RedeemTwoFactorRecoveryCodeAsync(user, recoveryCode);
450464
if (result.Succeeded)
451465
{
452-
await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent: false, rememberClient: false);
453-
return SignInResult.Success;
466+
return await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent: false, rememberClient: false);
454467
}
455468

456469
// We don't protect against brute force attacks since codes are expected to be random.
457470
return SignInResult.Failed;
458471
}
459472

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)
461474
{
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+
}
464483

465484
var claims = new List<Claim>();
466485
claims.Add(new Claim("amr", "mfa"));
@@ -478,6 +497,7 @@ private async Task DoTwoFactorSignInAsync(TUser user, TwoFactorAuthenticationInf
478497
await RememberTwoFactorClientAsync(user);
479498
}
480499
await SignInWithClaimsAsync(user, isPersistent, claims);
500+
return SignInResult.Success;
481501
}
482502

483503
/// <summary>
@@ -510,13 +530,18 @@ public virtual async Task<SignInResult> TwoFactorAuthenticatorSignInAsync(string
510530

511531
if (await UserManager.VerifyTwoFactorTokenAsync(user, Options.Tokens.AuthenticatorTokenProvider, code))
512532
{
513-
await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent, rememberClient);
514-
return SignInResult.Success;
533+
return await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent, rememberClient);
515534
}
516535
// If the token is incorrect, record the failure which also may cause the user to be locked out
517536
if (UserManager.SupportsUserLockout)
518537
{
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+
}
520545
}
521546
return SignInResult.Failed;
522547
}
@@ -551,13 +576,18 @@ public virtual async Task<SignInResult> TwoFactorSignInAsync(string provider, st
551576
}
552577
if (await UserManager.VerifyTwoFactorTokenAsync(user, provider, code))
553578
{
554-
await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent, rememberClient);
555-
return SignInResult.Success;
579+
return await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent, rememberClient);
556580
}
557581
// If the token is incorrect, record the failure which also may cause the user to be locked out
558582
if (UserManager.SupportsUserLockout)
559583
{
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+
}
561591
}
562592
return SignInResult.Failed;
563593
}
@@ -848,13 +878,77 @@ protected virtual Task<SignInResult> LockedOut(TUser user)
848878
/// </summary>
849879
/// <param name="user">The user</param>
850880
/// <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)
852882
{
853883
if (UserManager.SupportsUserLockout)
854884
{
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+
}
856951
}
857-
return Task.CompletedTask;
858952
}
859953

860954
internal sealed class TwoFactorAuthenticationInfo

0 commit comments

Comments
 (0)