0

Angular sometime fails to add XSRF token, so we added interceptor do double check and add token when missing. Then we found it is failing to read cookie sometimes, so we added 3rd party library to read cookies.. but we are still facing prod errors where XSRF token is missing on 1st attempt, especially when there is redirect from another site to our side, subsequent refresh of page is working fine.

One of our theories is that cookies are not yet set i.e. a race condition where angular read of cookies is running way before cookies are actually set.. thought amount traffic hitting this error is low, we need to address those errors and reduce customer frustration.

Now we would like to hold requests for few milliseconds then read cookies, even then if cookies are not found, we would like to perform a 2nd /ws/bootstrap call.

I am having hard time to understand or come up with a code inside this interceptor. Any suggestions or references are much appreciated.

Meanwhile I will try to post my modified code in comments, but that's pretty much non working code as of now.

import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpXsrfTokenExtractor } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { ILoggerInstance, LoggerService } from '@foo/logger-angular';
import { CookieService } from 'ngx-cookie-service';
import { Observable } from 'rxjs';

// Angular adds XSRF header only for Post, below code adds for all GET (i.e. any halHttp call)
// https://github.com/angular/angular/issues/20511
@Injectable()
export class HttpXsrfInterceptor implements HttpInterceptor {
  private readonly log: ILoggerInstance;
  private readonly xsrfHeaderName = 'X-XSRF-TOKEN';
  private readonly xsrfCookieName = 'XSRF-TOKEN';

  constructor(
    private readonly tokenExtractor: HttpXsrfTokenExtractor,
    private readonly cookieService: CookieService,
    private readonly logger: LoggerService
  ) {
    this.log = this.logger.getInstance('HttpXsrfInterceptor');
  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (!req.headers.has(this.xsrfHeaderName)) {
      // We have seen header missing in prod for approx 10% traffic, so adding non angular way of retrieving cookie if angular failed for some reason.
      const token = this.tokenExtractor.getToken() || this.cookieService.get(this.xsrfCookieName);
      if (token) {
        req = req.clone({ headers: req.headers.set(this.xsrfHeaderName, token) });
      } else if (req.url !== 'ws/bootstrap' && req.url !== 'bootstrap') {
        // Exclude bootstrap it issues xsrf cookie
        this.log.error('Missing xsrf cookie');
      }
    }
    return next.handle(req);
  }
}

kiranutt
  • 21
  • 5
  • what library are you using? I mean library with `tokenExtractor`? also do you expect gracefull solution, or just the easiest to implement? – Andrei Jan 11 '21 at 17:38
  • Updated code with full code, '@angular/common/http' and 'ngx-cookie-service'. Easiest solution is fine as long as its in interceptor and don't have to touch hundreds of files, i can work on it to make it graceful ! – kiranutt Jan 11 '21 at 21:31
  • Here our final code, error have come down from few thousands to single digit. – kiranutt Oct 02 '21 at 05:06

2 Answers2

0

i could propose you an easy hack to make cheap "polling" of the value:

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
   const req$ = req.url === 'ws/bootstrap' || req.url === 'bootstrap'
      ? of(req)
      : this.addXsrfToken(req);

    return req$.pipe(switchMap(newReq => next.handle(newReq)));
  }

addXsrfToken(request: HttpRequest<any>) {
   return timer(0, 100).pipe( // polling every 100msec
     map(() => this.tokenExtractor.getToken()), // try to read token
     first(token => !!token), // first token that is not null
     map(token => req.clone({ headers: req.headers.set(this.xsrfHeaderName, token) })) // will be converted to event
   );
}

this is pretty extensible if you plan to make fine solution. you should really replace timer().(first) to your logic. the rest will stay the same. Ideally you would want to handle your token value asynchroniously. in that case value will be an event to send the request

Andrei
  • 10,117
  • 13
  • 21
  • Thank you will try this, 1st will see if i can hold of app as we have this bootstrap call in APP_INITIALIZER – kiranutt Jan 14 '21 at 22:31
0

Ideally unnecessary angular should have worked out of box, but it did not for whatever external browser/network combinations.

Here's our final code, errors have come down from few thousands to single digit.

    import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpXsrfTokenExtractor } from '@angular/common/http';
    import { Injectable } from '@angular/core';
    import { ILoggerInstance, LoggerService } from '@xyz/logger-angular';
    import { Observable } from 'rxjs';
    import { BootstrapService } from '../services/bootstrap.service';
    
    @Injectable()
    export class HttpXsrfInterceptor implements HttpInterceptor {
      private readonly log: ILoggerInstance;
      private readonly xsrfHeaderName = 'X-XSRF-TOKEN';
    
      constructor(
        private readonly tokenExtractor: HttpXsrfTokenExtractor,
        private readonly logger: LoggerService,
        private readonly bootstrapService: BootstrapService
      ) {
        this.log = this.logger.getInstance('HttpXsrfInterceptor');
      }
    
      intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        if (!req.headers.has(this.xsrfHeaderName)) {
          // We have seen header missing in prod for approx 10% traffic, so adding non angular way of retrieving cookie if angular failed for some reason.
          const token = this.tokenExtractor.getToken() || this.bootstrapService.getXsrfToken();
          if (token) {
            req = req.clone({ headers: req.headers.set(this.xsrfHeaderName, token) });
          } else if (req.method === 'POST' && !req.url.endsWith('bootstrap')) {
            // Exclude GET and /ws/bootstrap as it issues xsrf cookie
            this.log.error('Missing xsrf cookie');
          }
        }
        return next.handle(req);
      }
    }

kiranutt
  • 21
  • 5