Aspnetcore: Clarity around IAntiforgery and ValidateAntiForgeryToken

Created on 16 Jan 2018  路  12Comments  路  Source: dotnet/aspnetcore

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

  1. 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. 馃槃

  2. When I figured out gotcha number 1, I was still perplexed as to why I got the 'Bad Request' response.

The problem code

  1. Send a GET request to a 'login' API route. The code in the method did the following:

    1. Sign out the IdentityConstants.ExternalScheme

    2. Sign in a user

    3. Append the XSRF-TOKEN cookie

    4. Return a 200 OK response.

```C#
[AllowAnonymous]
[Route("login")]
public async Task Login()
{
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();
}

Reproducing the issue

  1. Exercise the 'login' endpoint, and review the cookies that get set.

    1. Outcome: Identity cookie set, Antiforgery cookie set, XSRF-TOKEN cookie set.
  2. Exercise the 'logout' endpoint.

    1. Expected outcome: Identity cookie unset, 200 OK response
    2. Actual outcome: 400 Bad Request response.

The correct ASP.NET Core code

This code should be invoked by a client in the following sequence:

  1. Send GET request to /login endpoint
  2. Send GET request to /renew-xsrf-token endpoint
  3. 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.

```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 Login()
{
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 Logout()
{
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));
}

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: 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?

Proposal

Could you consider updating the documentation such that it makes mention of the importance of:

  1. Setting an XSRF-TOKEN cookie in the context of a logged in identity
  2. 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

area-mvc question

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:

  1. The user visits the login page.
  2. The server produces an antiforgery token pair, attaches one of them to form elements and another one to the cookie in the request.
  3. The user introduces the login/password and submits the form.
  4. The browser sends the login, password and antiforgery token to the server.
  5. The server validates the antiforgery token (for the anonymous user), the login and password, signs-in the user (emits the cookie) and redirects the user to some other page (let's say Index for simplicity).
  6. The browser follows the redirect and issues a get request to the location included in the response (with the cookie attached).
  7. The server determines that the antiforgery token is out of date and produces a new one for the current user (now authenticated). Any new rendered form gets the new antiforgery token.

When implementing an API endpoint for login the same general flow needs to be follow.

  1. Sign in the user.
  2. Redirect to a separate endpoint to update the antiforgery tokens with the new Identity.
  3. Redirect again to the final destination to produce the final response.
    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.

All 12 comments

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:

  1. Append a handler in the request pipeline that set the XSRF-TOKEN cookie when some arbitrary predicate held true (e.g.: when the route was equal to "/" or "/index", or perhaps on every request)
  2. Rely on that handler to set the XSRF-TOKEN.

Example code using this pattern:

  1. https://github.com/spboyer/webapi-antiforgery/blob/master/Startup.cs
  1. 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)

  2. 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:

  1. User logs in
  1. 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

  2. Subsequent requests would succeed when accessing anti-forgery protected routes.

@Lothy Let's say I'm an attacker and when you visit my page with your cookie I call renew-xsrf-token. That will give me a XSRF token. Can't I then go ahead and make requests? The attacker shouldn't be able to generate XSRF tokens.

This is similar to 2415 & 83 but they haven't found a solution yet.

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:

  1. The user visits the login page.
  2. The server produces an antiforgery token pair, attaches one of them to form elements and another one to the cookie in the request.
  3. The user introduces the login/password and submits the form.
  4. The browser sends the login, password and antiforgery token to the server.
  5. The server validates the antiforgery token (for the anonymous user), the login and password, signs-in the user (emits the cookie) and redirects the user to some other page (let's say Index for simplicity).
  6. The browser follows the redirect and issues a get request to the location included in the response (with the cookie attached).
  7. The server determines that the antiforgery token is out of date and produces a new one for the current user (now authenticated). Any new rendered form gets the new antiforgery token.

When implementing an API endpoint for login the same general flow needs to be follow.

  1. Sign in the user.
  2. Redirect to a separate endpoint to update the antiforgery tokens with the new Identity.
  3. Redirect again to the final destination to produce the final response.
    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.

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

farhadibehnam picture farhadibehnam  路  3Comments

markrendle picture markrendle  路  3Comments

rbanks54 picture rbanks54  路  3Comments

githubgitgit picture githubgitgit  路  3Comments

Kevenvz picture Kevenvz  路  3Comments