Skip to content

Commit 67f3b04

Browse files
Stephen HalterMackinnon Buck
Stephen Halter
authored and
Mackinnon Buck
committed
Merged PR 47275: Prevent RefreshSignInAsync if it is called with wrong user
Since this is a patch, instead of throwing an exception in cases where we wouldn't before like we do in the PR to `main`, we instead log an error and fail to refresh the cookie if RefreshSignInAsync is called with a TUser that does not have the same user ID as the currently authenticated user. While this does not make it *as* obvious to developers that something has gone wrong, error logs are still pretty visible, and stale cookies are something web developers have to account for regardless. The big upside to not throwing in the patch is that we do not have to react to it in the email change confirmation flow to account for the possibility of RefreshSignInAsync throwing. ---- #### AI description (iteration 1) #### PR Classification Bug fix #### PR Summary This pull request disables the `RefreshSignInAsync` method if it is called with the wrong user, ensuring proper user authentication handling. - `src/Identity/Core/src/SignInManager.cs`: Added checks to disable `RefreshSignInAsync` if the user is not authenticated or if the authenticated user ID does not match the provided user ID. - `src/Identity/test/Identity.Test/SignInManagerTest.cs`: Added tests to verify that `RefreshSignInAsync` logs errors and does not proceed if the user is not authenticated or if authenticated with a different user.
1 parent 31e6c6d commit 67f3b04

File tree

2 files changed

+80
-15
lines changed

2 files changed

+80
-15
lines changed

src/Identity/Core/src/SignInManager.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,21 @@ public virtual async Task<bool> CanSignInAsync(TUser user)
162162
public virtual async Task RefreshSignInAsync(TUser user)
163163
{
164164
var auth = await Context.AuthenticateAsync(AuthenticationScheme);
165-
IList<Claim> claims = Array.Empty<Claim>();
165+
if (!auth.Succeeded || auth.Principal?.Identity?.IsAuthenticated != true)
166+
{
167+
Logger.LogError("RefreshSignInAsync prevented because the user is not currently authenticated. Use SignInAsync instead for initial sign in.");
168+
return;
169+
}
166170

171+
var authenticatedUserId = UserManager.GetUserId(auth.Principal);
172+
var newUserId = await UserManager.GetUserIdAsync(user);
173+
if (authenticatedUserId == null || authenticatedUserId != newUserId)
174+
{
175+
Logger.LogError("RefreshSignInAsync prevented because currently authenticated user has a different UserId. Use SignInAsync instead to change users.");
176+
return;
177+
}
178+
179+
IList<Claim> claims = Array.Empty<Claim>();
167180
var authenticationMethod = auth?.Principal?.FindFirst(ClaimTypes.AuthenticationMethod);
168181
var amr = auth?.Principal?.FindFirst("amr");
169182

src/Identity/test/Identity.Test/SignInManagerTest.cs

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -592,38 +592,38 @@ public async Task CanExternalSignIn(bool isPersistent, bool supportsLockout)
592592
[InlineData(true, false)]
593593
[InlineData(false, true)]
594594
[InlineData(false, false)]
595-
public async Task CanResignIn(
596-
// Suppress warning that says theory methods should use all of their parameters.
597-
// See comments below about why this isn't used.
598-
#pragma warning disable xUnit1026
599-
bool isPersistent,
600-
#pragma warning restore xUnit1026
601-
bool externalLogin)
595+
public async Task CanResignIn(bool isPersistent, bool externalLogin)
602596
{
603597
// Setup
604598
var user = new PocoUser { UserName = "Foo" };
605599
var context = new DefaultHttpContext();
606600
var auth = MockAuth(context);
607601
var loginProvider = "loginprovider";
608-
var id = new ClaimsIdentity();
602+
var id = new ClaimsIdentity("authscheme");
609603
if (externalLogin)
610604
{
611605
id.AddClaim(new Claim(ClaimTypes.AuthenticationMethod, loginProvider));
612606
}
613-
// REVIEW: auth changes we lost the ability to mock is persistent
614-
//var properties = new AuthenticationProperties { IsPersistent = isPersistent };
615-
var authResult = AuthenticateResult.NoResult();
607+
608+
var claimsPrincipal = new ClaimsPrincipal(id);
609+
var properties = new AuthenticationProperties { IsPersistent = isPersistent };
610+
var authResult = AuthenticateResult.Success(new AuthenticationTicket(claimsPrincipal, properties, "authscheme"));
616611
auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.ApplicationScheme))
617612
.Returns(Task.FromResult(authResult)).Verifiable();
618613
var manager = SetupUserManager(user);
614+
manager.Setup(m => m.GetUserId(claimsPrincipal)).Returns(user.Id.ToString());
619615
var signInManager = new Mock<SignInManager<PocoUser>>(manager.Object,
620616
new HttpContextAccessor { HttpContext = context },
621617
new Mock<IUserClaimsPrincipalFactory<PocoUser>>().Object,
622618
null, null, new Mock<IAuthenticationSchemeProvider>().Object, null)
623619
{ CallBase = true };
624-
//signInManager.Setup(s => s.SignInAsync(user, It.Is<AuthenticationProperties>(p => p.IsPersistent == isPersistent),
625-
//externalLogin? loginProvider : null)).Returns(Task.FromResult(0)).Verifiable();
626-
signInManager.Setup(s => s.SignInWithClaimsAsync(user, It.IsAny<AuthenticationProperties>(), It.IsAny<IEnumerable<Claim>>())).Returns(Task.FromResult(0)).Verifiable();
620+
621+
signInManager.Setup(s => s.SignInWithClaimsAsync(user,
622+
It.Is<AuthenticationProperties>(properties => properties.IsPersistent == isPersistent),
623+
It.Is<IEnumerable<Claim>>(claims => !externalLogin ||
624+
claims.Any(claim => claim.Type == ClaimTypes.AuthenticationMethod && claim.Value == loginProvider))))
625+
.Returns(Task.FromResult(0)).Verifiable();
626+
627627
signInManager.Object.Context = context;
628628

629629
// Act
@@ -634,6 +634,58 @@ public async Task CanResignIn(
634634
signInManager.Verify();
635635
}
636636

637+
[Fact]
638+
public async Task ResignInNoOpsAndLogsErrorIfNotAuthenticated()
639+
{
640+
var user = new PocoUser { UserName = "Foo" };
641+
var context = new DefaultHttpContext();
642+
var auth = MockAuth(context);
643+
var manager = SetupUserManager(user);
644+
var logger = new TestLogger<SignInManager<PocoUser>>();
645+
var signInManager = new Mock<SignInManager<PocoUser>>(manager.Object,
646+
new HttpContextAccessor { HttpContext = context },
647+
new Mock<IUserClaimsPrincipalFactory<PocoUser>>().Object,
648+
null, logger, new Mock<IAuthenticationSchemeProvider>().Object, null)
649+
{ CallBase = true };
650+
auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.ApplicationScheme))
651+
.Returns(Task.FromResult(AuthenticateResult.NoResult())).Verifiable();
652+
653+
await signInManager.Object.RefreshSignInAsync(user);
654+
655+
Assert.Contains("RefreshSignInAsync prevented because the user is not currently authenticated. Use SignInAsync instead for initial sign in.", logger.LogMessages);
656+
auth.Verify();
657+
signInManager.Verify(s => s.SignInWithClaimsAsync(It.IsAny<PocoUser>(), It.IsAny<AuthenticationProperties>(), It.IsAny<IEnumerable<Claim>>()),
658+
Times.Never());
659+
}
660+
661+
[Fact]
662+
public async Task ResignInNoOpsAndLogsErrorIfAuthenticatedWithDifferentUser()
663+
{
664+
var user = new PocoUser { UserName = "Foo" };
665+
var context = new DefaultHttpContext();
666+
var auth = MockAuth(context);
667+
var manager = SetupUserManager(user);
668+
var logger = new TestLogger<SignInManager<PocoUser>>();
669+
var signInManager = new Mock<SignInManager<PocoUser>>(manager.Object,
670+
new HttpContextAccessor { HttpContext = context },
671+
new Mock<IUserClaimsPrincipalFactory<PocoUser>>().Object,
672+
null, logger, new Mock<IAuthenticationSchemeProvider>().Object, null)
673+
{ CallBase = true };
674+
var id = new ClaimsIdentity("authscheme");
675+
var claimsPrincipal = new ClaimsPrincipal(id);
676+
var authResult = AuthenticateResult.Success(new AuthenticationTicket(claimsPrincipal, new AuthenticationProperties(), "authscheme"));
677+
auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.ApplicationScheme))
678+
.Returns(Task.FromResult(authResult)).Verifiable();
679+
manager.Setup(m => m.GetUserId(claimsPrincipal)).Returns("different");
680+
681+
await signInManager.Object.RefreshSignInAsync(user);
682+
683+
Assert.Contains("RefreshSignInAsync prevented because currently authenticated user has a different UserId. Use SignInAsync instead to change users.", logger.LogMessages);
684+
auth.Verify();
685+
signInManager.Verify(s => s.SignInWithClaimsAsync(It.IsAny<PocoUser>(), It.IsAny<AuthenticationProperties>(), It.IsAny<IEnumerable<Claim>>()),
686+
Times.Never());
687+
}
688+
637689
[Theory]
638690
[InlineData(true, true, true, true)]
639691
[InlineData(true, true, false, true)]

0 commit comments

Comments
 (0)