DEV Community

Cover image for Authentication in Angular: The Circular Dependency in DI issue popping its ugly head
Ayyash
Ayyash

Posted on • Edited on • Originally published at garage.sekrab.com

Authentication in Angular: The Circular Dependency in DI issue popping its ugly head

Picking up where we left off, the first use case for our Auth service is a header token. The best way to add a header token is via an Http interceptor. Let's start.

Since we are injecting the AuthService anyway, and using AppModule, it does not make much difference to use HttpInterceptorFunction instead of the goold ol' HttpClientModule. Down the line it will be more evident that it is indeed a better choice.

You can read about Angular 15 standalone HTTPClient provider.

Follow along on StackBlitz

In our App Module provider array, we add another entry for the interceptor:

// app.module
@NgModule({
    // ...  
    providers: [
        {
        provide: HTTP_INTERCEPTORS,
        multi: true,
        useClass: AppInterceptor,
      },
        // ...
    ]
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

The interceptor injects the AuthService immediately to use it. Let me add a console log as the first line.

// services/http HttpInterceptor
@Injectable()
export class AppInterceptor implements HttpInterceptor {
  constructor(private authState: AuthState) {
    console.log('interceptor injected');
  }
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // prefixing the api with proper value, mostly from config
    // remote config url are expected to be filtered out, it would not make sense
    const url = 'https://saphire.sekrab.com/api' + req.url;

    const adjustedReq = req.clone({
      url: url,
      setHeaders: this.getHeaders(),
    });

    return next.handle(adjustedReq);
  }
  private getHeaders(): any {
    // TODO authorization here
    let headers: any = {};

    return headers;
  }
}
Enter fullscreen mode Exit fullscreen mode

Before we add our header, let's remember our sequence of events: assuming we are using Http call to get remote configuration, which normally has the correct API URL, it is clear that we need to filter out the configuration URL. In this example, I am not calling a remote URL for configurations, but it's good to know that the interceptor should check the req.url and filter out the ones it does not wish to deal with.

// simple check to exclude local data or config url
if (req.url.indexOf('config') > -1) {
    // pass through
    return next(req);
}
Enter fullscreen mode Exit fullscreen mode

The Circular Dependency in DI issue

Error: NG0200: Circular dependency in DI detected for InjectionToken HTTP_INTERCEPTORS.

Have you ever seen this? It occurs when you inject a service in another service, that injects it back in itself. In our case, the AuthService and HttpClient kind of inject each other.

In addition to those two services, the configuration service that uses Http is also injected in AuthService. It's a mess no matter how you look at it.

But here is the thing that will kill you before you reach midlife. Since we are not making any use of the HttpClient in the AuthService constructor, this tumor is benign. If however we do initiate an Http call in the constructor, that's when it blows up in our faces.

There are many fixes, most of them are around delaying the Http call a bit just to make sure the AuthService has been constructed. Like waiting for the remote configuration to be ready. But that isn't a clean cut solution.

That settles it then, as a general rule: avoid Http requests in your service constructors. Especially those injected early on.

If indeed you need to inject a service that calls an Http in its constructor (next week we'll have a use case for that), break your services apart, and spread them around in your app.

AuthState service

To clean up and be more systematic as we move forward let's move all non Http related methods to their own service. The AuthState is the service that will hold the Observable state, and contain no references to HttpClient. The constructor is responsible for reading the LocalStorage information, and it has the GetToken new method to return the token.

// services/auth.state

@Injectable({ providedIn: 'root' })
export class AuthState {
  // create an internal subject and an observable to keep track
  private stateItem: BehaviorSubject<IAuthInfo | null> = new BehaviorSubject(
    null
  );
  stateItem$: Observable<IAuthInfo | null> = this.stateItem.asObservable();

  constructor() {
    // simpler to initiate state here
    // check item validity
    console.log('authState in');
    const _localuser: IAuthInfo = JSON.parse(localStorage.getItem('user'));
    if (this.CheckAuth(_localuser)) {
      this.SetState(_localuser);
    } else {
      this.Logout();
    }
  }
// also move here: SetState  RemoveState CheckAuth  Logout
}
Enter fullscreen mode Exit fullscreen mode

Now the AuthService is much simpler, it only has the Login, and it uses AuthState to save into localStorage. We will enhance this later when we use a proper localStorage wrapper.

So now we need to create a GetToken method, to retrieve the access token, then use it in HttpInterceptor

// services/auth.state
// add this new method
GetToken() {
  const _auth = this.stateItem.getValue();
  // check if auth is still valid first before you return
  return this.CheckAuth(_auth) ? _auth.accessToken : null;;
}
Enter fullscreen mode Exit fullscreen mode

We will add the logic for checking the token later. Then let's use it in the interceptor

// update http file to fill out get headers
private getHeaders(): any {
  //  authorization here
  let headers: any = {};
    const _auth = this.authState.GetToken();
    if (_auth && _auth !== '') {
      headers['authorization'] = `Bearer ${_auth}`;
    }
    return headers;
}
Enter fullscreen mode Exit fullscreen mode

In Module-full solution, everything is a private member of our Class. In standalone, everything is a loose variable. Which way do you prefer?

401 Refresh

What happens when we get a 401? We can either show the user out, or use our refresh token to get a new access token. In Angular, this probably is one of the tasks that left me bruised for a while. Here is the sequence of events:

  • Catch a 401 (which 401)
  • Create a new Http call with refresh token, and request a new access token
  • Wait for response
  • Update localStorage
  • Resubmit original request (retry)
  • Return and have a nice life
  • Else logout
  • Catch another concurrent 401, queue and wait

So let's first modify the Http function to catch 401, and call a function for it.

// services/http

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

    return next.handle(adjustedReq).pipe(
      catchError(error => {
         // if this is really an http error
         if (error instanceof HttpErrorResponse
            // and of 401 status
            && error.status === 401
          ){
                    // handle 401 error, return an observable to continue the pipe
          return this.handle401Error();
         }
         // rethrow error, to be caught elsewhere
         return throwError(() => error);
      })
    );
  }

  private handle401Error(): Observable<any> {
    // let's first try to submit a refresh access token request
    // return authService.RefreshToken()
    // switchMap when done to resubmit the req passed, using next.handler
    // catchError means it is not working, rethrow and logout
  }
Enter fullscreen mode Exit fullscreen mode

What we want to do now, is fill out the handle401Error function. First, it looks like we need a RefreshToken method in AuthService (not AuthState). Which means we need to inject that as well. Remember: AuthService has no Http calls in the constructor.

// services/auth.service
// add RefreshToken method
RefreshToken(): Observable<boolean> {
  return this.http
    .post(this._refreshUrl, { token: this.authState.GetRefreshToken() })
    .pipe(
      map((response) => {
        // this response has the new refresh token and access token
       if (!response) {
          // something terrible happened
          throw(new Error('Oh oh'));
        }

        // update session
        const retUser: IAuthInfo = <IAuthInfo>(<any>response).data;
        // we'll be more selective later...
        localStorage.setItem('user', JSON.stringify(retUser));

        this.authState.SetState(retUser);

        return true;
      })
    );
}
Enter fullscreen mode Exit fullscreen mode

Back to our handle401Error function

// services/http
// update handle401Error function, also, inject AuthService in the constructor
private handle401Error(
    // pass in orginalReq and handler
    originalReq: HttpRequest<any>,
  next: HttpHandler
): Observable<any> {
    return this.authService.RefreshToken().pipe(
      switchMap((result: boolean) => {
        if (result) {
          // token saved (in RefreshToken), now recall the original req after adjustment
            // so we need to pass "next" handler, and "originalReq"
          return next.handle(originalReq.clone({setHeaders: this.getHeaders()}));
        }
      }),
      catchError(error => {
        // else refresh token did not work, its bigger than both of us
        // log out and throw error
        this.authState.Logout();
        return throwError(() => error);
      })
    );
}
Enter fullscreen mode Exit fullscreen mode

We adjust the signature to pass in the originalReq and the next handler:

// services/http
// adjust call
return next.handle(adjustedReq).pipe(
  catchError((error) => {
        // ...
      return this.handle401Error(adjustedReq, next);
    }
        // ...
  })
);
Enter fullscreen mode Exit fullscreen mode

Testing this, the first issue is the /login point. If it 401's, there is no need to retry, it just means bad credentials. So the handler must filter out /login point

// services/http filter out login from handler401Error
return next.handle(adjustedReq).pipe(
  catchError((error) => {
    // if this is really an http error
    if (
      error instanceof HttpErrorResponse &&
      // and of 401 status
      error.status === 401 &&
      // filter out login calls
      req.url.indexOf('login') < 0
    ) {
      return this.handle401Error(adjustedReq, next);
    }
    // rethrow error
    return throwError(() => error);
  })
Enter fullscreen mode Exit fullscreen mode

Testing this by making a call in some page, and hard coding few things on my test server, this is what I get of logging out the sequence:

Console log

So you can see that the original request was recalled, with the proper access token. (Don't mind the value 'access_token_1', it's dummy date.)

You want to produce similar colorful log? Read Taming the console

Locking and unlocking

We're not done yet. Let's create an example usage to see the problem that comes out of this. We are going to make two requests in parallel. This means that while the first request is trying to refresh token, the second request comes in, and it might request a new token as well, screwing up the original token. Here is the dummy log, that does not break the system, because well, its dumb:

Console log out

Notice the following:

  • Two 401 errors were thrown, that's expected
  • Two calls to refresh token, with the same refresh token, one should work, the other should not
  • Response comes in with new access token, in my example it's the same, because it's dumb. In real life there will be two different access tokens, one must fail (if it hadn't already)

To fix that we need to lock, queue, then unlock.

It's straightforward to lock and unlock, using a private Boolean member:

// services/http
// add lock boolean
@Injectable()
export class AppInterceptor implements HttpInterceptor {
  // if refreshing token, it is busy, lock
  isBusy: boolean;
  private handle401Error(
    originalReq: HttpRequest<any>,
    next: HttpHandler
  ): Observable<any> {

    if (!this.isBusy) {
            // lock
      this.isBusy = true;

      return this.authService.RefreshToken().pipe(
      // ...
                finalize(() => {
                    // unlock
          this.isBusy = false;
       })
      );
    } else {
      // return unadjusted, for now
      return next.handle(originalReq);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now with this, one call would retry, and all other would fail. We need to just wait a bit until the token is ready, before we adjust and recall all other ones. To do that, we can have a private member to keep track of the successful token. Once it's ready, flush out.

Console log

The most widely accepted solution to this is using a Subject of Boolean and piping on it. It updates when locking, and when token is ready.

// services/http update to allow subject queuing
@Injectable()
export class AppInterceptor implements HttpInterceptor {

    // create a subject to queue outstanding refresh calls
  recall: Subject<boolean> = new Subject();
 // ...
 private handle401Error(...): Observable<any> {
   if (!this.isBusy) {
    // ...
    // progress subject to false
    this.recall.next(false);
    return this.authService.RefreshToken().pipe(
      switchMap((result: boolean) => {
        if (result) {
          // progress subject to true
          this.recall.next(true);
                // ... return next.handle
        }
      }),
      // ...
    );
  } else {
    // return the subject, watch when it's ready, switch to recall original request
    return this.recall.pipe(
      filter(ready => ready === true),
      switchMap(ready => {
         // try again with adjusted header
         return next.handle(originalReq.clone({ setHeaders: this.getHeaders() }));
      })
     );
    }
}
Enter fullscreen mode Exit fullscreen mode

I tried to break it, but I couldn't. If you run into scenarios where it is acting up, let me know please.

Side point

You might be tempted to stop an outgoing request if the access token is not valid (expired). Don't. That is an API decision. Some points do not need an access token (like /login), some are flexible in returning less data if token is not valid.

Enhance

One enhancement we can add is to redirect user to login page if the refresh token fails.

Another enhancement is in the login resolve. We can now save the URL that caused the redirection in the auth state, and try to redirect to it after login. That and one more rant about user account details is coming up next week. 😴

Thank you for reading this far, did you break the 401 handler?

RESOURCES

Top comments (0)