private User updateUserInfo(final String subjectId, final User user, final JwtClaims jwtClaims) {
        AtomicReference<User> userRef = new AtomicReference<>(user);

        // We must default the displayName in the same way as happens when the DB record is created
        // (in stroom.security.impl.UserServiceImpl.getOrCreateUser)
        // else it will always detect a mismatch.
        final String displayName = JwtUtil.getUserDisplayName(openIdConfigProvider.get(), jwtClaims)
                .filter(str -> !str.isBlank())
                .orElse(subjectId);

        // Hopefully this one is enough of a standard to always be there.
        final String fullName = JwtUtil.getClaimValue(jwtClaims, OpenId.CLAIM__NAME)
                .orElse(null);

        final Predicate<User> hasUserInfoChangedPredicate = aUser ->
                !Objects.equals(displayName, aUser.getDisplayName())
                || !Objects.equals(fullName, aUser.getFullName());

        if (hasUserInfoChangedPredicate.test(user)) {
            synchronized (this) {
                securityContext.asProcessingUser(() -> {

                    int iterationCount = 0;
                    boolean success = false;

                    final UserService userService = userServiceProvider.get();
                    while (!success && iterationCount < 10) {
                        final User persistedUser = userService.loadByUuid(user.getUuid())
                                .orElseThrow(() -> new RuntimeException(
                                        "Expecting to find user with uuid " + user.getUuid()));

                        if (hasUserInfoChangedPredicate.test(user)) {
                            final String currentDisplayName = persistedUser.getDisplayName();
                            final String currentFullName = persistedUser.getFullName();

                            persistedUser.setDisplayName(displayName);
                            persistedUser.setFullName(fullName);
                            try {
                                // It is possible for another node to do this, so OCC would throw
                                // an exception
                                final User updatedUser = userService.update(persistedUser);
                                logNameChange(persistedUser,
                                        currentDisplayName,
                                        currentFullName,
                                        displayName,
                                        fullName);

                                // Caches need to know the user has changed
                                EntityEvent.fire(
                                        entityEventBus,
                                        DocRef.builder()
                                                .uuid(updatedUser.getUuid())
                                                .type(UserDocRefUtil.USER)
                                                .build(),
                                        EntityAction.UPDATE);

                                userRef.set(updatedUser);
                                success = true;
                            } catch (DataChangedException e) {
                                LOGGER.debug(LogUtil.message(
                                        "Another node has updated user {}, going round again. iterationCount: {}",
                                        user, iterationCount));
                            }
                        } else {
                            LOGGER.debug("Another node has updated it to how we want it");
                            success = true;
                        }
                        iterationCount++;
                    }
                    if (!success) {
                        throw new RuntimeException(LogUtil.message(
                                "Unable to update user {} after {} attempts", user, iterationCount));
                    }
                });
            }
        }
        return Objects.requireNonNull(userRef.get());
    }
External IDP users are identified by their subject which is a not very helpful UUID.
Therefore, we cache the preferred_username and full_name in the stroom user whenever the user
logs in, or hits the api. We have no way to request this information from the IDP prior to
them logging in though.
Each time we map their identity we check the cached info is up-to-date and if so update it.
stroom-security/stroom-security-impl/src/main/java/stroom/security/impl/StroomUserIdentityFactory.java