Preventing double login / close idle session / Spring

Hi,

I’m working on a requirement where users should not be able to log in twice (from different computers).
I’ve solved this using spring security (by using ConcurrentSessionControlAuthenticationStrategy) by limiting the number of sessions per user to 1.
This works fine.
What doesn’t work fine is when a user closes the browser tab or kills the browser.

Neither Vaadin nor Spring reliably detect that, so if the user tries to log in, and the old session is still around, he or she can’t log in until it expires.
https://vaadin.com/forum#!/thread/9108162 has a few hints, it looks like this is basically expected behaviour.

What I’d like to do now is to periodically check for inactive Sessions. VaadinService has cleanupSession() that seems to do that, so I’m thinking about using that.
I’m not sure how to do that, though. The current VaadinService seems to be stored in a ThreadLocal, so I can’t access it from a background thread.
So I’m thinking I should register all UIs with some service, that periodically iterates over them and then tries to call cleanupSession.
What I’d like to know is if there’s some simpler way of doing what I want and if not, if my approach would be feasible.

I’ve set closeIdleSessions to true in case that matters.

Thanks!
-tom

After trying a couple of things I’m not so sure that VaadinService.cleanupSession() is the way to go, it seems that it is really supposed to be called at the end of a request. Plus I’m not really comfortable calling stuff that is package private…

This is what I have now. The bean is registered as SessionInitListener and SessionDestroyListener.

It kind-of works. After about 4-5 Minutes it closes the Vaadin Session (Heartbeat is 60 seconds, so that sounds about right).
Then after about 2 Minutes, the underlying Http Session is closed.
The whole thing feels dirty, though…

@Component
@Slf4j
public class CleanupSession implements SessionInitListener, SessionDestroyListener {
    private final List<VaadinSession> sessions = new CopyOnWriteArrayList<>();
    // TODO should be 3x heartbeat interval
    private long cleanupAfterMillis = 60_000;


    public CleanupSession() {
    }

    @Scheduled(fixedRate = 5_000)
    public synchronized void cleanup() {
        for (VaadinSession vaadinSession : sessions) {
            vaadinSession.access(() -> {
                final VaadinService service = vaadinSession.getService();
                if (service != null && vaadinSession.getState() == VaadinSession.State.OPEN) {
                    if ((System.currentTimeMillis() - vaadinSession.getLastRequestTimestamp()) > cleanupAfterMillis) {
                        try {
                            log.info("Session {} not in use for {}ms, trying cleanup...", vaadinSession, cleanupAfterMillis);
                            Method method = VaadinService.class.getDeclaredMethod("cleanupSession", VaadinSession.class);
                            method.setAccessible(true);
                            method.invoke(service, vaadinSession);
                        } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
                            log.error("Error invoking cleanupSession via reflection", e);
                        }
                    }
                }

                if (!(vaadinSession.getState() == VaadinSession.State.OPEN)) {
                    log.info("Removing closed session {}", vaadinSession.toString());
                    sessions.remove(vaadinSession);
                }
            });
        }
    }

    @Override
    public void sessionDestroy(SessionDestroyEvent event) {
        sessions.remove(event.getSession());
    }

    @Override
    public void sessionInit(SessionInitEvent event) throws ServiceException {
        sessions.add(event.getSession());
    }
}

After some further testing I think I’m mistaken.
The VaadinSession is kept around if closeIdleSessions is active until the sessionTimeout has been exceeded.

From VaadinService:

    private boolean isSessionActive(VaadinSession session) {
        if (session.getState() != State.OPEN || session.getSession() == null) {
            return false;
        } else {
            long now = System.currentTimeMillis();
            int timeout = 1000 * getUidlRequestTimeout(session);
            return timeout < 0
                    || now - session.getLastRequestTimestamp() < timeout;
        }
    }
    private int getUidlRequestTimeout(VaadinSession session) {
        return getDeploymentConfiguration().isCloseIdleSessions() ? session
                .getSession().getMaxInactiveInterval() : -1;
    }

Which means cleanupSession() won’t kill the session after 3 missed heartbeats.

So with what I that in mind i added a check for that. cleanupSession() actually removes all UIs that aren’t active anymore, so I can check for sessions without any UI and invalidate it manually.

                        final Collection<UI> uIs = vaadinSession.getUIs();
                        if (uIs.size() == 0) {
                            log.info("No UIs active for session {}, invalidating it...", vaadinSession.toString());
                            vaadinSession.getSession().invalidate();
                            vaadinSession.close();
                        }