public <T, Ex extends Exception> T withAuthData(FunctionRequiringAuthData<T, Ex> f)
      throws Ex, InvalidTokenException, IOException {
    try {
      return f.execute(authData);
    } catch (InvalidTokenException e) {
      if (authData.getRefreshToken() == null || authData.getRefreshToken().isEmpty()) {
        monitor.severe(() -> "Refresh token not present with auth data");
        throw e;
      }

      this.authData = refreshToken();
      try {
        return f.execute(this.authData);
      } catch (InvalidTokenException innerException) {
        throw new InvalidTokenException("Token still expired after refresh", innerException);
      }
    }
  }
Call a function {@code f} requiring auth data, injecting the auth data. If the function raises
an {@link InvalidTokenException}, the token will be refreshed and the function will be called
again with the fresh token.

@param f The function to call - {@code (authData) -> T}
@param <T> {@code f}'s return type
@return The {@code <T>} returned by {@code f}
@throws IOException if {@code f} throws an {@link IOException}
@throws InvalidTokenException if {@code f} throws an {@link InvalidTokenException} after the
    access token has been refreshed
extensions/data-transfer/portability-data-transfer-generic/src/main/java/org/datatransferproject/datatransfer/generic/auth/OAuthTokenManager.java