package utils; import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSessionBindingEvent; import jakarta.servlet.http.HttpSessionBindingListener; import java.io.Serializable; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; /** * Tracks which HTTP sessions were created by which SAML login, so an IdP-initiated logout can end * exactly the sessions it names (by NameID and SessionIndex), whichever browser delivers the logout * message, and whether or not it carries a session cookie.
* Entries are removed when their session ends (logout, invalidation or timeout). The registry is * held in memory, so sessions restored after a server restart are not tracked.
* This file is part of the Security Shepherd Project. * *

The Security Shepherd project is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software Foundation, either * version 3 of the License, or (at your option) any later version.
* *

The Security Shepherd project is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details.
* *

You should have received a copy of the GNU General Public License along with the Security * Shepherd project. If not, see . */ public class SamlSessionRegistry { private static final Logger log = LogManager.getLogger(SamlSessionRegistry.class); static final String REGISTRATION_ATTRIBUTE = "samlSessionRegistration"; private static final Map sessions = new ConcurrentHashMap<>(); private static class Entry { final HttpSession session; final String nameId; final String sessionIndex; Entry(HttpSession session, String nameId, String sessionIndex) { this.session = session; this.nameId = nameId; this.sessionIndex = sessionIndex; } } /** Session attribute that removes the registry entry when its session ends */ private static class Registration implements HttpSessionBindingListener, Serializable { private static final long serialVersionUID = 1L; private final String key; Registration(String key) { this.key = key; } @Override public void valueUnbound(HttpSessionBindingEvent event) { sessions.remove(key); } } /** * Records that a session belongs to a SAML login. * * @param session The post-login session * @param nameId The complete NameID identity from the assertion (see * SamlSso.registerLoginSession) * @param sessionIndex The SessionIndex from the assertion, if any */ public static void register(HttpSession session, String nameId, String sessionIndex) { String key = UUID.randomUUID().toString(); sessions.put(key, new Entry(session, nameId, sessionIndex)); session.setAttribute(REGISTRATION_ATTRIBUTE, new Registration(key)); } /** * Invalidates the sessions belonging to a SAML login. * * @param nameId The complete NameID identity named in the logout request * @param sessionIndex The SessionIndex named in the logout request, or null to end every session * for the NameID * @return How many sessions were invalidated */ public static int invalidate(String nameId, String sessionIndex) { if (nameId == null) { return 0; } int invalidated = 0; for (Map.Entry item : sessions.entrySet()) { Entry entry = item.getValue(); if (!nameId.equals(entry.nameId) || (sessionIndex != null && !sessionIndex.equals(entry.sessionIndex))) { continue; } sessions.remove(item.getKey()); try { entry.session.invalidate(); invalidated++; } catch (IllegalStateException alreadyInvalidated) { log.debug("Session for SAML logout was already invalidated"); } } return invalidated; } /** * Whether a session is tracked for this login. * * @param nameId The complete NameID identity * @param sessionIndex The SessionIndex * @return true if a live session was registered for exactly this NameID and SessionIndex */ public static boolean isRegistered(String nameId, String sessionIndex) { if (nameId == null || sessionIndex == null) { return false; } for (Entry entry : sessions.values()) { if (nameId.equals(entry.nameId) && sessionIndex.equals(entry.sessionIndex)) { return true; } } return false; } /** Number of tracked sessions; for tests */ static int size() { return sessions.size(); } }