Search code examples
asp.netasp.net-mvcangularcsrf-protection

Unexpected behaviour with extended BaseRequestOptions in Angular2 App


I have an angular2 app which communicates with an ASP.Net MVC backend. The backend requires an AntiForgeryToken to be present on POST requests.

For this, my index.html is served with a hidden input where its value represents the token. I guess this is a common approach in an ASP.Net MVC project (not topic of this question).

Now to grab this token and use it in my request headers i extended BaseRequestOptions in my main.ts like this:

@Injectable()
class AntiForgeryRequestOptions extends BaseRequestOptions {
    constructor () {
        super();
        let xsrfElement = <HTMLInputElement>document.querySelector('[name=__RequestVerificationToken]');
        if (xsrfElement) {
            let xsrfToken:string = xsrfElement.value;
            this.headers.append('X-XSRF-Token', xsrfToken);
        }
        this.headers.append('Content-Type', 'application/json');
    }
}

and provided it as RequestOptions like this:

bootstrap(AppComponent, [
    ...
    provide(RequestOptions, {useClass: AntiForgeryRequestOptions}),
    ...
]);

With this set up, i can make POST-requests in my services like this:

post(url:string, body:any):Observable<any> {
        let options = new RequestOptions();

        return this._http.post(url, body, options)
            .map((response:any) => {
                ...
            })
            .catch(this._handleError);
    }

which works fine. The POST works and the CSRF Token is send with it.


The Problem

Today i had to adjust this, because i have to switch out the Token on some occassions and experienced some weird behaviour i can't explain:

In my understanding as soon as i do this: let options = new RequestOptions() the constructor of AntiForgeryRequestOptions should have been called and headers should be set for this RequestOptions i just creatd. But if i do a console.log(options) directly after the call, i see that all properties are NULL even the headers. (It still works, the CSRF Token is send, its content-type is application/json) but i can't access it or change it for the coming request.

Can anyone tell me why this is happening or how i would alter my previously defined RequestOptions in the constructor of my AntiForgeryRequestOptions?


Additional information:

There was a reason why i did it that way. When i started this project, angular2 was still in beta state (now i'm working with angular2 rc1). At that time there was an issue with CSRF Tokens where you can't set it directly in your options. Now with the new version i would not need to provide a custom AntiForgeryRequestOptions. I could leave that away and just do it like this in the post requests:

post(url:string, body:any):Observable<any> {
        let options = new RequestOptions();
        options.headers = new Headers();
        let xsrfElement = <HTMLInputElement>document.querySelector('[name=__RequestVerificationToken]');
        if (xsrfElement) {
            let xsrfToken:string = xsrfElement.value;
            options.headers.append('X-XSRF-Token', xsrfToken);
        }
        options.headers.append('Content-Type', 'application/json');

        ...
    }

Solution

As Günther suggested in his comment, i have to use merge in my custom AntiForgeryRequestOptions like this:

@Injectable()
class AntiForgeryRequestOptions extends BaseRequestOptions {
    constructor () {
        super();
    }

    merge(options?:RequestOptionsArgs):RequestOptions {
        options.headers = new Headers();
        let xsrfElement = <HTMLInputElement>document.querySelector('[name=__RequestVerificationToken]');
        if (xsrfElement) {
            let xsrfToken:string = xsrfElement.value;
            options.headers.append('X-XSRF-Token', xsrfToken);
        }
        options.headers.append('Content-Type', 'application/json');
        return super.merge(options);
    }
}

which works. Now everytime i call new RequestOptions() the Headers property is updated correctly, which means whenever i change the value of my hidden input field for the CSRF Token, the headers will adjust like they should.


Solution

  • You need to implement a custom merge(options?: RequestOptionsArgs): RequestOptions { ... } method because the default implementation just checks if headers were passed to merge(...) and then takes these or otherwise takes the headers of the current AntiForgeryRequestOptions instance but id doesn't actually merge.

    See also https://github.com/angular/angular/blob/f39c9c9e75671a7e235734b6b8aef263f6dff254/modules/%40angular/http/src/base_request_options.ts#L101

    Example (from the question - thanks for the permission)

    @Injectable()
    class AntiForgeryRequestOptions extends BaseRequestOptions {
        constructor () {
            super();
        }
    
        merge(options?:RequestOptionsArgs):RequestOptions {
            options.headers = new Headers();
            let xsrfElement = <HTMLInputElement>document.querySelector('[name=__RequestVerificationToken]');
            if (xsrfElement) {
                let xsrfToken:string = xsrfElement.value;
                options.headers.append('X-XSRF-Token', xsrfToken);
            }
            options.headers.append('Content-Type', 'application/json');
            return super.merge(options);
        }
    }
    

    See also https://stackoverflow.com/a/37550368/217408