using the quickstart 3 code samples, set the mvc client and the id4 server as start, go to the secure page and login. Ensure to hit the login button as many times as you can in quick sucession.
I think, and its a guess as I haven't manged to track this down, that it might be to do with anti forgery token and not directly related to ID4, but not sure.
You may have to try this several times. Its not always repeatable, you have to be quick on the button.
Nothing from the log has jumped out.
Application insights has a request that returns 400 but nothing else associated with this request.
I suspect this is an ASP.NET Core MVC issue, not an IS one.
I am going to create a really simple dotnet app to see if I can replicate.
With the debugger attached, it was hard to replicate. Speed obviously has something to do with this.
Got the logging set up correctly this time:
QuickstartIdentityServer> [10:37:39 Information] Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.ValidateAntiforgeryTokenAuthorizationFilter
QuickstartIdentityServer> Antiforgery token validation failed. The provided antiforgery token was meant for a different claims-based user than the current user.
QuickstartIdentityServer> Microsoft.AspNetCore.Antiforgery.AntiforgeryValidationException: The provided antiforgery token was meant for a different claims-based user than the current user.
QuickstartIdentityServer> at Microsoft.AspNetCore.Antiforgery.Internal.DefaultAntiforgery.ValidateTokens(HttpContext httpContext, AntiforgeryTokenSet antiforgeryTokenSet)
QuickstartIdentityServer> at Microsoft.AspNetCore.Antiforgery.Internal.DefaultAntiforgery.
QuickstartIdentityServer> --- End of stack trace from previous location where exception was thrown ---
QuickstartIdentityServer> at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
QuickstartIdentityServer> at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
QuickstartIdentityServer> at Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.ValidateAntiforgeryTokenAuthorizationFilter.
QuickstartIdentityServer>
QuickstartIdentityServer> [10:37:39 Information] Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker
QuickstartIdentityServer> Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.ValidateAntiforgeryTokenAuthorizationFilter'.
QuickstartIdentityServer>
QuickstartIdentityServer> [10:37:39 Information] Microsoft.AspNetCore.Mvc.StatusCodeResult
QuickstartIdentityServer> Executing HttpStatusCodeResult, setting HTTP status code 400
So confirmed that its the anti-forgery validator.
I added some debugging and I _think_ that the user cookie is added to the request and so the cookie and the anti forgery token are not in sync.
Yes, I am guessing it's related to anti forgery. That puts the user's id or anonymous in the anti-forgery token, so when your auth status the older value is no longer in sync with the current user. In short, I don't think this is an IdentityServer issue. If you discover otherwise, please reopen.
@AnthonyDewhirst did you solve this issue
hi @mostafahedawa,
I did, its a little bit of a hack. Basically, it looks like the a previous button hit succeeds in authenticating and then a later one attaches the token/cookie on to the later request, however it still has the original antiforgery token which thinks it shouldn't have an authenticated user, I do the following:
```c#
using System;
using System.Net;
using System.Threading.Tasks;
using IdentityServer4.Extensions;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Antiforgery.Internal;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace My.Middleware
{
///
/// The built in antiforgery token can sometimes have cause an issue where the user hits the login button several times in a row.
/// The affect is that the first request that succeeds has a new cookie on it which doesn't match the anti forgery token, thus
/// the subsequent requests fail. We catch and check for this just for login, and then allow a redirect if this is the case.
///
public class AntiforgeryRedirectIssueMiddleware
{
private readonly RequestDelegate _next;
private readonly IAntiforgeryTokenSerializer _antiforgeryTokenSerializer;
private readonly IAntiforgery _antiforgery;
private readonly IClaimUidExtractor _claimUidExtractor;
private readonly string _matchPath;
private readonly Uri _redirectUri;
private readonly ILogger
public AntiforgeryRedirectIssueMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, IAntiforgeryTokenSerializer antiforgeryTokenSerializer, IAntiforgery antiforgery, IClaimUidExtractor claimUidExtractor, string matchPath, Uri redirectUri)
{
_next = next;
_antiforgeryTokenSerializer = antiforgeryTokenSerializer;
_antiforgery = antiforgery;
_claimUidExtractor = claimUidExtractor;
_matchPath = matchPath;
_redirectUri = redirectUri;
_logger = loggerFactory.CreateLogger<AntiforgeryRedirectIssueMiddleware>();
}
/// <summary>
/// Processes a response to see if was a 400 response to a known url and if so redirects to a given url.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task Invoke(HttpContext context)
{
await _next.Invoke(context);
if (context.Response.StatusCode == (int)HttpStatusCode.BadRequest)
{
if (context.Request.Path.StartsWithSegments(new PathString(_matchPath)))
{
var tokenSet = _antiforgery.GetTokens(context);
DeserializeTokens(context, tokenSet, out var cookieToken, out var requestToken);
var claimId = _claimUidExtractor.ExtractClaimUid(context.User);
var hasClaimIdInRequestToken = requestToken.ClaimUid != null;
if (context.User.IsAuthenticated() && !string.IsNullOrWhiteSpace(claimId) && !hasClaimIdInRequestToken)
{
_logger.LogWarning("the user may have hit the login button multiple times. It looks like they have logged in, so redirect them back to the login page");
context.Response.Redirect(_redirectUri.ToString());
}
}
}
}
private void DeserializeTokens(HttpContext httpContext, AntiforgeryTokenSet antiforgeryTokenSet, out AntiforgeryToken cookieToken, out AntiforgeryToken requestToken)
{
var antiforgeryFeature = GetAntiforgeryFeature(httpContext);
if (antiforgeryFeature.HaveDeserializedCookieToken)
{
cookieToken = antiforgeryFeature.CookieToken;
}
else
{
cookieToken = _antiforgeryTokenSerializer.Deserialize(antiforgeryTokenSet.CookieToken);
antiforgeryFeature.CookieToken = cookieToken;
antiforgeryFeature.HaveDeserializedCookieToken = true;
}
if (antiforgeryFeature.HaveDeserializedRequestToken)
{
requestToken = antiforgeryFeature.RequestToken;
}
else
{
requestToken = _antiforgeryTokenSerializer.Deserialize(antiforgeryTokenSet.RequestToken);
antiforgeryFeature.RequestToken = requestToken;
antiforgeryFeature.HaveDeserializedRequestToken = true;
}
}
private static IAntiforgeryFeature GetAntiforgeryFeature(HttpContext httpContext)
{
var instance = httpContext.Features.Get<IAntiforgeryFeature>();
if (instance == null)
{
instance = new AntiforgeryFeature();
httpContext.Features.Set(instance);
}
return instance;
}
}
}
then wrap this in a use middleware call
```c#
using System;
using Microsoft.AspNetCore.Builder;
namespace My.Middleware
{
public static class AntiforgeryRedirectIssueExtensions
{
public static void UseAntiForgeryRedirectIssueSolution(this IApplicationBuilder app, string matchPath, Uri redirectUri)
{
if (string.IsNullOrWhiteSpace(matchPath))
{
throw new ArgumentNullException(nameof(matchPath));
}
if (redirectUri == null)
{
throw new ArgumentNullException(nameof(redirectUri));
}
app.UseMiddleware<AntiforgeryRedirectIssueMiddleware>(matchPath, redirectUri);
}
}
}
then make a call like so in startup after calls to ID4
c#
app.UseAntiForgeryRedirectIssueSolution("/account/login", redirectUri);
our login will round trip again and the user will not even see anything happening.
As this only happens after a successful call (as far as I can tell) I am quite happy to just make it look like the user logged in just fine.
You could always disable the button etc
I had to set AntiforgeryOptions.Cookie.SameSite = SameSiteMode.Lax. Otherwise antiforgery will always fail when user open's the login view in two different tabs.
hi @mostafahedawa,
I did, its a little bit of a hack. Basically, it looks like the a previous button hit succeeds in authenticating and then a later one attaches the token/cookie on to the later request, however it still has the original antiforgery token which thinks it shouldn't have an authenticated user, I do the following:
using System; using System.Net; using System.Threading.Tasks; using IdentityServer4.Extensions; using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Antiforgery.Internal; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace My.Middleware { /// <summary> /// The built in antiforgery token can sometimes have cause an issue where the user hits the login button several times in a row. /// The affect is that the first request that succeeds has a new cookie on it which doesn't match the anti forgery token, thus /// the subsequent requests fail. We catch and check for this just for login, and then allow a redirect if this is the case. /// </summary> public class AntiforgeryRedirectIssueMiddleware { private readonly RequestDelegate _next; private readonly IAntiforgeryTokenSerializer _antiforgeryTokenSerializer; private readonly IAntiforgery _antiforgery; private readonly IClaimUidExtractor _claimUidExtractor; private readonly string _matchPath; private readonly Uri _redirectUri; private readonly ILogger<AntiforgeryRedirectIssueMiddleware> _logger; public AntiforgeryRedirectIssueMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, IAntiforgeryTokenSerializer antiforgeryTokenSerializer, IAntiforgery antiforgery, IClaimUidExtractor claimUidExtractor, string matchPath, Uri redirectUri) { _next = next; _antiforgeryTokenSerializer = antiforgeryTokenSerializer; _antiforgery = antiforgery; _claimUidExtractor = claimUidExtractor; _matchPath = matchPath; _redirectUri = redirectUri; _logger = loggerFactory.CreateLogger<AntiforgeryRedirectIssueMiddleware>(); } /// <summary> /// Processes a response to see if was a 400 response to a known url and if so redirects to a given url. /// </summary> /// <param name="context"></param> /// <returns></returns> public async Task Invoke(HttpContext context) { await _next.Invoke(context); if (context.Response.StatusCode == (int)HttpStatusCode.BadRequest) { if (context.Request.Path.StartsWithSegments(new PathString(_matchPath))) { var tokenSet = _antiforgery.GetTokens(context); DeserializeTokens(context, tokenSet, out var cookieToken, out var requestToken); var claimId = _claimUidExtractor.ExtractClaimUid(context.User); var hasClaimIdInRequestToken = requestToken.ClaimUid != null; if (context.User.IsAuthenticated() && !string.IsNullOrWhiteSpace(claimId) && !hasClaimIdInRequestToken) { _logger.LogWarning("the user may have hit the login button multiple times. It looks like they have logged in, so redirect them back to the login page"); context.Response.Redirect(_redirectUri.ToString()); } } } } private void DeserializeTokens(HttpContext httpContext, AntiforgeryTokenSet antiforgeryTokenSet, out AntiforgeryToken cookieToken, out AntiforgeryToken requestToken) { var antiforgeryFeature = GetAntiforgeryFeature(httpContext); if (antiforgeryFeature.HaveDeserializedCookieToken) { cookieToken = antiforgeryFeature.CookieToken; } else { cookieToken = _antiforgeryTokenSerializer.Deserialize(antiforgeryTokenSet.CookieToken); antiforgeryFeature.CookieToken = cookieToken; antiforgeryFeature.HaveDeserializedCookieToken = true; } if (antiforgeryFeature.HaveDeserializedRequestToken) { requestToken = antiforgeryFeature.RequestToken; } else { requestToken = _antiforgeryTokenSerializer.Deserialize(antiforgeryTokenSet.RequestToken); antiforgeryFeature.RequestToken = requestToken; antiforgeryFeature.HaveDeserializedRequestToken = true; } } private static IAntiforgeryFeature GetAntiforgeryFeature(HttpContext httpContext) { var instance = httpContext.Features.Get<IAntiforgeryFeature>(); if (instance == null) { instance = new AntiforgeryFeature(); httpContext.Features.Set(instance); } return instance; } } }then wrap this in a use middleware call
using System; using Microsoft.AspNetCore.Builder; namespace My.Middleware { public static class AntiforgeryRedirectIssueExtensions { public static void UseAntiForgeryRedirectIssueSolution(this IApplicationBuilder app, string matchPath, Uri redirectUri) { if (string.IsNullOrWhiteSpace(matchPath)) { throw new ArgumentNullException(nameof(matchPath)); } if (redirectUri == null) { throw new ArgumentNullException(nameof(redirectUri)); } app.UseMiddleware<AntiforgeryRedirectIssueMiddleware>(matchPath, redirectUri); } } }then make a call like so in startup after calls to ID4
app.UseAntiForgeryRedirectIssueSolution("/account/login", redirectUri);our login will round trip again and the user will not even see anything happening.
As this only happens after a successful call (as far as I can tell) I am quite happy to just make it look like the user logged in just fine.
You could always disable the button etc
Where do we get the redirectUri on the Startup file?
@AnthonyDewhirst Thank you so much for this. I teach middle school where I am building an app, and the students love to keep clicking the login button repeatedly. This solved this for me. I'm using the default Identity setup that comes with a MVC .NET core project, so I'm not using Identity4. However, all your code works except for one thing...I just had to change
context.User.IsAuthenticated() to
context.User.Identity.IsAuthenticated
in the AntiforgeryRedirectIssueMiddleware class.
Super appreciated that you posted your solution. Now my students can click the login button as many times as they want and it doesn't matter.
@barelabs no worries. Glad to help!
Just to note though, the aspnet team have made the namespace Internal disappear and made the classes actually internal now and so the above workaround won't work for latest builds (core 3.0) -
I only found this out yesterday and so haven't come up with a replacement yet. If and when I do I will post in here.
@barelabs no worries. Glad to help!
Just to note though, the aspnet team have made the namespace Internal disappear and made the classes actually internal now and so the above workaround won't work for latest builds (core 3.0) -
I only found this out yesterday and so haven't come up with a replacement yet. If and when I do I will post in here.
@AnthonyDewhirst Thanks alot for sharing knowledge,
if u can share the link for this namespace or any link for this update I'd be thankful
Thanks again
@barelabs Sure:
https://github.com/aspnet/AspNetCore/blob/master/src/Antiforgery/src/Internal/IAntiforgeryTokenSerializer.cs - shows that the interface is now internal
4a5f2d16bbeab0e16f3786d5058872c013c686f9 is the commit that made this change
https://github.com/aspnet/AspNetCore/pull/8340 is the Issue that raised this and was then implemented
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Most helpful comment
hi @mostafahedawa,
I did, its a little bit of a hack. Basically, it looks like the a previous button hit succeeds in authenticating and then a later one attaches the token/cookie on to the later request, however it still has the original antiforgery token which thinks it shouldn't have an authenticated user, I do the following:
```c#
using System;
using System.Net;
using System.Threading.Tasks;
using IdentityServer4.Extensions;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Antiforgery.Internal;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace My.Middleware _logger;
{
///
/// The built in antiforgery token can sometimes have cause an issue where the user hits the login button several times in a row.
/// The affect is that the first request that succeeds has a new cookie on it which doesn't match the anti forgery token, thus
/// the subsequent requests fail. We catch and check for this just for login, and then allow a redirect if this is the case.
///
public class AntiforgeryRedirectIssueMiddleware
{
private readonly RequestDelegate _next;
private readonly IAntiforgeryTokenSerializer _antiforgeryTokenSerializer;
private readonly IAntiforgery _antiforgery;
private readonly IClaimUidExtractor _claimUidExtractor;
private readonly string _matchPath;
private readonly Uri _redirectUri;
private readonly ILogger
}
then make a call like so in startup after calls to ID4
c# app.UseAntiForgeryRedirectIssueSolution("/account/login", redirectUri);our login will round trip again and the user will not even see anything happening.
As this only happens after a successful call (as far as I can tell) I am quite happy to just make it look like the user logged in just fine.
You could always disable the button etc