angular4 httpclient csrf does not send x-xsrf-token
Asked Answered
S

3

27

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

Suki answered 4/9, 2017 at 16:17 Comment(6)
Do you use a CORS request?Ieshaieso
I am adding the headers "Access-Control-Allow-Headers","*"Suki
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?Ieshaieso
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?Suki
Yes we can make another interceptor which can do the same, but I think if it is mentioned in documentation, then HttpClient should do it.Suki
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.Ieshaieso
N
63

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 }
]
Neff answered 1/12, 2017 at 10:57 Comment(10)
Will it add the CSRF token to all requests or only data-changing requests (POST, PUT, DELETE)?Vermont
You acquire the CSRF token with GET, so even if you don't pass it, GET will return it for you (of course with the respect to user authentication) and from there on every request will carry it - since POST, PUT, DELETE, PATCH are changing data, having CSRF there is critical.Neff
What I meant is that with the code you provided all requests will have the token set, even GET request. I don't think that's intended though.Vermont
@Miroslav - I didn't understand your comment on absolute and relative path. In a web service, wouldn't path be always absolute? Otherwise, how would the client know where the server is (or does the client store server's info from response of initial GET)?Argillaceous
Your solution worked for me. Many thanks. I changed the url from absolute to relative. Though I don't understand why the behavior is different between absolute and relative. Could you please explain?Argillaceous
Relative means API is assumed to be located on the same domain (thus targeted only via relative/partial URL).Neff
The reasons why absolute urls aren't supported are documented here github.com/angular/angular/issues/18859Heroism
The mention about the absolute URL and example interceptor surely saved me loads of time. Thank you!Mischief
In addition to this answer, I also had to set the path of the header to / as mentioned here #50511498Mischief
This works on my development server but does not work on local. In my local machine angular app is available at localhost:4200 and spring boot app at localhost:8080Musset
A
7

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', })

Argillaceous answered 8/3, 2018 at 5:39 Comment(0)
O
0

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);
  }
}
Overly answered 24/5, 2018 at 16:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.