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()
{