-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Description
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).
The gotchas
-
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.
The problem code
- Send a GET request to a 'login' API route. The code in the method did the following:
- Sign out the IdentityConstants.ExternalScheme
- Sign in a user
- Append the XSRF-TOKEN cookie
- Return a 200 OK response.
[AllowAnonymous]
[Route("login")]
public async Task<IActionResult> Login()
{
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
var result = await signInManager.PasswordSignInAsync("test@test.com", "Password", false, lockoutOnFailure: false);
var tokens = antiforgery.GetAndStoreTokens(HttpContext);
HttpContext.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, xsrfCookieOptions);
return Ok(result);
}
- Send a POST request, with X-XSRF-TOKEN header, to a 'logout' API route. This code did the following:
- Invoke SignOutAsync
- Return a 200 OK response.
[HttpPost]
[ValidateAntiForgeryToken]
[Route("logout")]
public async Task<IActionResult> Logout()
{
await signInManager.SignOutAsync();
return Ok();
}
Reproducing the issue
-
Exercise the 'login' endpoint, and review the cookies that get set.
- Outcome: Identity cookie set, Antiforgery cookie set, XSRF-TOKEN cookie set.
-
Exercise the 'logout' endpoint.
- Expected outcome: Identity cookie unset, 200 OK response
- Actual outcome: 400 Bad Request response.
The correct ASP.NET Core code
This code should be invoked by a client in the following sequence:
- Send GET request to /login endpoint
- Send GET request to /renew-xsrf-token endpoint
- Send POST request to /logout endpoint.
This sequence results in the Antiforgery functionality working as expected when /logout is invoked, rather than the 400 Bad Request response.
[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<IActionResult> Login()
{
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
var result = await signInManager.PasswordSignInAsync("test@test.com", "Password", false, lockoutOnFailure: false);
return Ok(result);
}
[HttpPost]
[ValidateAntiForgeryToken]
[Route("logout")]
public async Task<IActionResult> Logout()
{
await signInManager.SignOutAsync();
return Ok();
}
The correct Angular code
Note the following sequencing in this TypeScript code:
- Perform the login
- In a separate GET request, renew the XSRF-TOKEN cookie
- Perform the logout (now successful).
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));
}
Root cause
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: aspnet/Antiforgery#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?
Proposal
Could you consider updating the documentation such that it makes mention of the importance of:
- Setting an XSRF-TOKEN cookie in the context of a logged in identity
- Having a clear demarcation between first logging the user in, and then renewing their XSRF-TOKEN in the context of that newly logged-in user (i.e., demarcation via separate HTTP request, rather than trying to set all cookies in a single request).
I'd love to hear what you all think.
Kind regards,
Daniel Loth