Angular 8 suppress HTTP calls before authentication is complete

Viewed 716

So I have an API call for the header section of my page which is a part of app.component.html. And then I have several screens in my routing module which has canActivate. Only routing to these screens can trigger auth.guard which will then authenticate. But since this menu code is inside app.component.html. A call is being made even before I have a token and I have a 401 error in console.log. Is there a way to prevent this call until authentication is over ie I have refresh, id, and access tokens?

app-routing module

const routes: Routes = [
    { path: 'abc', component:abcComponent , canActivate: [AuthGuard] },
    {path: 'xyz', component:xyzComponent , canActivate: [AuthGuard] }
];

app.component.ts

ngOnIt(){
    this.httpService.getMenu().subscribe((response) => {
        this.menuArray=response
    });
}
export class TokenInterceptorService implements HttpInterceptor {
    private isRefreshing = false;
    private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);

    constructor(private injector: Injector) {}
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        if (req.url == environment.authTokenPostUrl) {
            const tokenreq = req.clone({});
            return next.handle(tokenreq);
        } else {
            const tokenreq = this.addToken(req);

            return next.handle(tokenreq).pipe(
                catchError((error) => {
                    const cookie = this.injector.get(CookieService);
                    if (error.status === 401) {
                        return this.handle401Error(tokenreq, next);
                    } else {
                        return throwError(error);
                    }
                })
            );
        }
    }

    private addToken(request: HttpRequest<any>) {
        const authservice = this.injector.get(AuthService);
        const headers = { Authorization: `Bearer ${authservice.setIdToken()}` };
        return request.clone({
            setHeaders: headers,
        });
    }

    private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
        if (!this.isRefreshing) {
            const authservice = this.injector.get(AuthService);
            this.isRefreshing = true;
            this.refreshTokenSubject.next(null);

            return authservice.refreshToken().pipe(
                switchMap((token: any) => {
                    this.isRefreshing = false;
                    this.refreshTokenSubject.next(token.id_token);
                    return next.handle(this.addToken(request));
                })
            );
        } else {
            return this.refreshTokenSubject.pipe(
                filter((token) => token != null),
                take(1),
                switchMap((jwt) => {
                    return next.handle(this.addToken(request));
                })
            );
        }
    }
}


So I tried

ngOnIt(){
    if(this.cookieService.get('refresh_token'))
        this.httpService.getMenu().subscribe((response) => {
            this.menuArray=response
        });
    }
}

But this logic is not working because authentication is happening after this call is being triggered.

Is there a way to tackle this problem?

1 Answers

While RxJS solution might be more elegant (and I recommend you dig in), I'm gonna go against the flow and provide a more "old-school-angular" solution. It uses plain properties, no $ signs in sight. This might be a good option for less experienced programmers while preserving maintainability.

In your AuthService, create a property with name authenticated. Set it to true once you authenticate.

class AuthService {
    authenticated = false;

    authenticate(params) {
      this.http.post([authentication stuff]).subscribe(() => {
        // do what you do now
        this.authenticated = true;
   }
}

Then, separate your code for menu inside a separate <app-menu> component.

class MenuComponent {
  ngOnInit() {
    this.httpService.getMenu().subscribe((response) => {
        this.menuArray=response
    });
  }
}

In main AppComponent, link the auth service and proxy the authenticated property through getter.

class AppComponent {
  get authenticated() { return this.authService.authenticated; }
  constructor(authService: AuthService) {}
}

Then, in AppComponents template use *ngIf to show the menu component.

<app-menu *ngIf="authenticated"></app-menu>

The alternative is to use an authenticated$ BehaviourSubject, but I'll leave that to other answers.

Related