From 4510d95f4ff41f8276c614360dfd9e5f2829471a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 Aug 2025 17:25:21 +0000 Subject: [PATCH 1/2] Initial plan From b701f6ed87ce7dc299ca88b8a18402f3939f0654 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 Aug 2025 17:41:08 +0000 Subject: [PATCH 2/2] Implement better error message for unauthenticated antiforgery token validation Co-authored-by: MackinnonBuck <10456961+MackinnonBuck@users.noreply.github.com> --- .../DefaultAntiforgeryTokenGenerator.cs | 23 +++- src/Antiforgery/src/Resources.resx | 3 + .../DefaultAntiforgeryTokenGeneratorTest.cs | 109 ++++++++++++++++++ 3 files changed, 133 insertions(+), 2 deletions(-) diff --git a/src/Antiforgery/src/Internal/DefaultAntiforgeryTokenGenerator.cs b/src/Antiforgery/src/Internal/DefaultAntiforgeryTokenGenerator.cs index 06a08914924d..8800cf337b45 100644 --- a/src/Antiforgery/src/Internal/DefaultAntiforgeryTokenGenerator.cs +++ b/src/Antiforgery/src/Internal/DefaultAntiforgeryTokenGenerator.cs @@ -160,13 +160,32 @@ public bool TryValidateTokenSet( if (!comparer.Equals(requestToken.Username, currentUsername)) { - message = Resources.FormatAntiforgeryToken_UsernameMismatch(requestToken.Username, currentUsername); + // Special case: if current user is not authenticated but the request token was meant for an authenticated user, + // provide a more helpful error message suggesting middleware ordering issue + if (authenticatedIdentity == null && !string.IsNullOrEmpty(requestToken.Username)) + { + message = Resources.AntiforgeryToken_ClaimUidMismatch_UnauthenticatedUser; + } + else + { + message = Resources.FormatAntiforgeryToken_UsernameMismatch(requestToken.Username, currentUsername); + } return false; } if (!object.Equals(requestToken.ClaimUid, currentClaimUid)) { - message = Resources.AntiforgeryToken_ClaimUidMismatch; + // Special case: if current user is not authenticated but the request token was meant for an authenticated user, + // provide a more helpful error message suggesting middleware ordering issue + if (authenticatedIdentity == null && + (requestToken.ClaimUid != null || !string.IsNullOrEmpty(requestToken.Username))) + { + message = Resources.AntiforgeryToken_ClaimUidMismatch_UnauthenticatedUser; + } + else + { + message = Resources.AntiforgeryToken_ClaimUidMismatch; + } return false; } diff --git a/src/Antiforgery/src/Resources.resx b/src/Antiforgery/src/Resources.resx index 1bf0528d9e35..90e7b0f8a7ac 100644 --- a/src/Antiforgery/src/Resources.resx +++ b/src/Antiforgery/src/Resources.resx @@ -127,6 +127,9 @@ The provided antiforgery token was meant for a different claims-based user than the current user. + + The provided antiforgery token was meant for an authenticated user, but the current user is not authenticated. Did you put UseAntiforgery() after UseAuthentication()? + The antiforgery token could not be decrypted. diff --git a/src/Antiforgery/test/DefaultAntiforgeryTokenGeneratorTest.cs b/src/Antiforgery/test/DefaultAntiforgeryTokenGeneratorTest.cs index 3691b24aa3b0..a2a990b0d6b5 100644 --- a/src/Antiforgery/test/DefaultAntiforgeryTokenGeneratorTest.cs +++ b/src/Antiforgery/test/DefaultAntiforgeryTokenGeneratorTest.cs @@ -460,6 +460,115 @@ public void TryValidateTokenSet_ClaimUidMismatch() Assert.Equal(expectedMessage, message); } + [Fact] + public void TryValidateTokenSet_ClaimUidMismatch_UnauthenticatedUser_WithClaimUid() + { + // Arrange + var httpContext = new DefaultHttpContext(); + var identity = new ClaimsIdentity(); // Not authenticated + httpContext.User = new ClaimsPrincipal(identity); + + var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; + var fieldtoken = new AntiforgeryToken() + { + SecurityToken = cookieToken.SecurityToken, + IsCookieToken = false, + ClaimUid = new BinaryBlob(256) // Token was meant for authenticated user + }; + + var mockClaimUidExtractor = new Mock(); + mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(It.IsAny())) + .Returns((string)null); + + var tokenProvider = new DefaultAntiforgeryTokenGenerator( + claimUidExtractor: mockClaimUidExtractor.Object, + additionalDataProvider: null); + + string expectedMessage = + "The provided antiforgery token was meant for an authenticated user, but the current user is not authenticated. Did you put UseAntiforgery() after UseAuthentication()?"; + + // Act + string message; + var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message); + + // Assert + Assert.False(result); + Assert.Equal(expectedMessage, message); + } + + [Fact] + public void TryValidateTokenSet_ClaimUidMismatch_UnauthenticatedUser_WithUsername() + { + // Arrange + var httpContext = new DefaultHttpContext(); + var identity = new ClaimsIdentity(); // Not authenticated + httpContext.User = new ClaimsPrincipal(identity); + + var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; + var fieldtoken = new AntiforgeryToken() + { + SecurityToken = cookieToken.SecurityToken, + IsCookieToken = false, + Username = "test-user" // Token was meant for authenticated user with username + }; + + var mockClaimUidExtractor = new Mock(); + mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(It.IsAny())) + .Returns((string)null); + + var tokenProvider = new DefaultAntiforgeryTokenGenerator( + claimUidExtractor: mockClaimUidExtractor.Object, + additionalDataProvider: null); + + string expectedMessage = + "The provided antiforgery token was meant for an authenticated user, but the current user is not authenticated. Did you put UseAntiforgery() after UseAuthentication()?"; + + // Act + string message; + var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message); + + // Assert + Assert.False(result); + Assert.Equal(expectedMessage, message); + } + + [Fact] + public void TryValidateTokenSet_ClaimUidMismatch_AuthenticatedUser_StillUsesOriginalMessage() + { + // Arrange + var httpContext = new DefaultHttpContext(); + var identity = GetAuthenticatedIdentity("current-user"); // Authenticated + httpContext.User = new ClaimsPrincipal(identity); + + var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; + var fieldtoken = new AntiforgeryToken() + { + SecurityToken = cookieToken.SecurityToken, + IsCookieToken = false, + ClaimUid = new BinaryBlob(256) // Different ClaimUid + }; + + var differentToken = new BinaryBlob(256); + var mockClaimUidExtractor = new Mock(); + mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(It.Is(c => c.Identity == identity))) + .Returns(Convert.ToBase64String(differentToken.GetData())); + + var tokenProvider = new DefaultAntiforgeryTokenGenerator( + claimUidExtractor: mockClaimUidExtractor.Object, + additionalDataProvider: null); + + string expectedMessage = + "The provided antiforgery token was meant for a different claims-based user than the current user."; + + // Act + string message; + var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message); + + // Assert + Assert.False(result); + Assert.Equal(expectedMessage, message); + } + [Fact] public void TryValidateTokenSet_AdditionalDataRejected() {