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.
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:
- Clears the subject from the configured
SubjectStore. - Consults the
SubjectSessionRegistryto find every session that belongs to that subject (when the scope isAllSessionsOfSubject). - 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 viaPage.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
LogoutListenerrather 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.