Renew JWT token in Angular

Currently I am working on a small project with a frontend application in Angular (version 10) and a backend in Springboot. Because this application is rather small I decided to issue the JWT token in the backend and not use an external Authorization provider like Azure. So the workflow goes like this:

  • User enters credentials
  • Frontend calls backend /login, backend verifies hashed password and if successful returns a self-signed JWT token
  • Frontend stores the JWT token in session-storage, and sends the token in the http header for each subsequent request
  • In this blog post I discuss how the frontend app assures that there is always a valid token sent along with each http request to the backend.

    Step 1: Include token in http header

    In Angular you can define a http interceptor which checks the session-storage for the token, and includes it in the http header. Let’s reuse an existing header for this which is the Authorization header. See RFC 6750 for more details. The interceptor code looks quite simple:

    @Injectable()
    export class AuthInterceptor implements HttpInterceptor {
    
        intercept(request: HttpRequest, next: HttpHandler): Observable> {
            const idToken = this.localSessionService.getIdToken();
            if (idToken) {
                const cloned = request.clone({
                    headers: request.headers.set('Authorization', 'Bearer ' + idToken)
                });
                return next.handle(cloned);
            } else {
    
                return next.handle(request);
            }
        }
    }
    

    The interceptor needs to be defined in the Angular module as a provider, so my app.module.ts file includes this:

    ...
    providers: [
      {provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true},
      {provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true}
    ],
    

    Step 2: Renew token

    But a token is not really valid forever, e.g. in my application the token will be valid for 60 minutes. After that, the token is no longer valid, and when the frontend application would not check for this use-case, after 60 minutes, the token expires, the backend returns a 403 http error code, and the frontend application displays the login-screen again… This is not really user-friendly.

    What I want to achieve is that the token is renewed automatically, say, when the token is only 30 minutes valid. There are a lot of OAUTH2 libraries which do the token-renewal deeply hidden in an own iframe (e.g. MSAL Angular library) and don’t need any special implementation in the frontend application.

    But in my frontend app I don’t like this kind of magic, so I included it like this:

  • Whenever there is a call to the backend, check whether the time to live (TTL) of the token is smaller than 30 minutes.
  • If yes, then (in the background) invoke a call to /renewToken endpoint and renew the token.
  • Nevertheless, as the token is still valid 30 minutes, the ongoing http call can proceed.
  • It could be that there are lot of http requests going on, and we don’t want to request a token-renewal several times (when the token-renewal request takes too much time), so we need to make sure to note whether we have sent a token-renewal-request, and only send new renewal-requests when there was no request sent earlier. The whole logic takes place in the same interceptor as before:

    @Injectable()
    export class AuthInterceptor implements HttpInterceptor {
    
        private renewTokenRequestSent: boolean;
    
        constructor(private localSessionService: LocalSessionService,
                    private readonly httpHandler: HttpBackend) {
        }
    
        intercept(request: HttpRequest, next: HttpHandler): Observable> {
            const idToken = this.localSessionService.getIdToken();
            if (idToken) {
    
                // *********************************** Changes for token renewal *******************
                const expiresAt = this.localSessionService.getIdTokenExpiresAtAsUnitTimestamp();
                const loggedInTTL = moment.duration(moment(expiresAt).diff(moment())).asSeconds();
                if (!this.renewTokenRequestSent) {
                    // Check age of token and if necessary, renew it
                    if (loggedInTTL < (30*60) ) {
                        this.invokeRenewTokenAysnc(idToken);
                    }
                }
                // *******************************************************************************
                const cloned = request.clone({
                    headers: request.headers.set('Authorization', 'Bearer ' + idToken)
                });
                return next.handle(cloned);
            } else {
    
                return next.handle(request);
            }
        }
    }
    

    First, we calculate the TTL of the current token using moment.js. To make this calculation faster I store the Unix timestamp (number) in the localSessionService (moment.valueOf()). If the TTL is smaller than our threshold (30 minutes) the method invokeRenewTokenAysnc is invoked. After that, the ordinary http request continues (next.handle(cloned).

    The method invokeRenewTokenAysnc is quite straight-forward:

  • Don't use an ordinary httpClient as this would mean that there are http interceptors, and we don't want to have http interceptors for the renewal call (this could lead to an infinite loop where the interceptor calls itself infinite times)
  • Make sure that in case of an error the flag (this.renewTokenRequestSent) is reset so that another renewal request will be sent in the future (where maybe the backend server is up and running again)
  • I thought long about error-handling, but actually the error handling callback does not need to do much except log the exception. There is an ongoing http request, and this ongoing http request has a valid error handling. The token-renewal request is invoked in the background, and when the token-renewal fails, our application can do nothing except to try again and again until it works, or until the TTL of the existing token times out.

        private invokeRenewTokenAysnc(idToken: string) {
            // Create own httpClient so that there are no interceptors (e.g. this interceptor) in it, plain vanilla http connection
            const httpClientForRenewal = new HttpClient(this.httpHandler);
            const renewHeaderMap = {
                Authorization: 'Bearer ' + idToken
            };
            const options = {
                headers: new HttpHeaders(renewHeaderMap),
            };
    
            this.renewTokenRequestSent = true;
            httpClientForRenewal.get('/todo/secured/v1/renew', options)
                .pipe(take(1))
                .subscribe(
                    loginResp => this.callbackSuccessRenewToken(loginResp),
                    err => this.callbackErrorRenewToken(err),
                    () => this.callbackCompleteRenewToken()
                );
        }
    
        callbackSuccessRenewToken(authResult: LoginResponse) {
            this.localSessionService.loginSuccessful(authResult);
        }
    
        callbackErrorRenewToken(err: any): void {
            console.error('Error while renewing token. Ignore error and continue (and try again to renew token).');
            // if (err.error instanceof ErrorEvent) {
            //     // *************************************************************************************
            //     // Something could go wrong on the client-side such as a network error that prevents the
            //     // request from completing successfully or an exception thrown in an RxJS operator.
            //     // These errors produce JavaScript ErrorEvent objects.
            //     // *************************************************************************************
            // } else {
            //     // *************************************************************************************
            //     // The backend returned an unsuccessful response code.
            //     // The response body may contain clues as to what went wrong.
            //     // *************************************************************************************
            //     if (err.error.code) {
            //         // Cool, we have a error which we can interpret because it contains the field "code"
            //     } else {
            //         // *************************************************************************************
            //         // We received unknown body, but status is still ok. Mostly this happens when a Proxy/
            //         // LoadBalancer in-between returns a HTML error page.
            //         // *************************************************************************************
            //     }
            // }
            this.callbackCompleteRenewToken();
        }
    
        /**
         * Complete the renewal call, make sure to invoke this method in case of error as well
         */
        callbackCompleteRenewToken(): void {
            this.renewTokenRequestSent = false;
        }
    

    Hope that helps others!

  • Leave a Reply

    Your email address will not be published. Required fields are marked *

    This site uses Akismet to reduce spam. Learn how your comment data is processed.