Step 1
Add a middleware to your middleware pipeline that generates an AntiforgeryToken, and embeds the token in a non-http-only cookie that's attached to the response:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAntiforgery(options => {
options.HeaderName = "X-XSRF-TOKEN";
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IAntiforgery antiforgery)
{
...;
app.Use((context, next) => {
var tokens = antiforgery.GetAndStoreTokens(httpContext);
httpContext.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions() { Path = "/", HttpOnly = false });
});
}
}
I created a little package for this that contains this middleware.
Step 2
Configure your angular app to read the value of the non-http-only cookie (XSRF-TOKEN
) through javascript, and pass this value as a X-XSRF-TOKEN
header for requests sent by the HttpClient
:
@NgModule({
declarations: [...],
imports: [
HttpClientModule,
HttpClientXsrfModule.withOptions({
cookieName: 'XSRF-TOKEN',
headerName: 'X-XSRF-TOKEN'
}),
...
],
providers: [...],
bootstrap: [AppComponent]
})
export class AppModule { }
Step 3
Now you can decorate your controller methods with the [ValidateAntiforgeryToken]
attribute:
[ApiController]
[Route("web/v1/[controller]")]
public class PersonController : Controller
{
private IPersonService personService;
public PersonController(IPersonService personService)
{
this.personService = personService;
}
[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public async Task<ActionResult<Person>> Post([FromBody] Person person)
{
var new_person = await personService.InsertPerson(person);
return Ok(new_person);
}
}
Step 4
Make sure the requests you're sending have the following type of url as stated here:
/my/url
//example.com/my/url
Wrong url:
https://example.com/my/url
Note
I use Identity Cookie authentication:
services.AddAuthentication(/* No default authentication scheme here*/)
Since the ASP.NET Core Authentication
middleware only looks after the XSRF-TOKEN
header, and not the X-XSRF-TOKEN
cookie, you're no longer susceptible to Cross-site request forgery.
Spoiler
You will notice that right after signing in/out, the first webrequest that's being sent will still be blocked by XSRF protection. This is because the Identity does not change during the lifetime of the webrequest. So when sending the Login
webrequest, the response will attach a cookie with a csrf-token. But this token is still generated with the identity from when you weren't signed in yet.
The same counts for sending the Logout
webrequest, the response will contain a cookie with a csrf-token as if you're still signed in.
To solve this, you have to simply send another webrequest that does literally nothing, every time you've signed in/out. During this request you'll once again have the correct Identity in order to generate the csrf-token.
logoutClicked() {
this.accountService.logout().then(() => {
this.accountService.csrfRefresh().then(() => {
this.activeUser = null;
});
}).catch((error) => {
console.error('Could not logout', error);
});
}
Same for login
this.accountService.login(this.email, this.password).then((loginResult) => {
this.accountService.csrfRefresh().then(() => {
switch (loginResult.status) {
case LoginStatus.success:
this.router.navigateByUrl(this.returnUrl);
this.loginComplete.next(loginResult.user);
break;
default:
this.loginResult = loginResult;
break;
}
});
});
The contents of the csrfRefresh
method
public csrfRefresh() {
return this.httpClient.post(`${this.baseUrl}/web/Account/csrf-refresh`, {}).toPromise();
}
Server-side
[HttpPost("csrf-refresh")]
public async Task<ActionResult> RefreshCsrfToken()
{
// Just an empty method that returns a new cookie with a new CSRF token.
// Call this method when the user has signed in/out.
await Task.Delay(5);
return Ok();
}
This is where I login the user in my own app