protected FetchTokenResult refreshUsingRefreshToken(final UpdatableToken updatableToken) {
        Objects.requireNonNull(updatableToken);
        final UserIdentity identity = Objects.requireNonNull(updatableToken.getUserIdentity());
        final TokenResponse currentTokenResponse = updatableToken.getTokenResponse();

        FetchTokenResult fetchTokenResult;

        if (hasRefreshTokenExpired(currentTokenResponse)) {
            if (identity instanceof final HasSession userWithSession) {
                LOGGER.info("Refresh token has expired, removing user identity from " +
                            "session to force re-authentication. userIdentity: {}", userWithSession);
                userWithSession.removeUserFromSession();
            } else {
                LOGGER.warn("Refresh token has expired, can't refresh token or create new.");
            }
            fetchTokenResult = null;
        } else {
            TokenResponse newTokenResponse = null;
            JwtClaims jwtClaims = null;
            try {
                LOGGER.debug("Refreshing token " + identity);

                LOGGER.debug(LogUtil.message(
                        "Current token expiry max age: {}, refresh token expiry max age: {}",
                        NullSafe.toString(updatableToken.getTokenResponse(),
                                TokenResponse::getExpiresIn,
                                Duration::ofSeconds),
                        NullSafe.toString(updatableToken.getTokenResponse(),
                                TokenResponse::getEffectiveRefreshExpiresIn,
                                Duration::ofSeconds)));

                fetchTokenResult = refreshTokens(currentTokenResponse);
                newTokenResponse = fetchTokenResult.tokenResponse();
                jwtClaims = fetchTokenResult.jwtClaims();
            } catch (final RuntimeException e) {
                LOGGER.error("Error refreshing token for {} - {}", identity, e.getMessage(), e);
                if (identity instanceof final HasSession userWithSession) {
                    userWithSession.invalidateSession();
                }
                throw e;
            } finally {
                // Some IDPs don't seem to send updated refresh tokens so keep the existing refresh token.
                if (newTokenResponse != null
                    && newTokenResponse.getRefreshToken() == null
                    && currentTokenResponse.getRefreshToken() != null) {
                    newTokenResponse = newTokenResponse
                            .copy()
                            .refreshToken(currentTokenResponse.getRefreshToken())
                            .refreshTokenExpiresIn(Objects.requireNonNullElseGet(
                                    newTokenResponse.getEffectiveRefreshExpiresIn(),
                                    currentTokenResponse::getEffectiveRefreshExpiresIn))
                            .build();
                }
                fetchTokenResult = new FetchTokenResult(newTokenResponse, jwtClaims);

                LOGGER.debug(LogUtil.message(
                        "New token expiry max age: {}, refresh token expiry max age: {}",
                        NullSafe.toString(newTokenResponse, TokenResponse::getExpiresIn, Duration::ofSeconds),
                        NullSafe.toString(newTokenResponse,
                                TokenResponse::getEffectiveRefreshExpiresIn,
                                Duration::ofSeconds)));
            }
        }
        return fetchTokenResult;
    }
Refresh the user identity including any tokens associated with that user.

@param userIdentity
stroom-security/stroom-security-common-impl/src/main/java/stroom/security/common/impl/AbstractUserIdentityFactory.java