Skip to content
Flag of Europe
Made in the European Union · Independently built · Released under EUPL 1.2
Logout Flows

Logout Flows

A safe logout in Vaadin has to do more than drop the subject from some store. The Vaadin session, the underlying HTTP session, the bearer token (in the REST adapter), and any other active sessions of the same subject must all be handled. The LogoutService chain in security-core captures this cleanly.

Status: implemented. Feature §6 in Konzept-V00.60.00.md (delivered 2026-05-06, evolved 2026-05-11 to multi-session shape).

The contract

public interface LogoutService {
  void logout(SubjectId subjectId, LogoutScope scope);
  void addListener(LogoutListener listener);
  void removeListener(LogoutListener listener);
}

Two pieces of information:

  • SubjectId — who is being logged out.
  • LogoutScope — how far the logout reaches.
public enum LogoutScope {
  CurrentSession,         // sign-out button
  AllSessionsOfSubject    // "force-sign-out everywhere", password change, etc.
}

Adapter-neutral default

SubjectClearingLogoutService in security-core is the framework-free default. It:

  1. Clears the subject from the configured SubjectStore.
  2. Consults the SubjectSessionRegistry to find every session that belongs to that subject (when the scope is AllSessionsOfSubject).
  3. Fans out a notification to every registered LogoutListener.

No Vaadin, no Servlet, no transport-specific code.

Adapter-specific cleanup

Adapters register a LogoutListener instead of being called directly:

public interface LogoutListener {
  void onLogout(SubjectId subjectId, SessionId sessionId, LogoutScope scope);
}
  • The Vaadin adapter listens to invalidate the VaadinSession, close the underlying servlet session, and redirect the browser via Page.setLocation(...).
  • The REST adapter listens to revoke the bearer token associated with the session.

Because adapters subscribe instead of being called, the core service doesn’t need to know about Vaadin or HTTP. New transport adapters register a new listener — no core change.

Tracking sessions

SubjectSessionRegistry (and the in-memory default InMemorySubjectSessionRegistry) maps SubjectId → set of SessionId. The adapter registers a session on login and removes it on logout. This is what makes AllSessionsOfSubject work:

// admin "force sign-out user X everywhere"
logoutService.logout(SubjectId.of("alice"), LogoutScope.AllSessionsOfSubject);

Using it in your application

public class MainView extends AppLayout {

  private final LogoutService logoutService =
      SecurityServiceResolver.logoutService();

  private final SubjectId currentSubject = ...;  // from session

  private Component logoutButton() {
    return new Button("Logout", e -> logoutService.logout(
        currentSubject, LogoutScope.CurrentSession
    ));
  }
}

For an admin who needs to terminate every active session of a user (for example after a password reset), call with LogoutScope.AllSessionsOfSubject instead.

Why this shape, not a method-per-variant?

A single logout(subjectId, scope) API:

  • composes cleanly with the audit pipeline (one call site to instrument — emits LogoutPerformed),
  • keeps the contract stable across framework versions,
  • lets new transport adapters opt in via LogoutListener rather than changing the API,
  • supports cluster-aware implementations later (replace the registry, keep everything else).

Adding a new scope (e.g. AllSessionsExceptCurrent) becomes a new enum value — not a new overload and not a new method.