Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting ValidateAntiForgeryToken working with Angular 4 and Asp.Net Core 2

I am currently attempting to get security working properly with Angular 4 (4.3.5) on the newly released Asp.Net Core 2.0, and specifically anti forgery tokens.

I am using JavascriptServices, which provides the starter application (its the default Angular template in Visual Studio 2017.3). Javascript services hosts the main page of the Angular site on a .cshtml page. This actually turns out to be quite beneficial, as I can then lock everything down using standard forms authentication (dot net core Identity), which redirects the user to a separate (non Angular) login page at /Account/Login when the user is not logged in. You can then log in on that page and get redirected to the home page and the spa is up and running within the context of the authorised user.

That working application may be found here.

The final piece of the puzzle is the get the ValidateAntiForgeryToken attributes working. This is fine when you log in to the Account/Login page, as it is not running in the context of angular 4. But when I am running within Angular 4 on the home page, when I make a post back to the server, the post will be blocked by the ValidateAntiForgeryToken if that attribute is present.

Because of this I have commented out the ValidateAntiForgeryToken attribute on the Account/Logout method. This is because I am logging out from the site using an Angular http post. It works when the attribute is not being used, but it fails/is blocked when it is used.

Following the Angular 4 documention, found here, I have changed the Anti Forgery Token name to match what Angular 4 recognises. To do this, I modified my Startup.cs file, adding in some lines, as follows:

public void ConfigureServices(IServiceCollection services)
{
            services.AddAntiforgery(options =>
            {
                options.Cookie.Name = "XSRF-TOKEN";
                options.Cookie.HttpOnly = false;
            });
...
}

This should enable the Angular app to access the Anti Forgery cookie with the name Angular 4 expects.

Within my app, I have just changed over to use the new HttpClient service (apparently the Http service has been deprecated!) which is supposed to use an interceptor to send the XSRF_TOKEN automatically to the server.

But I have been unable to make this work.

I tried a standard post call using the HttpClient service:

this.httpClient.post(this.baseUrl + 'Account/Logout', "", options).subscribe(result => {
    location.replace("/");
}, error => {
    console.error(error);
})

I tried adding headers manually:

let token = this.cookieService.get("XSRF-TOKEN");
console.log(token);

var httpHeaders = new HttpHeaders({ 'XSRF-TOKEN': token })

this.httpClient.post(this.baseUrl + 'Account/Logout', "", { headers: httpHeaders }).subscribe(result => {
    location.replace("/");
}, error => {
    console.error(error);
})

I tried using the old service both with and without added headers:

let token = this.cookieService.get("XSRF-TOKEN");
        console.log(token);

        let headers = new Headers({
            //'Content-Type': 'application/json',
            'X-XSRF-TOKEN': token
        });
        let options = new RequestOptions({ headers: headers });
        this.http.post(this.baseUrl + 'Account/Logout', "", options).subscribe(result => {
            location.replace("/")
        }, error => console.error(error));

Unfortunately, I've had no luck. Has anyone else managed to get this working?

like image 429
tone Avatar asked Oct 29 '22 03:10

tone


2 Answers

Ok, I've figured out a solution.

My Index.cshtml page now looks like this:

@Html.AntiForgeryToken()

<app>Loading...</app>

<script src="~/dist/vendor.js" asp-append-version="true"></script>
@section scripts {
    <script src="~/dist/main-client.js" asp-append-version="true"></script>
}

What this does is generate an anti-forgery token on the server side, and place it in a hidden input field on the page. When you view page source, that hidden input field looks like this:

<input name="__RequestVerificationToken" type="hidden" value="CfDJ8DaEnvKVNL9EhPVzHKQWhC-PeT4eNm_svdTEyGZje4WnH34sBfG_D_AphtPzBM1JEkQUHsSX1KWBivxAOtPsOvfMKs5N_dLn0Sr3xRG-N2s0oFaa3-yvG87qdzXYm1yBSYH7dlRiBu5It3wi2iYzWqyo4B1i_iRtmikz41gmuldze8VE72zVqmeHZav5rQiHkw" />

In my Logout method, I obtain the token and submit it in the headers to the server-side controller. The name of the header is RequestVerificationToken, no underscores necessary.

Logout() {

    let token: any = $("input[name=__RequestVerificationToken]").val();
    if (token !== null) {
        var httpHeaders: any = new HttpHeaders({ 'RequestVerificationToken': token });

        this.httpClient.post("/Account/Logout", null, { headers: httpHeaders }).subscribe(() => {
            window.location.replace("/Account/Login");
        }, error => {
            console.log(error);
        });
    }
}

On the server-side, the AntiForgery filter runs, compares it to the submitted value, and if it is the value expected, will allow the server side Account/Logout method to be executed.

The server-side method looks like this:

//
// POST: /Account/Logout
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout()
{
  await _signInManager.SignOutAsync();
  return RedirectToAction(nameof(HomeController.Index), "Home");
}

Putting a break point in the Logout method will prove that it executes.

There might be a gotcha here. I am not sure whether the token changes on each request. I haven't done any testing on this. If it does, then a new token will need to be added to the page after each request.

Also, I did not need to modify the behaviour of the default cookie. I am not doing this the Angular 4 way. It is purely the ASP.Net approach. That is, in the ConfigureServices method in the Startup.cs file, I have commented out the Cookie changes:

public void ConfigureServices(IServiceCollection services)
        {
            //services.AddAntiforgery(options =>
            //{
            //    options.Cookie.Name = "XSRF-TOKEN";
            //    options.Cookie.HttpOnly = false;
            //});

If you think you've found a better way of doing this, by all means, please post your solution.

like image 102
tone Avatar answered Nov 13 '22 22:11

tone


Had the same problem here. My solution:

  • Adding a Func to the request pipeline like that (F#):

    member this.Configure(app: IApplicationBuilder, env: IHostingEnvironment, appLifetime : IApplicationLifetime, antiforgery : IAntiforgery) =
    let tokenMiddleware = fun (context : HttpContext) (next: Func<Task>) ->
                              let path = context.Request.Path.Value
                              if path <> null && not (path.ToLower().Contains("/api")) then
                                  let tokens = antiforgery.GetAndStoreTokens(context)
                                  context.Response.Cookies.Append("XSRF-TOKEN", 
                                      tokens.RequestToken, CookieOptions (
                                                               HttpOnly = false, 
                                                               Secure = false
                                                           )
                                  )
                              next.Invoke ()
    app
        .UseStaticFiles()
        .UseIdentityServer()
        .Use(tokenMiddleware)
    

The point here is to set secure to false, otherwise the script code won't be able to fetch the cookie.

  • In Angular I had to set the header options manually

    login(login: UserLogin, completed: () => void, failed: (message: string) => void) {
    const token = this.cookieService.get('XSRF-TOKEN');
    const httpHeaders = (token) ? new HttpHeaders({ 'X-XSRF-TOKEN': token }) : null;
    
    this.http.post(this.apiUrl() + AccountServiceService.Login_Url, login, { headers: httpHeaders })
    

Works ok for me. HIH

like image 25
user2878120 Avatar answered Nov 14 '22 00:11

user2878120