Hi everyone, I hope you're well.
I've spent the last two nights messing around with the anti-forgery request functionality. Specifically, I've been trying to figure out why I keep getting a 'Bad Request' response.
Nothing struck me as being overtly incorrect when using Chrome's developer tooling to examine the request and response headers (i.e., cookies were being set correctly, and the X-XSRF-TOKEN header was appended to POST requests).
For quite a while I was sending GET requests and wondering why Angular wasn't appending the aforementioned X-XSRF-TOKEN header. This one is my bad. 馃槃
When I figured out gotcha number 1, I was still perplexed as to why I got the 'Bad Request' response.
```C#
[AllowAnonymous]
[Route("login")]
public async Task
{
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
var result = await signInManager.PasswordSignInAsync("[email protected]", "Password", false, lockoutOnFailure: false);
var tokens = antiforgery.GetAndStoreTokens(HttpContext);
HttpContext.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, xsrfCookieOptions);
return Ok(result);
}
2. Send a POST request, with X-XSRF-TOKEN header, to a 'logout' API route. This code did the following:
1. Invoke SignOutAsync
2. Return a 200 OK response.
```C#
[HttpPost]
[ValidateAntiForgeryToken]
[Route("logout")]
public async Task<IActionResult> Logout()
{
await signInManager.SignOutAsync();
return Ok();
}
Exercise the 'login' endpoint, and review the cookies that get set.
Exercise the 'logout' endpoint.
This code should be invoked by a client in the following sequence:
This sequence results in the Antiforgery functionality working as expected when /logout is invoked, rather than the 400 Bad Request response.
```C#
[AllowAnonymous]
[Route("renew-xsrf-token")]
public IActionResult RenewXsrfToken()
{
var tokens = antiforgery.GetAndStoreTokens(HttpContext);
HttpContext.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, xsrfCookieOptions);
return Ok();
}
[AllowAnonymous]
[Route("login")]
public async Task
{
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
var result = await signInManager.PasswordSignInAsync("[email protected]", "Password", false, lockoutOnFailure: false);
return Ok(result);
}
[HttpPost]
[ValidateAntiForgeryToken]
[Route("logout")]
public async Task
{
await signInManager.SignOutAsync();
return Ok();
}
# The correct Angular code
Note the following sequencing in this TypeScript code:
1. Perform the login
2. In a separate GET request, renew the XSRF-TOKEN cookie
3. Perform the logout (now successful).
```TypeScript
public login() {
this.http.get('/login').subscribe(tokenRenewResult => {
this.http.get('/renew-xsrf-token').subscribe(loginResult => console.log(loginResult));
});
}
public logout() {
this.http.post('/logout', {}).subscribe(result => console.log(result));
}
Setting an identity by invoking a PasswordSignInAsync method and then immediately setting an XSRF-TOKEN cookie in the same request does not work.
It appears that the XSRF-TOKEN, even when set after invoking the PasswordSignInAsync method, does not recognise the newly signed in user immediately.
Demarcating this process by spanning it across two separate HTTP requests (i.e., the first request sets the user identity using the PasswordSignInAsync method, and a second request sets the XSRF-TOKEN cookie in the context of this newly signed in user) resolves the issue.
This issue was the give-away for me: https://github.com/aspnet/Antiforgery/issues/155
Specifically this quote:
After I login using my service the anti-forgery token returned is not valid as it was created based on a null user. I've tried setting ClaimsPrincipal after my PasswordSignInAsync and regenerating the anti-forgery token (see below) but that still does not work. Any ideas?
Could you consider updating the documentation such that it makes mention of the importance of:
I'd love to hear what you all think.
Kind regards,
Daniel Loth
Similar issue: https://github.com/aspnet/Antiforgery/issues/155
When I did a google search of GitHub repository code last night, all of the examples were using the same strategy to set XSRF-TOKEN cookies:
Example code using this pattern:
https://github.com/DanWahlin/Angular-ASPNET-Core-CustomersService/blob/master/modules/module5/files/beginFiles/Startup.cs (relevant code is commented out, so I suspect this programmer might have resorted to forgoing the CSRF protection)
https://github.com/damienbod/AspNetCoreMvcAngular/blob/master/AspNetCoreMvcAngular/Startup.cs
This would 'work' insofar that the following would occur in the context of a SPA (Single Page Application), such as an Angular application:
The first attempt to access an anti-forgery protected route would fail, but that failed request would potentially result in a renewal of the XSRF-TOKEN cookie (this time, the renewed version of the cookie would be created in the context of the now logged-in user) when the aforementioned pipeline handler executed
Subsequent requests would succeed when accessing anti-forgery protected routes.
This fundamental design flaw/issue has been around since the early days of MVC on ASP.NET. In short, the anti forgery token contains identity information, so if you get an anti forgery token and the identity of the user changes (anonymous to logged in, logged in to anonymous, user A to user B) then subsequent validations will fail.
@brockallen Right, that's what I'm doing with my JWT, but my point is that the XSRF token needs to be generated and returned with the login request, not on a subsequent request. I'm having a hard time figuring out how to do that.
After I get my JWT I need to figure out a way to login my user in so the context.HttpContext.User is set for
var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);
context.HttpContext.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions() { HttpOnly = false });
Which runs at the end of the request.
So I was able to figure out how to do it in one request. After the JWT is generated, you need to validate that JWT and it will return a ClaimsPrincipal which can then be set for the current user.
// Generate JWT
// Validate JWT
ClaimsPrincipal principal = validator.ValidateToken(jwt, validationParameters, out validatedToken);
this.HttpContext.User = principal;
I've found a workaround without the need for an extra roundtrip (/renew-xsrf-token). Inspired by https://github.com/aspnet/Antiforgery/issues/155#issuecomment-318271448
I'm using netcoreapp2.1
public class AccountController : Controller
{
private readonly UserManager<User> _userManager;
private readonly SignInManager<User> _signInManager;
private readonly IUserClaimsPrincipalFactory<User> _principalFactory;
public AccountController(
UserManager<User> userManager,
SignInManager<User> signInManager,
IUserClaimsPrincipalFactory<User> principalFactory)
{
_userManager = userManager;
_signInManager = signInManager;
_antiforgeryprincipalFactory = principalFactory;
}
[HttpPost]
[AllowAnonymous]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> Login(LoginModel model)
{
var result = await _signInManager.PasswordSignInAsync("[email protected]", "Password", false, lockoutOnFailure: false);
await ForceSetXsrsTokens();
return Ok(result);
}
private async Task ForceSetXsrsTokens()
{
// set principle on HttpContext.User as m_SignInManager.PasswordSignInAsync doesnt do it
// username is automatically part of tokens
var user = await _userManager.FindByEmailAsync("[email protected]");
var principle = await _principalFactory.CreateAsync(user);
HttpContext.User = principle;
var tokens = _antiforgery.GetAndStoreTokens(HttpContext);
Response.Cookies.Append(
"XSRF-TOKEN",
tokens.RequestToken,
new CookieOptions()
{
HttpOnly = false,
IsEssential = true
});
}
}
I have a fairly simple approach working for ASP.Net 2.1.
I'm not totally happy with it, because it feels like manually setting HttpContext.User is rather .... hacky.
I created a ResultFIlterAction which will set the cookie after Controller Actions have run.
Inspiration: https://github.com/aspnet/Home/issues/2415#issuecomment-354674201
public class AntiforgeryCookieResultFilterAttribute : ResultFilterAttribute
{
protected IAntiforgery Antiforgery { get; set; }
public AntiforgeryCookieResultFilterAttribute(IAntiforgery antiforgery) => this.Antiforgery = antiforgery;
public override void OnResultExecuting(ResultExecutingContext context)
{
var tokens = this.Antiforgery.GetAndStoreTokens(context.HttpContext);
context.HttpContext.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions() { HttpOnly = false });
}
}
And I hooked that up in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddAntiforgery(options =>
{
options.HeaderName = "X-XSRF-TOKEN";
});
services.AddTransient<AntiforgeryCookieResultFilterAttribute>();
services
.AddMvc(options =>
{
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
options.Filters.AddService<AntiforgeryCookieResultFilterAttribute>();
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
{...}
}
And finally a line needs adding to the Login and Logout actions to make sure the HttpContext.User is set.
Login: HttpContext.User = await signInManager.CreateUserPrincipalAsync(user);
Logout: HttpContext.User = new ClaimsPrincipal();
There is an MS "recommended" approach, which is to wait for ASP.Net 2.2 and use something that isn't available yet.
Thanks for contacting us.
@javiercn, can you please look into this? Thanks!
The general guiding principle with authentication in ASP.NET Core is that the identity of the user doesn't change within a given request but only between requests. For example, when a user signs in with Identity, a cookie gets added to the response, but the Principal in the HttpContext doesn't change. During sign-in, a user is unauthenticated for the entirety of the request, it's not up until the next requests that the user becomes authenticated. The way this affects antiforgery is as follows. When login in the following things happen:
When implementing an API endpoint for login the same general flow needs to be follow.
Thank you very much for clarifying that.
It's very useful to have that information available.
Good point about disallowing CORS.
A question...
Why is second redirect necessary?
The first is required for the HttpContext to be updated, but at that point it now has everything it needs to proceed, or am I missing something?
@AndyCJ It's not required, but normally you'll log in the user, redirect to another endpoint to update the antiforgery tokens and then either return or redirect again to the original place where the user wanted to go.
This is usually the case when you click a link for an endpoint that requires authentication on normal web applications.
Closing this issue as the question has been answered and there's no more action to be taken here.
Most helpful comment
The general guiding principle with authentication in ASP.NET Core is that the identity of the user doesn't change within a given request but only between requests. For example, when a user signs in with Identity, a cookie gets added to the response, but the Principal in the HttpContext doesn't change. During sign-in, a user is unauthenticated for the entirety of the request, it's not up until the next requests that the user becomes authenticated. The way this affects antiforgery is as follows. When login in the following things happen:
When implementing an API endpoint for login the same general flow needs to be follow.
It is important to note that you should either completely disallow CORS on the login/antiforgery endpoints or to at least configure it for the most restrictive settings.