Seamless User Sessions in Angular: Parking API Calls and Re-execution Post OIDC Token Refresh


When working with Angular applications that leverage the OpenID Connect (OIDC) protocol for authentication, it’s crucial to manage tokens effectively. While tokens simplify authentication and help secure user sessions, they eventually expire. As a result, users could be suddenly logged out of their session, potentially losing work in progress and leading to a negative user experience. This issue can be mitigated by silently refreshing the OIDC token before the session expires. In this post we go through different steps we need to take to implement this feature using Angular Interceptor.

WHAT?

Let’s take step back. What are OIDC tokens? OpenID Connect is a simple identity layer built on top of the OAuth 2.0 protocol. It enables clients to verify the identity of an end-user based on the authentication performed by an authorization server and to obtain basic profile information about the end-user in an interoperable and REST-like manner. In OIDC, tokens play a pivotal role in the authentication process. The three kinds of tokens used are:

WHY?

OIDC can be easily integrated with Angular applications using any one of the available 3rd party libraries. Most of these handle the “Silent Refresh” process under the hood. They typically use the “Refresh Token” to acquire new “Access Token” and “Id Token” through a hidden iframe. However, if for any reason you need to develop your own custom implementation, you’ll need to manage this process manually.

HOW?

In my specific situation, when the frontend application calls an API, the backend needs to verify the token with the Identity Provider. There are three possible outcomes:

  1. If the tokens are currently valid, the http request can proceed to the domain service. The response will then be sent back to the frontend application.
  2. If the tokens have expired, the frontend will receive a 401 http status, and the user will be logged out.
  3. If the tokens are still valid but nearing expiration (for example, with only 2 minutes left until they expire), the backend will return a special http error code, such as 499. This code serves as an indication that the tokens are about to expire, and the frontend should trigger the “silent refresh” process.

We are primarily concerned with the last scenario, where we aim to handle the situation discreetly without causing any interruptions to the customer, thereby avoiding the display of an error message.

To achieve this, we can follow these steps:

  1. As soon as we receive a 499 response from one of the APIs, we initiate the “Silent Refresh” process.
  2. We temporarily park all subsequent API calls until the tokens are refreshed. This prevents them from also returning a 499 response, so it’s best to wait until we get a new set of tokens.
  3. We save all ongoing requests and execute them again after the token has been refreshed.
  4. Once we obtain a new set of tokens, we can emit a value on the tokenIsRefreshedSubject in order to trigger the execution of the pending http requests.
// Interceptor snippet
...
  intercept(httpRequests: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (this.isRefreshing) {
      return this.parkRequest(httpRequest, next);
    }
    return next
      .handle(httpRequest)
      .pipe(catchError(error => this.handleError(error, httpRequest, next)));
  }

  private handleError(
    error,
    httpRequest: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    if (error instanceof HttpErrorResponse && error.status === 499) {
      return this.handleTokenError(httpsRequest, next);
    }
    if (error instanceof HttpErrorResponse && error.status === 401) {
      this.logout();
    }
    return throwError(error);
  }

  private handleTokenError(
    httpRequest: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    if (!this.isRefreshing) {
      this.isRefreshing = false;
      this.tokenRefreshedSubject.next(false);

      return from(triggerSilentRefresh()).pipe(
        switchMap((result: boolean) => {
          if (result) {
            this.isRefreshing = false;
            this.tokenIsRefreshedSubject.next(true); // Notifies parked request to run
            return next
              .handle(httpRequest)
              .pipe(catchError(error => this.handleError(error, httpRequest, next)));
          }
          // If the result is false, it indicates that the silent refresh has failed.
          this.logout();
          throw new Error('Silent refresh failed');
        })
      );
    } else {
      return this.parkRequest(httpsRequest, next);
    }
  }

  private parkRequest(httpRequest: HttpRequest<any>, next: HttpHandler) {
    return tokenIsRefreshedSubject.pipe(
      filter(isRefreshed => isRefreshed),
      take(1),
      switchMap(() => next.handle(httpRequest))
    );
  }
...

Efficient OIDC token management using Angular Interceptors ensures a better user experience by preventing sudden session logouts. This process, done through silent token refreshing and parking API calls, enables developers to tailor security solutions and improve application functionality.