Skip to content

Commit 31ec24f

Browse files
Stephen Halterwtgodbe
authored andcommitted
Merged PR 31212: [internal/release/6.0] Always return SignInResult.Failed if updating AccessFailedCount fails
1 parent 7422b16 commit 31ec24f

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
@@ -6,6 +6,7 @@
66
using System.Diagnostics.CodeAnalysis;
77
using System.Linq;
88
using System.Security.Claims;
9+
using System.Text;
910
using System.Threading.Tasks;
1011
using Microsoft.AspNetCore.Authentication;
1112
using 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

Comments
 (0)