Skip to content

Commit e5ea1ee

Browse files
Stephen Halterwtgodbe
authored andcommitted
Merged PR 31116: [internal/release/7.0] Always return SignInResult.Failed if updating AccessFailedCount fails
1 parent 1b84f49 commit e5ea1ee

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;
@@ -388,7 +389,14 @@ public virtual async Task<SignInResult> CheckPasswordSignInAsync(TUser user, str
388389
// Only reset the lockout when not in quirks mode if either TFA is not enabled or the client is remembered for TFA.
389390
if (alwaysLockout || !await IsTfaEnabled(user) || await IsTwoFactorClientRememberedAsync(user))
390391
{
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+
}
392400
}
393401

394402
return SignInResult.Success;
@@ -398,7 +406,13 @@ public virtual async Task<SignInResult> CheckPasswordSignInAsync(TUser user, str
398406
if (UserManager.SupportsUserLockout && lockoutOnFailure)
399407
{
400408
// 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+
402416
if (await UserManager.IsLockedOutAsync(user))
403417
{
404418
return await LockedOut(user);
@@ -467,18 +481,23 @@ public virtual async Task<SignInResult> TwoFactorRecoveryCodeSignInAsync(string
467481
var result = await UserManager.RedeemTwoFactorRecoveryCodeAsync(user, recoveryCode);
468482
if (result.Succeeded)
469483
{
470-
await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent: false, rememberClient: false);
471-
return SignInResult.Success;
484+
return await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent: false, rememberClient: false);
472485
}
473486

474487
// We don't protect against brute force attacks since codes are expected to be random.
475488
return SignInResult.Failed;
476489
}
477490

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)
479492
{
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+
}
482501

483502
var claims = new List<Claim>();
484503
claims.Add(new Claim("amr", "mfa"));
@@ -496,6 +515,7 @@ private async Task DoTwoFactorSignInAsync(TUser user, TwoFactorAuthenticationInf
496515
await RememberTwoFactorClientAsync(user);
497516
}
498517
await SignInWithClaimsAsync(user, isPersistent, claims);
518+
return SignInResult.Success;
499519
}
500520

501521
/// <summary>
@@ -528,13 +548,18 @@ public virtual async Task<SignInResult> TwoFactorAuthenticatorSignInAsync(string
528548

529549
if (await UserManager.VerifyTwoFactorTokenAsync(user, Options.Tokens.AuthenticatorTokenProvider, code))
530550
{
531-
await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent, rememberClient);
532-
return SignInResult.Success;
551+
return await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent, rememberClient);
533552
}
534553
// If the token is incorrect, record the failure which also may cause the user to be locked out
535554
if (UserManager.SupportsUserLockout)
536555
{
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+
}
538563
}
539564
return SignInResult.Failed;
540565
}
@@ -569,13 +594,18 @@ 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
576600
if (UserManager.SupportsUserLockout)
577601
{
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+
}
579609
}
580610
return SignInResult.Failed;
581611
}
@@ -867,13 +897,77 @@ protected virtual Task<SignInResult> LockedOut(TUser user)
867897
/// </summary>
868898
/// <param name="user">The user</param>
869899
/// <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)
871901
{
872902
if (UserManager.SupportsUserLockout)
873903
{
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+
}
875970
}
876-
return Task.CompletedTask;
877971
}
878972

879973
internal sealed class TwoFactorAuthenticationInfo

0 commit comments

Comments
 (0)