diff --git a/src/Identity/Core/src/SignInManager.cs b/src/Identity/Core/src/SignInManager.cs index e303012f9849..709171c9a9f5 100644 --- a/src/Identity/Core/src/SignInManager.cs +++ b/src/Identity/Core/src/SignInManager.cs @@ -206,6 +206,13 @@ public virtual async Task RefreshSignInAsync(TUser user) return (false, auth.Properties?.IsPersistent); } + var presentedStamp = auth.Principal?.FindFirstValue(Options.ClaimsIdentity.SecurityStampClaimType); + if (!await ValidateSecurityStampAsync(user, presentedStamp)) + { + Logger.LogError("RefreshSignInAsync prevented because the presented security stamp is stale."); + return (false, auth.Properties?.IsPersistent); + } + IList claims = Array.Empty(); var authenticationMethod = auth.Principal?.FindFirst(ClaimTypes.AuthenticationMethod); var amr = auth.Principal?.FindFirst("amr"); diff --git a/src/Identity/test/Identity.Test/SignInManagerTest.cs b/src/Identity/test/Identity.Test/SignInManagerTest.cs index 2dcba35b6fd0..7d3ccafa2553 100644 --- a/src/Identity/test/Identity.Test/SignInManagerTest.cs +++ b/src/Identity/test/Identity.Test/SignInManagerTest.cs @@ -1091,6 +1091,68 @@ public async Task ResignInNoOpsAndLogsErrorIfAuthenticatedWithDifferentUser() Times.Never()); } + [Fact] + public async Task ResignInNoOpsAndLogsErrorIfSecurityStampIsStale() + { + var user = new PocoUser { UserName = "Foo" }; + var context = new DefaultHttpContext(); + var auth = MockAuth(context); + var manager = SetupUserManager(user); + manager.Setup(m => m.SupportsUserSecurityStamp).Returns(true); + manager.Setup(m => m.GetSecurityStampAsync(user)).ReturnsAsync("current-stamp"); + var logger = new TestLogger>(); + var signInManager = new Mock>(manager.Object, + new HttpContextAccessor { HttpContext = context }, + new Mock>().Object, + null, logger, new Mock().Object, null) + { CallBase = true }; + var id = new ClaimsIdentity("authscheme"); + id.AddClaim(new Claim(new IdentityOptions().ClaimsIdentity.SecurityStampClaimType, "stale-stamp")); + var claimsPrincipal = new ClaimsPrincipal(id); + var authResult = AuthenticateResult.Success(new AuthenticationTicket(claimsPrincipal, new AuthenticationProperties(), "authscheme")); + auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.ApplicationScheme)) + .Returns(Task.FromResult(authResult)).Verifiable(); + manager.Setup(m => m.GetUserId(claimsPrincipal)).Returns(user.Id.ToString()); + + await signInManager.Object.RefreshSignInAsync(user); + + Assert.Contains("RefreshSignInAsync prevented because the presented security stamp is stale.", logger.LogMessages); + auth.Verify(); + signInManager.Verify(s => s.SignInWithClaimsAsync(It.IsAny(), It.IsAny(), It.IsAny>()), + Times.Never()); + } + + [Fact] + public async Task CanResignInWhenSecurityStampMatches() + { + var user = new PocoUser { UserName = "Foo" }; + var context = new DefaultHttpContext(); + var auth = MockAuth(context); + var manager = SetupUserManager(user); + manager.Setup(m => m.SupportsUserSecurityStamp).Returns(true); + manager.Setup(m => m.GetSecurityStampAsync(user)).ReturnsAsync("current-stamp"); + var id = new ClaimsIdentity("authscheme"); + id.AddClaim(new Claim(new IdentityOptions().ClaimsIdentity.SecurityStampClaimType, "current-stamp")); + var claimsPrincipal = new ClaimsPrincipal(id); + var authResult = AuthenticateResult.Success(new AuthenticationTicket(claimsPrincipal, new AuthenticationProperties(), "authscheme")); + auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.ApplicationScheme)) + .Returns(Task.FromResult(authResult)).Verifiable(); + manager.Setup(m => m.GetUserId(claimsPrincipal)).Returns(user.Id.ToString()); + var signInManager = new Mock>(manager.Object, + new HttpContextAccessor { HttpContext = context }, + new Mock>().Object, + null, null, new Mock().Object, null) + { CallBase = true }; + signInManager.Setup(s => s.SignInWithClaimsAsync(user, It.IsAny(), It.IsAny>())) + .Returns(Task.FromResult(0)).Verifiable(); + signInManager.Object.Context = context; + + await signInManager.Object.RefreshSignInAsync(user); + + auth.Verify(); + signInManager.Verify(); + } + [Theory] [InlineData(true, true, true, true)] [InlineData(true, true, false, true)]