BrowserSessionInventory.java

package com.taxonomy.security.service;

import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/** A read-only, bounded projection of the actual local Spring Security registry. */
@Service
public class BrowserSessionInventory {
    static final int MAX_ROWS = 200;
    static final int MAX_DISPLAY_NAME_LENGTH = 256;
    private final SessionRegistry registry;

    public BrowserSessionInventory(SessionRegistry registry) {
        this.registry = registry;
    }

    public Snapshot snapshot() {
        Map<Identity, UserSessions> users = new LinkedHashMap<>();
        int totalSessions = 0;
        int unidentifiedSessions = 0;
        for (Object principal : registry.getAllPrincipals()) {
            List<SessionInformation> sessions = registry.getAllSessions(principal, false);
            if (sessions.isEmpty()) continue;
            totalSessions += sessions.size();
            Identity identity = identity(principal);
            if (identity == null) {
                unidentifiedSessions += sessions.size();
                continue;
            }
            Instant lastRequest = sessions.stream().map(session -> session.getLastRequest().toInstant())
                    .max(Comparator.naturalOrder()).orElseThrow();
            String displayName = displayName(principal);
            UserSessions previous = users.get(identity);
            int count = sessions.size();
            if (previous != null) {
                count += previous.sessionCount();
                if (previous.lastRequest().isAfter(lastRequest)) {
                    lastRequest = previous.lastRequest();
                    displayName = previous.username();
                }
            }
            users.put(identity, new UserSessions(displayName, identity.kind(), count, lastRequest));
        }
        List<UserSessions> rows = users.values().stream()
                .sorted(Comparator.comparing(UserSessions::username)
                        .thenComparing(UserSessions::authenticationType))
                .limit(MAX_ROWS).toList();
        return new Snapshot(Instant.now(), "LOCAL_INSTANCE", users.size(), totalSessions,
                unidentifiedSessions, users.size() > rows.size(), rows);
    }

    private static Identity identity(Object principal) {
        if (principal instanceof OidcUser oidc) {
            // Display names may collide or change. Issuer + subject is the identity;
            // neither claims nor tokens are serialized by this projection.
            if (oidc.getIssuer() == null || oidc.getSubject() == null) return null;
            return new Identity("OIDC", oidc.getIssuer().toString(), oidc.getSubject());
        }
        if (principal instanceof UserDetails user) {
            return new Identity("LOCAL", "", user.getUsername());
        }
        return null; // Never fall back to arbitrary principal.toString().
    }

    private static String displayName(Object principal) {
        String name;
        if (principal instanceof OidcUser oidc) {
            name = oidc.getPreferredUsername();
            if (name == null || name.isBlank()) name = oidc.getSubject();
        } else {
            name = ((UserDetails) principal).getUsername();
        }
        return name.length() <= MAX_DISPLAY_NAME_LENGTH
                ? name
                : name.substring(0, MAX_DISPLAY_NAME_LENGTH - 1) + "…";
    }

    private record Identity(String kind, String issuer, String subject) { }
    public record UserSessions(String username, String authenticationType, int sessionCount, Instant lastRequest) { }
    public record Snapshot(Instant timestamp, String scope, int userCount, int sessionCount,
                           int unidentifiedSessionCount, boolean truncated, List<UserSessions> users) { }
}