I have a .net core app using angularJS, and I want to protect the api calls protected by our cookie based authentication. I Followed the steps in this article:
https://learn.microsoft.com/en-us/aspnet/core/security/anti-request-forgery
I added the services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");
to my services configuration
I am seeing the XSRF-TOKEN
cookie in my developer tools when loading the page.
I am seeing the X-XSRF-TOKEN
header being added to my $http sent requests.
I have added the [AutoValidateAntiforgeryToken]
to my controller that is handling the ajax request.
I have ssl enabled, and am accessing the pages via https.
I can make GET requests fine through this api endpoint as expected. However, I am receiving a 400 error without any details of why the request was bad on PUTs and POSTs.
I know the X-XSRF-TOKEN
is on the request, (seen in the network tab of chrome dev tools) so I am unsure what I am missing to allow these requests to be received correctly.
TL;DR: Why is .net core rejecting my valid AntiforgeryToken?
UPDATE Attempted @joey's suggested solution, but it did not work, still receiving 400 responses. code below reflects another solution I tried to fix this problem (aka angularjs's solution to setting default cookies and headers for cross site scripting protection)
I have also attempted to configure AngularJS to change what the cookie and header names match what I configured in my Startup.cs
. I changed their names to try both XSRF-TOKEN
(cookie) and X-XSRF-TOKEN
(header) as well as CSRF-TOKEN
and X-CSRF-TOKEN
, and while the configurations within angular is correctly using the new default to what ever I provide, my authentication code in .net core is still not working.
for more information here is how i am configuring AngularJS:
app.config(function ($httpProvider) {
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-TOKEN';
$httpProvider.defaults.xsrfCookieName = 'CSRF-TOKEN';
});
here is the ConfigureServices
line I have in my Startup.cs
file:
services.AddAntiforgery(options => options.HeaderName = "X-CSRF-TOKEN");
and lastly here is the code I added to the Configure
method of the Startup.cs
file:
app.Use(next => context =>
{
string path = context.Request.Path.Value;
if (path.Contains("/MyProtectedPath/"))
{
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("CSRF-TOKEN", tokens.RequestToken,
new CookieOptions { HttpOnly = false });
}
return next(context);
});
UPDATE 2: I have added the following to my controller action:
if (HttpContext.Request.Method.ToLower(CultureInfo.InvariantCulture) != "get")
{
await _antiforgery.ValidateRequestAsync(HttpContext);
}
AntiforgeryValidationException: The provided antiforgery token was meant for a different claims-based user than the current user.
I thought maybe antiforgery.GetAndStoreTokens(context)
might be overriding the current cookie sent on the page load, so I make it only hit that code on GET requests, but I get the same result for the posts.
Update 3 Thanks @joey for your help, I have looked at my startup.cs file, and I have the UseAuthentication in a seperate branch:
app.Use(next => context => //note: order of these use statements is key. keep this antiforgery section above auth!
{
string path = context.Request.Path.Value;
if (path.ToLower(CultureInfo.InvariantCulture).Contains("/campaigns/proxy/"))
{
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken,
new CookieOptions { HttpOnly = false });
}
return next(context);
});
// Use Basic Auth for /api
app.UseWhen(context => context.Request.Path.StartsWithSegments(new PathString("/api")), branch =>
{
branch.UseBasicAuth();
});
// Use Identity for everything except /api
app.UseWhen(context => !context.Request.Path.StartsWithSegments(new PathString("/api")), branch =>
{
app.UseAuthentication(); // required for .net core 2.0 authentication
});
I am not sure why my tokens would be considered invalid at this point...
The most common approach to defending against CSRF attacks is to use the Synchronizer Token Pattern (STP). STP is used when the user requests a page with form data: The server sends a token associated with the current user's identity to the client. The client sends back the token to the server for verification.
To prevent CSRF attacks, use anti-forgery tokens with any authentication protocol where the browser silently sends credentials after the user logs in. This includes cookie-based authentication protocols, such as forms authentication, as well as protocols such as Basic and Digest authentication.
The XSRF request verification session token is stored as an HTTP cookie and currently contains the following information in its payload: A security token, consisting of a random 128-bit identifier.
MVC's anti-forgery support writes a unique value to an HTTP-only cookie and then the same value is written to the form. When the page is submitted, an error is raised if the cookie value doesn't match the form value. It's important to note that the feature prevents cross site request forgeries.
In the code you have posted, you are adding the header X-XSRF-TOKEN
, but the header should be X-CSRF-TOKEN
.
The example from the web page you linked provides the example:
services.AddAntiforgery(options => options.HeaderName = "X-CSRF-TOKEN");
Update:
Thanks for clarifying with the additional code & information. The means of implementing CSRF selected here is one which passes the token as a header on the response of the initial HTML file. Here is an example of how such a case might be configured for a SPA web application:
app.Use(next => context =>
{
string path = context.Request.Path.Value;
if (
string.Equals(path, "/", StringComparison.OrdinalIgnoreCase) ||
string.Equals(path, "/index.html", StringComparison.OrdinalIgnoreCase)
)
{
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("CSRF-TOKEN", tokens.RequestToken,
new CookieOptions() { HttpOnly = false });
}
return next(context);
});
However, with your service configured to using the String.Contains method [1], antiforgery.GetAndStoreTokens(context)
is invoked for any path containing /MyProtectedPath/
anywhere. This means it has been configured in such a way that matches not only /MyProtectedPath/
, but also /MyProtectectedPath/a/b/c
or /a/b/c/MyProtectedPath/
.
To check the CSRF token sent in subsequent requests of an applicable HTTP method as shown below:
if (string.Equals("POST", context.Request.Method, StringComparison.OrdinalIgnoreCase))
{
await antiforgery.ValidateRequestAsync(context);
// The line above will throw if the CSRF token is invalid.
}
If the method GetAndStoreTokens
is called before this for any matching path, the token will be overwritten before it is checked, which is why .net examples will typically order GetAndStoreTokens
first, but with the specific condition for looking at the path and HTTP method.
[1] https://msdn.microsoft.com/en-us/library/dy85x1sa(v=vs.110).aspx
This is the configuration that worked for me,
I have CORS
enabled,
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder
.WithOrigins("https://www.artngcore.com:4200") //Note: The URL must be specified without a trailing slash (/).
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
then the add the following to your services at startup,
services.AddAntiforgery(options =>
{
options.HeaderName = "X-XSRF-TOKEN";
options.SuppressXFrameOptionsHeader = false;
});
and the middleware,
app.UseAuthentication();
app.Use(next => context =>
{
string path = context.Request.Path.Value;
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken,
new CookieOptions() { HttpOnly = false,
Secure = true // set to false if not using SSL });
return next(context);
});
and in the controller,
[Route("/api/[controller]/[action]")]
[EnableCors("CorsPolicy")]
[Authorize]
public class AccountController : Controller ....
configure your header as following,
const headers = new HttpHeaders({
'Content-Type': 'application/json',
'X-XSRF-TOKEN': `${this.cookieService.get('XSRF-TOKEN')}`
});
Finally, what does the trick is that the token has to be refreshed after login (authentication), what I did is just call an API path just to invoke the token to refresh, i.e., call this API after login,
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ContactInitialization()
{
await Task.Delay(1);
return StatusCode(200);
}
Hope it will help,
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With