[Fixed] Securing a route to use a refresh token

Issue

The route configuration is making use of an isAuthenticated service method :

canActivate(route: ActivatedRouteSnapshot): boolean {
  const expectedRole = route.data.expectedRole ? route.data.expectedRole : null;
  const tokenPayload = this.authService.getDecodedAccessToken();
  const role = tokenPayload.role ? tokenPayload.role : null;
  if (!this.authService.isAuthenticated()) {
    this.router.navigate(['login']);
    return false;
  } else if (role != null && role !== expectedRole) {
    this.router.navigate(['login']);
    return false;
  } else {
    return true;
  }
}

This method is checking the access token validity in the browser local storage without yet trying to use a refresh token :

public isAuthenticated(): boolean {
  const token = this.getAccessTokenFromLocalStorage();
  return (token && !this.jwtHelperService.isTokenExpired(token));
}

I wonder how to go about using the refresh token.

I was hoping to have my interceptor do the job :

return this.refreshToken()
.pipe(
  switchMap(() => {
    request = this.addAccessToken(request);
    return next.handle(request);
  })
)
.pipe(
  catchError(
    (refreshError) => {
      this.authService.logout();
      return empty();
      // return throwError(refreshError); TODO
    })
);

The refresh token is then sent in the request :

private refreshToken() {
  if (this.refreshTokenInProgress) {
    return new Observable(observer => {
      this.tokenRefreshed$.subscribe(() => {
        observer.next();
        observer.complete();
      });
    });
  } else {
    this.refreshTokenInProgress = true;
    console.log('Sending a refresh token request...');
    return this.authService.refreshAccessToken()
      .pipe(
        tap(() => {
          console.log('The refresh token has been received');
          this.refreshTokenInProgress = false;
          this.tokenRefreshedSource.next();
        })
      );
  }
}

The renewed access token is then added to the next request :

private addAccessToken(request): HttpRequest<any> {
  if (!this.tokenService.getAccessTokenFromLocalStorage()) {
    return request;
  }

  // The original request is immutable and cannot be changed
  return this.authService.addAccessTokenToClonedRequest(request);
}

But for now, my isAuthenticated method completely ignores this.

Should I modify the isAuthenticated method to have it call the refreshToken method ? Or is there a way to plug the interceptor into the route configuration ?

UPDATE:

I modified the isAuthenticated method :

public isAuthenticated(): boolean {
  let isAuthenticated = false;
  if (this.tokenService.accessTokenIsNotExpired()) {
    isAuthenticated = true;
  } else {
    if (this.tokenService.refreshTokenIsNotExpired()) {
      this.refreshAccessToken()
      .pipe(
        map(() => {
          console.log('The access token has been refreshed');
          // TODO How to resend this unauthorized request ?
        })
      );
    }
  }
  return isAuthenticated;
}

But then the refresh token response is asynchronous, whereas the canActivate property is synchronous. So I suppose I lose the unauthorized request in the updated method above. Is there any way to resend this unauthorized request ?

Also, what to do with my beautiful interceptor ? Shall it remain unused for the access token refreshing part ? UPDATE : I can answer that one now : the interceptor access token refreshing is getting used when the access token is still valid when the client looks it up in the isAuthenticated method, but is then not valid any longer when the request arrives to the REST token refresh endpoint and the server checks the token. I’d say so.

UPDATE :
I also tried this method but it didn’t help :

public isAuthenticated(): boolean {
  let isAuthenticated = true;
  if (this.tokenService.accessTokenExpired()) {
    isAuthenticated = false;
    if (this.tokenService.refreshTokenExpired()) {
      isAuthenticated = false;
    } else {
      this.refreshAccessToken()
        .pipe(
          map((response: HttpResponse<any>) => {
            console.log('The access token has been refreshed');
          }),
          catchError((error, caught) => {
            console.log('The access token has not been refresh');
            console.log(error);
            return empty();
          })
        );
    }
  }
  return isAuthenticated;
}

UPDATE : It nows works fine, the refresh token does renew the access token. And the routing is behaving as expected.

I changed the canActivate method as it can use an Observable too in fact :

canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
  const expectedRole = route.data.expectedRole ? route.data.expectedRole : null;
  const tokenPayload = this.tokenService.getDecodedAccessToken();
  return this.authService.isAuthenticated()
  .pipe(
    map(isAuth => {
      console.log('A response was returned');
      console.log(isAuth);
      if (!isAuth) {
        this.router.navigate(['login']);
        return false;
      } else {
        return true;
      }
    }),
    catchError((error, caught) => {
      console.log('An error was returned');
      console.log(error);
      return of(false);
    })
  );
}

With the isAuthenticated method now looking like :

public isAuthenticated(): Observable<boolean> {
  if (this.tokenService.accessTokenExpired()) {
    console.log('The access token expired.');
    if (this.tokenService.refreshTokenExpired()) {
      console.log('The refresh token expired.');
      return of(false);
    } else {
      return this.refreshAccessToken()
      .pipe(
        map(response => {
          if (response) {
            console.log('The access token has been refreshed');
            return true;
          }
        }),
        catchError((error, caught) => {
          console.log('The access token could not be refreshed');
          console.log(error);
          return of(false);
        })
      );
    }
  }
  return of(true);
}

public refreshAccessToken(): Observable<any> {
  console.log('Sending the refresh token to obtain a new access token');
  let httpHeaders: HttpHeaders = this.httpService.buildHeader(null);
  httpHeaders = this.addRefreshTokenHeader(httpHeaders);
  httpHeaders = this.addClientIdHeader(httpHeaders);

  return this.httpService.postWithHeadersInResponse(URI_REFRESH_TOKEN, {}, httpHeaders)
    .pipe(
      map((response: HttpResponse<any>) => {
        // Only the access token is refreshed
        // Refresing the refresh token would be like giving a never expiring refresh token
        this.storeAccessTokenInLocalStorage(response);
        console.log('Stored the refreshed access token in the local storage');
        return true;
      })
    );
}

Solution

To avoid refreshing the token on every HTTP request I used rxjs Scheduler to refresh the session before the expiration.

Don’t refresh on every request. The user might be using the webpage(maybe writing a message) but not sending requests, so the user is active and yet the session would expire anyway.

Don’t trust the expiration time field in the JWT to calculate when to refresh the token because the server might have a different time than the client. Adjust the time differences in your calculations if needed.

Also, when using refresh token, remember to set a time limit for the refresh. Otherwise, a user might have an infinite session by just refreshing over and over.

Those are the issues I faced. The client had requested session expiration after 8 minutes of inactivity.

Leave a Reply

(*) Required, Your email will not be published