2

I am building a basic auth system with JWT. The following is the basic workflow:

enter image description here

Before I send a request to the API, I have an HttpInterceptor that checks whether or not the token has expired. If it hasn't expired, the interceptor will attach an authorization header to the request with the JWT. If it has expired, the token needs to be refreshed before sending the actual request. When the refresh requests arrives at the backend, it will check the refresh token against the database and remove the entry such that it can only be used once, then it will return a new JWT + refresh token.

Some of my pages fire multiple requests when accessing them and here comes the issue. Multiple referesh requests will be sent at once and hence only the first one arriving at the backend will return succesfully. All other request will return 401 errors which causes issues clientside.

Therefore, I am searching for a way to halt all requests until the first refresh request has returned. The HttpInterceptor calls a function that returns the JWT by checking the expiration date and firing a refresh request if it expired.

Token Interceptor:

intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // Get access token
    this._auth.getAccessToken().subscribe((accessToken: string) => {
        request = request.clone({
            setHeaders: {
                Authorization: `Bearer ${accessToken}`
            }
        });
        return next.handle(request);
    });
}

Get Access Token Function:

public getAccessToken(): Observable<string> {
    if (!this._authUser) return throwError("User is not logged in.");   // Make sure user is logged in

    // Refresh token
    if (this.checkTokenExpired())
        return this.refreshToken().pipe(map(() => this._authUser.tokenManager.accessToken));

    return of(this._authUser.tokenManager.accessToken);
}

For simulatenous requests this architecture becomes an issue because the refresh request will be sent multiple times. I need something like a mutex to halt all requests besides the first one and release all of them once the token has been refreshed and return the new JWT.

Tom el Safadi
  • 6,164
  • 5
  • 49
  • 102

2 Answers2

1

Thanks to the following post, I was able to get the implementation I wanted: https://stackoverflow.com/a/54328099/5203853

This is how my implementation of the refreshToken() function looks like:

// Variables
private _tokenSubject: BehaviorSubject<auth.User> = new BehaviorSubject<auth.User>(null);
private _isRefreshingToken: Boolean = false;

private refreshToken(): Observable<auth.User> {
    if (!this._authUser) return throwError("User is not logged in."); // Make sure user is logged in

    if (!this._isRefreshingToken) {
        this._isRefreshingToken = true;
        // Reset such that the following requests wait until the token
        // comes back from the refreshToken call.
        this._tokenSubject.next(null);

        let req = this._httpClient.post<AuthService.authUser>('/api/auth/refresh', { 
            accessToken: this._authUser.tokenManager.accessToken,
            refreshToken: this._authUser.tokenManager.refreshToken 
        })

        return req.pipe(
            tap(authUser => {
                this.saveUser(authUser);
                // Emit event & return promise
                this.onAuthStateChange.emit(authUser.user);
                // Retry previous request
                this._tokenSubject.next(authUser.user);
            }),
            map(authUser => authUser.user),
            catchError((err: HttpErrorResponse) => {
                this.signOut();
                this._router.navigate(['/auth/login']);
                return throwError(err.message);
            }), 
            finalize(() => {
                this._isRefreshingToken = false;
            })
        );
    }
    else {
        return this._tokenSubject
            .pipe(
                filter(authUser => authUser != null), 
                take(1)
            );
    }
}
Tom el Safadi
  • 6,164
  • 5
  • 49
  • 102
0

React.js approach

auth contains accessToken and axiosPrivate is axios instance

import { axiosPrivate } from "../api/axios";
import { useContext, useEffect } from "react";
import useRefresh from "../hooks/useRefreshToken";
import { AuthTokenContext } from "../context/AuthTokenContext";
let refreshTokenPromise = null;
const useAxiosPrivate = () => {
  const { auth } = useContext(AuthTokenContext);
  const refresh = useRefresh();
  useEffect(() => {
    const requestIntercept = axiosPrivate.interceptors.request.use(
      (config) => {
        if (!config.headers["Authorization"]) {
          config.headers["Authorization"] = `Bearer ${auth?.accessToken}`;
        }
        return config;
      },
      (error) => Promise.reject(error)
    );
    const responseIntercept = axiosPrivate.interceptors.response.use(
      (response) => response,
      (error) => {
        const prevRequest = error?.config;
        if (error.config && error.response && error.response.status === 403 && !prevRequest.sent) {
          if (!refreshTokenPromise) {
            refreshTokenPromise = refresh().then((token) => {
              refreshTokenPromise = null; // clear state
              prevRequest.sent = true;
              return token; // resolve with the new token
            });
          }
          return refreshTokenPromise.then((token) => {
            prevRequest.headers["Authorization"] = `Bearer ${token}`;
            return axiosPrivate.request(prevRequest);
          });
        }
        return Promise.reject(error);
      }
    );
    return () => {
      axiosPrivate.interceptors.request.eject(requestIntercept);
      axiosPrivate.interceptors.response.eject(responseIntercept);
    };
  }, [auth, refresh]);
  return axiosPrivate;
};

export default useAxiosPrivate;

After importing this hook, attach this with every request you want like this:

const axiosHook = useAxiosPrivate()
await axiosHook.get(url)

and it will work like you want.

janw
  • 8,758
  • 11
  • 40
  • 62