Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions src/Antiforgery/src/Internal/DefaultAntiforgeryTokenGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
3 changes: 3 additions & 0 deletions src/Antiforgery/src/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@
<data name="AntiforgeryToken_ClaimUidMismatch" xml:space="preserve">
<value>The provided antiforgery token was meant for a different claims-based user than the current user.</value>
</data>
<data name="AntiforgeryToken_ClaimUidMismatch_UnauthenticatedUser" xml:space="preserve">
<value>The provided antiforgery token was meant for an authenticated user, but the current user is not authenticated. Did you put UseAntiforgery() after UseAuthentication()?</value>
</data>
<data name="AntiforgeryToken_DeserializationFailed" xml:space="preserve">
<value>The antiforgery token could not be decrypted.</value>
</data>
Expand Down
109 changes: 109 additions & 0 deletions src/Antiforgery/test/DefaultAntiforgeryTokenGeneratorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IClaimUidExtractor>();
mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(It.IsAny<ClaimsPrincipal>()))
.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<IClaimUidExtractor>();
mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(It.IsAny<ClaimsPrincipal>()))
.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<IClaimUidExtractor>();
mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(It.Is<ClaimsPrincipal>(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()
{
Expand Down
Loading