HttpXsrfInterceptor and HttpXsrfCookieExtractor implementation example

In this example I demonstrate the implementation of cookie extraction and interception in Angular (5, 6, 7).

The application server (tomcat) implements Spring Security with CSRF token response with the following permission for HEAD requests:

import com.allanditzel.springframework.security.web.csrf.CsrfTokenResponseHeaderBindingFilter;
...
@Configuration
@EnableWebSecurity
public class AppSecurityConfig extends WebSecurityConfigurerAdapter {
...
@Override
protected void configure(HttpSecurity http) throws Exception {
CsrfTokenResponseHeaderBindingFilter csrfTokenFilter = new CsrfTokenResponseHeaderBindingFilter();
http.addFilterAfter(csrfTokenFilter, CsrfFilter.class);
http
.antMatchers(HttpMethod.HEAD, "/_app").permitAll()
...

(“_app” is the context root of the application)

For the authentication process we need to intercept the request, adding the X-CSRF-TOKEN coming back from the server, using the following 2 classes HttpXsrfInterceptor to add the headers needed and the HttpXsrfCookieExtractor to extract the token (file: app-xhr-manipulation-ts):

import { InjectionToken } from ‘@angular/core’;
import { Observable } from ‘rxjs’;
import { HttpHandler } from ‘@angular/common/http’;
import { HttpInterceptor } from ‘@angular/common/http’;
import { HttpRequest } from ‘@angular/common/http’;
import { HttpEvent } from ‘@angular/common/http’;
import { HttpXsrfTokenExtractor } from ‘@angular/common/http’;
import { Inject, Injectable, OnInit, PLATFORM_ID } from ‘@angular/core’;
import { HttpClient } from ‘@angular/common/http’;
import {DOCUMENT, ɵparseCookieValue as parseCookieValue} from ‘@angular/common’;

export const XSRF_COOKIE_NAME: InjectionToken<string> = new InjectionToken<string>(‘X-CSRF-HEADER’);
export const XSRF_HEADER_NAME: InjectionToken<string> = new InjectionToken<string>(‘X-CSRF-TOKEN’);

@Injectable()
export class HttpXsrfCookieExtractor implements HttpXsrfTokenExtractor {
private lastCookieString: string = ;
private lastToken: string|null = null;
private parseCount: number = 0;
private MAX_RETRIES: number = 5;

constructor(
@Inject(DOCUMENT) private doc: any, @Inject(PLATFORM_ID) private platform: string,
@Inject(XSRF_COOKIE_NAME) private cookieName: string, private httpClient: HttpClient) {}

//make a HEAD request to retrieve the cookie
headRequest(): Promise<any> {

let promise = new Promise((resolve, reject) => {
console.log(“head request:”);
this.httpClient.head(“/_app/”, {observe: “response”})
.toPromise()
.then(
response => {
console.log(“headRequest resolved”);
this.lastCookieString=response.headers.get(this.cookieName);//”X-CSRF-TOKEN”
this.lastToken=response.headers.get(this.lastCookieString);
resolve();
}
).catch(
err => {
console.log(“headRequest rejected”);
reject();
}
);
});

return promise;
}

getToken(): string|null {

if (this.platform === ‘server’) {//it should be ‘browser’
return null;
}

do {
console.log(“retrying HEAD”);
this.parseCount++;
console.log(“expecting the promise: headRequest()”);
this.headRequest();
} while(this.lastToken==null && this.parseCount < this.MAX_RETRIES);

return this.lastToken;
}
}

@Injectable()
export class HttpXsrfInterceptor implements HttpInterceptor {

constructor(
private tokenService: HttpXsrfTokenExtractor,
@Inject(XSRF_HEADER_NAME) private headerName: string) {}

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

const lcUrl = req.url.toLowerCase();
// Skip both non-mutating requests and absolute URLs.
// Non-mutating requests don’t require a token, and absolute URLs require special handling
// anyway as the cookie set
// on our origin is not the same as the token expected by another origin.
/*
if (req.method === ‘GET’ || req.method === ‘HEAD’ || lcUrl.startsWith(‘http://’) ||
lcUrl.startsWith(‘https://’)) {
return next.handle(req);
}
*/
if(req.method === ‘HEAD’){
return next.handle(req);
}

const token = this.tokenService.getToken();

console.log(“the token: “ + token);
console.log(“the URL: “ + lcUrl);
console.log(“the header name: “ + this.headerName);
console.log(“the request method: “ + req.method);

if (token !== null && !req.headers.has(this.headerName)) {
req = req.clone({headers: req.headers
.set(this.headerName, token)
.set(‘X-Requested-With’, ‘XMLHttpRequest’)
.set(‘X-Login-Ajax-call’,‘true’)
.set(‘Content-Type’,‘application/x-www-form-urlencoded’)
});
}
return next.handle(req);
}
}

In the app-module.ts we need to declare these classes:

HttpClientModule,
HttpClientXsrfModule.withOptions({
cookieName: ‘X-CSRF-HEADER’,
headerName: ‘X-CSRF-TOKEN’,
})
],
providers: [LoggingService,
RestService,
AppService,
HttpXsrfCookieExtractor,
HttpXsrfInterceptor,
{ provide: HTTP_INTERCEPTORS, useClass: HttpXsrfInterceptor, multi: true },
{ provide: HttpXsrfTokenExtractor, useClass: HttpXsrfCookieExtractor },
{ provide: XSRF_COOKIE_NAME, useValue: ‘X-CSRF-HEADER’ },
{ provide: XSRF_HEADER_NAME, useValue: ‘X-CSRF-TOKEN’ }
],
bootstrap: [AppComponent]
})

The idea behind this implementation is, at the authentication time to intercept the request and send first a HEAD request to the server to get the X-CSRF-COOKIE and then add the cookie plus some extra required headers, so the authentication will be accepted.

Leave a Reply