I am developing a web API
app running using asp.net core2
and Angular
. The detailed development environment config is here.
I am trying to configure AntiForgeryToken
validation but it keeps failing. I followed the config. here, but I had to modify it as my angular app and asp.net servers are running on two different ports because the front end startup doesn't generate the token. I kick start the backend by calling an API
path (/api/Account/ContactInitialization
) at the app component ngOnInit
which allowed me to generate the token.
The config is shown below,
IServiceCollection
Service:
services.AddAntiforgery(options =>
{
options.HeaderName = "X-CSRF-TOKEN";
options.SuppressXFrameOptionsHeader = false;
});
and at IApplicationBuilder Configure
:
app.Use(next => context =>
{
string path = context.Request.Path.Value;
if (
string.Equals(path, "/", StringComparison.OrdinalIgnoreCase) ||
string.Equals(path, "/api/Account/ContactInitialization", StringComparison.OrdinalIgnoreCase) ||
string.Equals(path, "/index.html", StringComparison.OrdinalIgnoreCase))
{
// We can send the request token as a JavaScript-readable cookie,
// and Angular will use it by default.
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken,
new CookieOptions() { HttpOnly = false });
}
return next(context);
});
asp.net. generates two set of keys,
I decorated my method with [ValidateAntiForgeryToken]
and angular includes XSRF-TOKEN
cookie content in my header request. yet I keep receiving a 400 (Bad Request)
response after calling the API
! what am I missing here?
Controller Method,
[Authorize]
[ValidateAntiForgeryToken]
[HttpPost]
public IEnumerable<string> AutherizeCookie()
{
return new string[] { "Hello", "Auth Cookie" };
}
my detailed header request looks like below,
POST /api/Values/AutherizeBaseController HTTP/1.1
Host: localhost:4200
Connection: keep-alive
Content-Length: 2
Pragma: no-cache
Cache-Control: no-cache
Accept: application/json, text/plain, */*
Origin: http://localhost:4200
Authorization: **Bearer** eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhaG1hZCIsImp0aSI6IjJhMTA1NzJjLWY2MWMtNGQwNi05ZjEzLThmOWZlOTUyYjQ4NyIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3QvIiwiaWF0IjoxNTE1MDI4MzUzLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6ImNhYjIxYTdlLTMwZjAtNDJhNi05NGIyLWNiYjczN2MzNmNmMyIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiJhaG1hZCIsIkRpc3BsYXlOYW1lIjoiQWJ1IE1haXphciIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvc2VyaWFsbnVtYmVyIjoiZjgyZDdlZTgtOTA5ZC00MTMzLTlmZjUtMjczYTBjMzAxNDgyIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy91c2VyZGF0YSI6ImNhYjIxYTdlLTMwZjAtNDJhNi05NGIyLWNiYjczN2MzNmNmMyIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6WyJEYXRhYmFzZSBTdXBlciBVc2VyIiwiQWRtaW5pc3RyYXRvciJdLCJuYmYiOjE1MTUwMjgzNTIsImV4cCI6MTUxNTAzMTk1MiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdC8ifQ.Jhnh-vRSANR0_dDqzBOEwtG5BxI3V6pX28Zdc5IQZMw
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36
Content-Type: application/json
Referer: http://localhost:4200/dbapp
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cookie: .AspNetCore.Antiforgery.jY5Bh2HGy8s=CfDJ8MxOovOd259BlsEMLYWAq1gbumuwD_ifl51KgQeExpEGd81dp7zarQ2mZ0C4ZexLTiPJXNSbrCR06mzi2OxqeXh4ZcrQGuo-K4yyIYG3bUhIy1RiXdA6IuIjcYP13ylyWJeNJ7FiOzm3c5AGvsF2YTk; XSRF-TOKEN=CfDJ8MxOovOd259BlsEMLYWAq1gds0ipk9X5sh3kqK20RnvPBSoo5x5EpzzGyncHjx3-PXX40Orc9NUUlpTejlL8RWTDGB9QUuOk_PCWudkYXyiqdebgcnfuQNQt_2fJkhyijawUSGsX9HmATMsZrHWvGOk; .AspNetCore.Identity.Application=CfDJ8MxOovOd259BlsEMLYWAq1gHJUrf-RKdswXaTtdaDrRIDxhC1FejiDQPMXNMUkox-DIzcIblxOgwwkg-EdQd6q0B0BQd4V5G2tTdncGJWrfqaBwbhVCCIOFNxsJZ8Px0KiSeaL_chJ27VN9M3dSnhIm30gGVN8IEzzhA8JicJUooK2O4Dr4OCzBuYvJKez4A35nnO8XFxFmncSq5vc18hhgH4-CFNKPRT24t1XgN8s_FpV2hdNtSoqMVFlKvwQaiDieM3tLRWsYnjpvDprmCr-MPTNA5oCBEVE56vOc-gViAPPBrA_AVDH0EaOBOBFVrBP2UFUOHE0wz3y8jsHIOCRNOeowyQnPsBMobywd1bmioTi2LTX0tk4FbC_Y3kE7n4BkggOQiOE-XaXifoNoxaWPQTkWtClr-hVWW1orQZ7hDUIA_VyGxsrOetEd1KMLMVQD_fGAOrBBm4qJZvGFGqq1RWgAGdjf3c15Lrpj4DrSDyF7nICyn_z2LOQkXTQ81VByrqAy4C_boSbCjAO1YlpnBKJpVkrsUPr8szj6kwilumwhSB-PXAYU7Z8nVM3XcgKIvRWDYxLqS8AQZU7-Z4roMmAYlRrqUziEcpnSL5LNJgMUaBnFESpxFpnXmyOiFEKp6-W0f02uwrRhn__eP8vidr27cANwxghDxP92V-b8Wya5Ogbqw_DMmNYweEx1jbAYAqJOhF8Jx_QFoGIUQT8EEeAqsaCNIrMVVQsTWfqRkD_t4RDOaRF5zguTmKl5r_MQcM6xyOtj-UtzhWU8RzetumCQ4i2z7-_qnCLfpuEax3BrdTGMlkVUas__Up6-l_A
Is it possible that the issue is in your AddAntiforgery where you use X-CSRF-TOKEN
instead of X-XSRF-TOKEN
?
@kichalla any idea about this?
Couple of things I noticed here
X-XSRF-TOKEN
in options (which is what angular sends on seeing the XSRF-TOKEN
cookie).X-XSRF-TOKEN
header (even though there is a XSRF-TOKEN
cookie), this tells me you are making a cross site request? (based on the referrer
, host
and origin
headers). I think this is working as expected. @Eilon, do you agree?Ah, interesting findings, I think you are right.
@masterjs , @Eilon , @kichalla , Thank you guys for chiming in. I added X-XSRF-TOKEN
header and somehow started to work again (I will explain why!). However, I am confused. according to the documentation, Angular is supposed to add the XSRF-TOKEN
cookie automatically, which it does, but there is nothing mentioned about adding the X-XSRF-TOKEN
header explicitly, is it because I am using HttpClient API from Angular?
I am using HttpInterceptor and I updated the code as below,
if (token) {
const headers = new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.authService.getToken()}`,
'X-XSRF-TOKEN': `${this.authService.getCookie('XSRF-TOKEN')}`
});
const cloneReq = req.clone({ headers });
Now, generating the token at startup
based on invoking an API
as mentioned above, for some reason, renders the token as invalid. so I modified the code by removing the if block
.
app.Use(next => context =>
{
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken,
new CookieOptions()
{
HttpOnly = false,
Secure = true // I am currently using it because I enforced SSL
});
return next(context);
});
I noticed the following,
There is a kind of conflict between Antiforgery
and JwtBearer Auth scheme
. In my project, I am using asp Identity
cookie and JwtBearer
as means of auth. whenever I add JwtBearer Auth scheme
to the controller, the antiforgery token is invalidated.
[HttpPost]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[Authorize]
[ValidateAntiForgeryToken]
public IEnumerable<string> AutherizeBaseController()
{
return new string[] { "Hello", "Auth Base controller" };
}
The token is generated every time an HTTP
event takes place, is this a normal behavior?
Token is still invalid at login, I can only use it after I login, what am I still doing wrong here?
I am confused by the details here. Can we separate the issues?
(I am assuming you are using 2.0.0 version of ASP.NET Core MVC)
Antiforgery validation runs after authentication.
Questions:
ValidateAntiForgeryToken
?@kichalla
I am using version 2 of ASP.NET Core MVC, My authentication is working fine, with either of both attributes. I tested my controller with [Authorize]
or [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
or both and I receive 200 response no issues. once I add [ValidateAntiForgeryToken]
the controller sometimes respond other times give me a 400 bad request.
Where is the best place to generate the token at? can I generate it at login?
var antiForgeryToken = antiforgery.GetTokens(HttpContext).RequestToken;
HttpContext.Response.Cookies.Append("XSRF-TOKEN", token, new CookieOptions { HttpOnly = false, Secure = true });
I am working through a proxy too, my angular app is served on a server different from dotnet
Today after testing ValidateAntiForgeryToken
is not working
@embryologist I would like to understand your application setup clearly.
I am trying to configure AntiForgeryToken validation but it keeps failing. I followed the config. here, but I had to modify it as my angular app and asp.net servers are running on two different ports because the front end startup doesn't generate the token. I kick start the backend by calling an API path (/api/Account/ContactInitialization) at the app component ngOnInit which allowed me to generate the token.
Based on the above comment from the first post in this thread, you have 2 apps on different ports. One an asp.net webapi app and other an angular app. Your login UI from angular app posts data to the asp.net webapi app? (have you enabled CORS on the asp.net webapi app to allow requests from the angular app?)
Basically you need to refresh the XSRF-TOKEN once the user is authenticated, but I would like to understand your application setup better.
@kichalla, your questions, and comments helped a lot, now it is working.
to answer your questions. yes, it is correct, my web API
is served on a different port and yes I have my CORS
enabled. I think my problem was mis-config in the latter.
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());
});
and I decorate my controller
,
[Route("/api/[controller]/[action]")]
[EnableCors("CorsPolicy")]
public class AccountController : ArtCoreSecuredController ....
and what did the trick is that I call the token generating API
that I mentioned above at the time of login which allows refreshing the token after login.
one question though before closing the issue,
with the following middleware config, I noticed that I am getting a newXSRF-TOKEN
with every Http request
made to the server, is this a normal behavior or I will be running into issues later on?
in another word, shall I generate the token only once at the time of login and stick with it?
ps, if I uncomment the code below, I get a fixed token and does not refresh at each Http request
```
app.Use(next => context =>
{
string path = context.Request.Path.Value;
// if (
// string.Equals(path, "/", StringComparison.OrdinalIgnoreCase) ||
// string.Equals(path, "/api/Account/ContactInitialization", StringComparison.OrdinalIgnoreCase) ||
// string.Equals(path, "/index.html", StringComparison.OrdinalIgnoreCase))
// {
// We can send the request token as a JavaScript-readable cookie,
// and Angular will use it by default.
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken,
new CookieOptions() { HttpOnly = false, Secure = true });
// }
return next(context);
});
```
Thank you
I notice that you appear to be using a bearer token;
Authorization': `Bearer ${this.authService.getToken()}`,
If this is your sole method of authentication then CSRF protection is _not_ needed. CSRF arises when authentication automatically flows, when it's cookie based, or uses basic authentication, or kerberos. If you are specifically adding the authorization header on every request, which you can only do within javascript, then a normal POST request which comes from elsewhere isn't going to have this header added, so all attempts to call the API from outside of the web site are either going to be blocked by same origin policy, or CORS.
Now if you have an HTML form somewhere that then triggers the AJAX calls then that form would have to be protected.
Closing this issue now as we do not have any action item here.
Most helpful comment
I notice that you appear to be using a bearer token;
If this is your sole method of authentication then CSRF protection is _not_ needed. CSRF arises when authentication automatically flows, when it's cookie based, or uses basic authentication, or kerberos. If you are specifically adding the authorization header on every request, which you can only do within javascript, then a normal POST request which comes from elsewhere isn't going to have this header added, so all attempts to call the API from outside of the web site are either going to be blocked by same origin policy, or CORS.
Now if you have an HTML form somewhere that then triggers the AJAX calls then that form would have to be protected.