Search code examples
angulartypescriptauthenticationjwtrefresh-token

Unable to get newest token before component loaded in angular


I need relevant suggestions to achieve my need which is this :

I implemented a jwt based authentication system in my angular app. When i get my new token and refresh token, i create a setTimeout so that the token can be refreshed before its expiration. But I also purposely refresh my token when I bootstrap the app (in my AppComponent), and my issue is related to this case.

I get the logged in user infos from the jwt, and the issue is that when the app is being bootstraped and the token refresh is executed, the tokens are stored after the component is being displayed, which causes the app to get the users information of the previous token and not the newly created.

We could think of using resolvers, but the issue is that when using a resolver, i will have 2 refresh token queries being triggered, one in my AppComponent and one inside the resolver, which is not the behavior I need.

So basically, here's a part of my AppComponent :

     ngOnInit() {
        this.refreshToken();
        ...
      }
    
    private refreshToken() {
        this.authSvc.refreshToken().pipe(
          untilDestroyed(this),
          catchError(error => {
            if (error.status === 401) {
              this.router.navigate(['/login']);
            }
            return of();
          })
        ).subscribe();
      }

Here's a part of the AuthService :

    get user(): User | null {
      const token = localStorage.getItem('token');
      return token ? this.jwtHelper.decodeToken(token) : null;
    }

    refreshToken(): Observable<LoginResponse> {
        return this.http.post<LoginResponse>(
          `${environment.apiUrl}/token/refresh`,
          {refresh_token: localStorage.getItem('refresh_token')}, this.CONTEXT
        ).pipe(
          tap((res: LoginResponse) => this.storeTokens(res as LoginSuccess))
        );
      }
    
    storeTokens(data: LoginSuccess): void {
        localStorage.setItem('token', data.token);
        localStorage.setItem('refresh_token', data.refresh_token);
        this.scheduleTokenRefresh(data.token);
      }

And here's a component where I need the user data (it will not be the only component which will need this data ):

export class HomeComponent implements OnInit {
  user!: User | null;
  constructor(private authSvc: AuthService) {
  }

  ngOnInit() {
    this.user = this.authSvc.user;
  }

The issue I'm facing is that Home component is being displayed before storeTokens gets called so if the user data has been updated on the backend side since the last token, it my HomeComponent won't know it because it will use the previous token...

I tried using a resolver bug it forces me to call refreshToken again and it's not what I want. I need to keep my refreshToken inside the AppComponent and not call it twice.. here's the resolver :

export const tokenRefreshedResolver: ResolveFn<boolean> = (route, state) => {
  return inject(AuthService).refreshToken().pipe(
    map(() => true),
    catchError(() => of(false))
  );
};

What would be a suitable solution ?


Solution

  • I would strongly recommend you to use the APP_INITIALIZER provider to handle this. If you are not familiar with it, here's the documentation and here's my short explanation.

    APP_INITIALIZER can be used to execute desired functionality before the application is bootstrapped. This means that you should be able to obtain the token before anything starts to render, thus avoiding your issue. It's way better than handling it in the AppComponent.

    You need to add the APP_INITIALIZER provider in the AppModule and then implemented it the way you want. Common solution is to use the factory function for that.

    // AppModule
    import { APP_INITIALIZER, NgModule } from '@angular/core';
    import { AuthService } from 'somewhere/in/you/project';
    
    @NgModule({
      declarations: [
        AppComponent, AboutUsComponent,HomeComponent,ContactUsComponent
      ],
      imports: [
        HttpClientModule,
        BrowserModule,
        AppRoutingModule,
      ],
      // Add a custom provider
      providers: [ 
        {
          provide: APP_INITIALIZER,
          useFactory: appInit, // function returning a Promise or Observable
          deps: [AuthService] // dependencies needed for the factory func
        }
      ],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    
    // FOR STANDALONE APPLICATION
    // to it in bootstrapApplication instead of the AppModule
    bootstrapApplication(AppComponent, {
      providers: [
        // ... 
        {
          provide: APP_INITIALIZER,
          useFactory: appInit,
          multi: true,
          deps: [AuthService],
        },
      ],
    }).catch(err => console.error(err));
    

    Implement the factory function. Put it to the separate file or as a function above the NgModule declaration. Mostly depending on how large can it get.

    // Factory method file
    import { AuthService } from 'somewhere/in/you/project';
    
    // Deps defined in AppModule reflect into the parameters of your factory function. 
    // You can add more if needed.
    export function appInit(
      authService: AuthService,
    ): () => Observable<boolean> {
      // Since return type is a function, we need to return this way.
      return () => {
        authService.refreshToken().pipe(
          map(response => !!response) // Map it in some way that it returns boolean. 
          // Check if response exists, or contains some property, etc.
        )
      }
    }
    
    

    Most of the AUTH libraries recommend this approach. I used it already with KeyCloak and Azure and the UX is very smooth. What you might encounter though, it's a bit prolonged time of loading. But it's very simple to implement some loading screen for this directly in the index.html