26
votes

In angular documentation, it is mentioned that the angular httpclient will automatically send the value of cookie XSRF-TOKEN in the header X-XSRF-TOKEN of post request. Documentation link

But it does not send the header for me. Here is my code

Nodejs code to set the cookie

router.get('/set-csrf',function(req,res,next){
    res.setHeader('Set-Cookie', "XSRF-TOKEN=abc;Path=/; HttpOnly; SameSite=Strict");    
    res.send();
  })

I have used the httpclient in app.module.ts

imports: [
  HttpClientModule
]

** The above code is just for debug purpose. I do not have a set-csrf endpoint.

But it does not send any header when I send a post request. I am not able to debug.

I have added the issue in the github repository of angular too. HttpXsrfInterceptor checks if the request is GET or HEAD, or if it starts with http. If true, it skips adding the header.

Here is the code in HttpXsrfInterceptor class

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);
    }
    const token = this.tokenService.getToken();

    // Be careful not to overwrite an existing header of the same name.
    if (token !== null && !req.headers.has(this.headerName)) {
      req = req.clone({headers: req.headers.set(this.headerName, token)});
    }
    return next.handle(req);
  }

I am not sure why they have skipped for http/s part. Here is my issue in github

3
Do you use a CORS request? - Alexandr
I am adding the headers "Access-Control-Allow-Headers","*" - netuser
I saw your issue. It seems to me that angular should process http[s] links in different way. For example store last csrf token for each domain separately. Maybe is it good to make another Interceptor winch handles csrf in such way? - Alexandr
One thing which I didn't understand (I have mentioned it in github issue also) that why do http urls require special handling? Because niether the owasp guide nor the wikipedia article mention any such case. You mentioned storing csrf token for each domain, can you explain me bit more of this http/https and multiple domains? I guess this only applies to mutiple domains scenario(sub domains), but how is it related to urls starting with http/https? - netuser
I mean that if url starts with http/https angular app calls backend on different resource, not the same on which angular app loaded. It can be subdomain scenario or even absolutely different site which allows CORS access. So if it different domains each of them can't know each other and they send csrf tokens separately. So if angular app see url starts with http it should not send csrf token obtained from url not started with http because it can lead to disclosure token. Instead of this app should track csrf tokens splitted by domains and sends tokens to that domain from which it was obtained. - Alexandr

3 Answers

61
votes

What you are looking for is HttpClientXsrfModule.

Please read more about it here: https://angular.io/api/common/http/HttpClientXsrfModule.

Your usage should be like this:

imports: [   
 HttpClientModule,  
 HttpClientXsrfModule.withOptions({
   cookieName: 'My-Xsrf-Cookie', // this is optional
   headerName: 'My-Xsrf-Header' // this is optional
 }) 
]

Additionally, if your code targets API via absolute URL, default CSRF interceptor will not work out of the box. Instead you have to implement your own interceptor which does not ignore absolute routes.

@Injectable()
export class HttpXsrfInterceptor implements HttpInterceptor {

  constructor(private tokenExtractor: HttpXsrfTokenExtractor) {
  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const headerName = 'X-XSRF-TOKEN';
    let token = this.tokenExtractor.getToken() as string;
    if (token !== null && !req.headers.has(headerName)) {
      req = req.clone({ headers: req.headers.set(headerName, token) });
    }
    return next.handle(req);
  }
}

And finally add it to your providers:

providers: [
  { provide: HTTP_INTERCEPTORS, useClass: HttpXsrfInterceptor, multi: true }
]
7
votes

I suppose the correct method is withOptions. I used withConfig and got error Property 'withConfig' does not exist on type 'typeof HttpClientXsrfModule'. This is a typing issue in the documentation. You need to use "withOptions" instead HttpClientXsrfModule.withOptions({ cookieName: 'My-Xsrf-Cookie', headerName: 'My-Xsrf-Header', })

0
votes

Using the recent Angular version I ran into the following problem. While the token is passed to the client using the header name 'XSRF-TOKEN', the response must feed back the token using the header name 'X-XSRF-TOKEN'. So here is a slightly modified version of Miroslav's code above which works for me.

@Injectable()
export class HttpXSRFInterceptor implements HttpInterceptor {

  constructor(private tokenExtractor: HttpXsrfTokenExtractor) {
  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const headerName = 'XSRF-TOKEN';
    const respHeaderName = 'X-XSRF-TOKEN';
    let token = this.tokenExtractor.getToken() as string;
    if (token !== null && !req.headers.has(headerName)) {
      req = req.clone({ headers: req.headers.set(respHeaderName, token) });
    }
    return next.handle(req);
  }
}