0

I'm working with angular interceptors for the first time and I almost have what I want but there is something I can't quite figure out even after googling around for a while. I am storing a refresh token locally and the access token expires every 15 minutes; I want to be able to use the refresh token to automatically refresh their auth token when it expires.

My first attempt went like this:

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.url.toLowerCase().includes('/auth')) {
  // It's an auth request, don't get a token
  return next.handle(req);
}

// Not an auth endpoint, should have a token
this.authService.GetCurrentToken().subscribe(token => {
  // Make sure we got something
  if (token == null || token === '') {
    return next.handle(req);
  }

  // Have a token, add it
  const request = req.clone({
    setHeaders: {
      Authorization: `Bearer ${token}`
    }
  });

  return next.handle(request);
});
}

This did not seem to work and I couldn't figure out why (I'm new to Angular and fairly new to JS as well so sorry if it's obvious to others). On a hunch I wondered if it was the observable messing things up and it doesn't like waiting for the observable to return so I tried this:

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.url.toLowerCase().includes('/auth')) {
  // It's an auth request, don't get a token
  return next.handle(req);
}

const token = this.authService.GetAccessTokenWithoutRefresh();
const request = req.clone({
  setHeaders: {
    Authorization: `Bearer ${token}`
  }
});
return next.handle(request);   
}

And now it seems to work! That suggests that I may have been correct in my hunch (or that it's something else inside the other code that I'm not seeing). Anyway, working is good but this leaves me with the question of how to refresh. The original reason I had it using an observable from the auth service was in case it needed to refresh. Basically the auth service would look at it's current token and see if it was expired or not. If not it would just return of(token) but if it was expired it would reach back out to the server via an http post which is observable, so the string would arrive whenever the server responded.

So I guess my question is two-fold:

  1. Can anyone confirm or refute that I was correct about the observable messing up the interceptor? It seems like that's the issue but would like to be sure.
  2. How do I deal with refreshing the token for them in the background without them having to log back in every 15 minutes?

EDIT

Here is the logic in the auth token method:

GetCurrentToken(): Observable<string> {
if (this.AccessToken == null) {
  return null;
}
if (this.Expiry > new Date()) {
  return of(this.AccessToken);
}

// Need to refresh
return this.RefreshToken().pipe(
  map<LoginResult, string>(result => {
    return result.Success ? result.AccessToken : null;
  })
);
}

and the refresh method:

private RefreshToken(): Observable<LoginResult> {
const refreshToken = localStorage.getItem('rt');
if (refreshToken == null || refreshToken === '') {
  const result = new LoginResult();
  // Set other stuff on result object
  return of(result);
}

const refresh = new RefreshTokenDto();
refresh.MachineId = 'WebPortal';
refresh.TokenId = refreshToken;
return this.http.post(ApiData.baseUrl + '/auth/refresh', refresh)
  .pipe(
    tap<AuthResultDto>(authObject => {
      this.SetLocalData(authObject);
    }),
    map<AuthResultDto, LoginResult>(authObject => {
      const result = new LoginResult();
      // Set other stuff on the result object
      return result;
    }),
    catchError(this.handleError<LoginResult>('Refresh'))
  );
}

EDIT

Ok so with help from the answer below as well as this question here is what I came up with:

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.url.toLowerCase().includes('/auth')) {
  // It's an auth request, don't get a token
  return next.handle(req.clone());
}

return this.authService.GetCurrentToken().pipe(
  mergeMap((token: string) => {
    if (token === null || token === '') {
      throw new Error('Refresh failed to get token');
    } else {
      return next.handle(req.clone({setHeaders: {Authorization: `Bearer ${token}`}}));
    }
  }),
  catchError((err: HttpErrorResponse) => {
    if (err.status === 401) {
      this.router.navigateByUrl('/login');
    }
    return throwError(err);
  })
);
}

So basically it ended up that my first attempt was not terribly far off, the 'secret' was to use a pipe and merge map instead of trying to subscribe.

sfaust
  • 2,089
  • 28
  • 54
  • Do you have the _refresh_ logic capsulated in a function? If you, could you add the signature of it? – Jota.Toledo Jun 07 '19 at 06:40
  • 1
    _Can anyone confirm or refute that I was correct about the observable messing up the interceptor?_ most likely. In general is an error-prone practice to subscribe inside services or http-interceptors. This can be avoided by using the correct rxjs operators for the case. – Jota.Toledo Jun 07 '19 at 06:47
  • In your first code snippet in case of auth request you return `HttpEvent` instead of `Observable>`. So, try to wrap this first return in `of()` emitter (https://www.learnrxjs.io/operators/creation/of.html), so it should look like `return of(next.handle(req));` (ONLY IN AUTH REQUEST) – Andriy Jun 07 '19 at 06:55
  • @Jota.Toledo Ok I guess that makes sense since the observable would return and continue at a separate time so the interceptor wouldn't be able to return a result right away? So this has to be a pretty common thing... how do you handle automatic token refresh then? I have posted the code of my token getting and refreshing methods. – sfaust Jun 07 '19 at 13:53
  • @Andriy Auth request is working though... Looking at the code for 'handle' it also returns an `Observable>` so wouldn't that be valid and preferred to just return that? Of the examples I've found online so far they don't seem to wrap it like that and just return it directly, however they also don't auto-refresh the token... – sfaust Jun 07 '19 at 13:57
  • @Jota.Toledo thanks for the edits and direction, but wouldn't it be better to leave the original and respond with an answer to allow it to be tracked? Also in your new 'GetCurrentToken' method it always either returns current token or refreshes; it's also possible that the user has never logged in and doesn't have a refresh token (which would be the token null state in the original method). Your new method doesn't handle this as far as I can tell. Last, why did you change 'handleError' from LoginResult to string? Method should return LoginResult... – sfaust Jun 07 '19 at 15:24
  • 1
    My bad, I rolled back the changes :). Guess my approach is missing that case as well. – Jota.Toledo Jun 07 '19 at 15:38

1 Answers1

0

You could try with an approach as follows. I might have exaggerated with the amount of FP in it:

export class AuthInterceptor {
 ctor(private authService: AuthService){}
 intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

   return of(req.url.toLowerCase().includes('/auth')).pipe(
     mergeMap(isAuthRequest => !isAuthRequest
       // Missing: handle error when accessing the access token
       ? this.authService.accessToken$.pipe(map(addAuthHeader(req)))
       : of(req)
     ),
     mergeMap(nextReq => next.handle(nextReq))
   );
 }
}

function addAuthHeader(req: HttpRequest<any>): (token:string)=> HttpRequest<any> {
  return token => req.clone({setHeaders: {Authorization: `Bearer ${token}`}})
} 

And the auth service:

export class AuthService {
  ctor(private http: HttpClient){}

  get accessToken$(): Observable<string> {
    return of(this.AccessToken).pipe(
       mergeMap(token => token === null
         ? throwError("Access token is missing")
         : of(this.Expiry > new Date())
       ),
       mergeMap(accessTokenValid => accessTokenValid
         ? of(this.AccessToken)
         : this.refreshToken()
       )
    );
  }

  refreshToken(): Observable<string> {
    return of(localStorage.getItem('rt')).pipe(
      mergeMap(refreshToken => !refreshToken 
        ? of(extractAccessTokenFromLogin(createLoginResult())
        : this.requestAccessToken(this.createRefreshToken(refreshToken))
      )
    );
  }

  private requestAccessToken(refreshToken: RefreshTokenDto): Observable<string> {
    return this.http.post<AuthResultDto>(ApiData.baseUrl + '/auth/refresh', refreshToken)
     .pipe(
       tap(auth => this.SetLocalData(auth )),
       map(auth => this.mapAuthObjToLoginRes(auth)),
       map(extractAccessTokenFromLogin)
       catchError(this.handleError<string>('Refresh'))
     )
  }

  private createRefreshToken(tokenId: string): RefreshTokenDto{...}

  private createLoginRes(): LoginResult {...}

  private mapAuthObjToLoginRes(val: AuthResultDto): LoginResult{...}
}

function extractAccessTokenFromLogin(login: LoginResult): string 
     => login.Success ? login.AccessToken : null;
Jota.Toledo
  • 27,293
  • 11
  • 59
  • 73
  • Ok to make sure I understand...Basically instead of subscribing like I was doing you are using pipe and mergeMap which essentially give the observable 'stuff to do' when it comes back with a result instead of actually subscribing and trying to make modifications and return there, is that correct? Then the interceptor is able to continue with it's chain without trying to wait on something but still makes the transformations when invoked? Going to try this out now... – sfaust Jun 07 '19 at 16:00
  • So sorry for the delay on this. I went ahead and marked this as the answer even though it's a little different than what I ended up doing since it helped lead to the answer and I believe is the same idea... – sfaust Aug 05 '19 at 15:48