diff --git a/Common/pom.xml b/Common/pom.xml
index 99a5778..b54ed9f 100644
--- a/Common/pom.xml
+++ b/Common/pom.xml
@@ -51,5 +51,11 @@
5.0.4
+
+ org.bouncycastle
+ bcprov-jdk15on
+ 1.70
+
+
-
\ No newline at end of file
+
diff --git a/Common/src/main/java/gov/uspto/session/lifecycle/ConcurrentSessionManager.java b/Common/src/main/java/gov/uspto/session/lifecycle/ConcurrentSessionManager.java
new file mode 100644
index 0000000..299d327
--- /dev/null
+++ b/Common/src/main/java/gov/uspto/session/lifecycle/ConcurrentSessionManager.java
@@ -0,0 +1,164 @@
+package gov.uspto.session.lifecycle;
+
+import gov.uspto.session.management.SessionStore;
+import gov.uspto.session.model.Session;
+import gov.uspto.session.model.SessionState;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Manages concurrent sessions for users.
+ * Enforces concurrent session limits and handles session conflicts.
+ */
+public class ConcurrentSessionManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ConcurrentSessionManager.class);
+
+ private final SessionStore sessionStore;
+ private final int maxConcurrentSessions;
+
+ public ConcurrentSessionManager(SessionStore sessionStore, int maxConcurrentSessions) {
+ this.sessionStore = sessionStore;
+ this.maxConcurrentSessions = maxConcurrentSessions;
+ }
+
+ /**
+ * Get all active sessions for a user
+ * @param userId the user ID
+ * @return array of active sessions
+ */
+ public Session[] getActiveSessions(String userId) {
+ Session[] allSessions = sessionStore.findByUserId(userId);
+ List activeSessions = new ArrayList<>();
+
+ for (Session session : allSessions) {
+ if (session.getState() == SessionState.ACTIVE ||
+ session.getState() == SessionState.REQUIRES_REAUTH) {
+ activeSessions.add(session);
+ }
+ }
+
+ return activeSessions.toArray(new Session[0]);
+ }
+
+ /**
+ * Get count of active sessions for a user
+ * @param userId the user ID
+ * @return number of active sessions
+ */
+ public int getActiveSessionCount(String userId) {
+ return sessionStore.countActiveSessionsForUser(userId);
+ }
+
+ /**
+ * Check if user has reached concurrent session limit
+ * @param userId the user ID
+ * @return true if limit reached
+ */
+ public boolean hasReachedLimit(String userId) {
+ int activeCount = getActiveSessionCount(userId);
+ return activeCount >= maxConcurrentSessions;
+ }
+
+ /**
+ * Terminate oldest session if limit is exceeded
+ * @param userId the user ID
+ * @return true if a session was terminated
+ */
+ public boolean terminateOldestIfLimitExceeded(String userId) {
+ Session[] activeSessions = getActiveSessions(userId);
+
+ if (activeSessions.length >= maxConcurrentSessions) {
+ Session oldestSession = findOldestSession(activeSessions);
+ if (oldestSession != null) {
+ oldestSession.setState(SessionState.TERMINATED);
+ sessionStore.save(oldestSession);
+ LOGGER.info("Terminated oldest session {} for user {} due to concurrent session limit",
+ oldestSession.getSessionId(), userId);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Terminate all sessions except the specified one
+ * @param userId the user ID
+ * @param keepSessionId the session ID to keep
+ * @return number of sessions terminated
+ */
+ public int terminateAllExcept(String userId, String keepSessionId) {
+ Session[] allSessions = sessionStore.findByUserId(userId);
+ int terminatedCount = 0;
+
+ for (Session session : allSessions) {
+ if (!session.getSessionId().equals(keepSessionId) &&
+ session.getState() != SessionState.TERMINATED) {
+ session.setState(SessionState.TERMINATED);
+ sessionStore.save(session);
+ terminatedCount++;
+ }
+ }
+
+ LOGGER.info("Terminated {} sessions for user {}, keeping session {}",
+ terminatedCount, userId, keepSessionId);
+ return terminatedCount;
+ }
+
+ /**
+ * Get session information for all active sessions
+ * @param userId the user ID
+ * @return array of session info strings
+ */
+ public String[] getSessionInfo(String userId) {
+ Session[] activeSessions = getActiveSessions(userId);
+ String[] info = new String[activeSessions.length];
+
+ for (int i = 0; i < activeSessions.length; i++) {
+ Session session = activeSessions[i];
+ info[i] = String.format("Session %s: created=%s, lastAccessed=%s, IP=%s",
+ session.getSessionId(),
+ session.getCreatedAt(),
+ session.getLastAccessed(),
+ session.getIpAddress());
+ }
+
+ return info;
+ }
+
+ /**
+ * Find the oldest session by creation time
+ * @param sessions array of sessions
+ * @return oldest session
+ */
+ private Session findOldestSession(Session[] sessions) {
+ if (sessions == null || sessions.length == 0) {
+ return null;
+ }
+
+ return Arrays.stream(sessions)
+ .min(Comparator.comparing(Session::getCreatedAt))
+ .orElse(null);
+ }
+
+ /**
+ * Find the least recently accessed session
+ * @param sessions array of sessions
+ * @return least recently accessed session
+ */
+ private Session findLeastRecentlyAccessedSession(Session[] sessions) {
+ if (sessions == null || sessions.length == 0) {
+ return null;
+ }
+
+ return Arrays.stream(sessions)
+ .min(Comparator.comparing(Session::getLastAccessed))
+ .orElse(null);
+ }
+}
diff --git a/Common/src/main/java/gov/uspto/session/lifecycle/SessionCreationService.java b/Common/src/main/java/gov/uspto/session/lifecycle/SessionCreationService.java
new file mode 100644
index 0000000..16f378f
--- /dev/null
+++ b/Common/src/main/java/gov/uspto/session/lifecycle/SessionCreationService.java
@@ -0,0 +1,94 @@
+package gov.uspto.session.lifecycle;
+
+import gov.uspto.session.management.SessionFactory;
+import gov.uspto.session.management.SessionStore;
+import gov.uspto.session.model.Session;
+import gov.uspto.session.security.SessionHijackingPrevention;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Service for creating new sessions with security controls.
+ * Enforces concurrent session limits and security policies.
+ */
+public class SessionCreationService {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(SessionCreationService.class);
+
+ private final SessionFactory sessionFactory;
+ private final SessionStore sessionStore;
+ private final SessionHijackingPrevention hijackingPrevention;
+
+ public SessionCreationService(SessionFactory sessionFactory,
+ SessionStore sessionStore,
+ SessionHijackingPrevention hijackingPrevention) {
+ this.sessionFactory = sessionFactory;
+ this.sessionStore = sessionStore;
+ this.hijackingPrevention = hijackingPrevention;
+ }
+
+ /**
+ * Create a new session for a user
+ * @param userId the user ID
+ * @return the created session
+ * @throws SessionCreationException if session cannot be created
+ */
+ public Session createSession(String userId) throws SessionCreationException {
+ validateConcurrentSessionLimit(userId);
+
+ Session session = sessionFactory.createSession(userId);
+ sessionStore.save(session);
+
+ LOGGER.info("Created session {} for user {}", session.getSessionId(), userId);
+ return session;
+ }
+
+ /**
+ * Create a new session with security context
+ * @param userId the user ID
+ * @param ipAddress client IP address
+ * @param userAgent client user agent
+ * @return the created session
+ * @throws SessionCreationException if session cannot be created
+ */
+ public Session createSession(String userId, String ipAddress, String userAgent)
+ throws SessionCreationException {
+ validateConcurrentSessionLimit(userId);
+
+ Session session = sessionFactory.createSession(userId, ipAddress, userAgent);
+ sessionStore.save(session);
+
+ LOGGER.info("Created session {} for user {} from IP {}",
+ session.getSessionId(), userId, ipAddress);
+ return session;
+ }
+
+ /**
+ * Validate concurrent session limit
+ * @param userId the user ID
+ * @throws SessionCreationException if limit exceeded
+ */
+ private void validateConcurrentSessionLimit(String userId) throws SessionCreationException {
+ int activeSessionCount = sessionStore.countActiveSessionsForUser(userId);
+
+ if (hijackingPrevention.isConcurrentSessionLimitExceeded(activeSessionCount)) {
+ LOGGER.warn("Concurrent session limit exceeded for user {}: {} active sessions",
+ userId, activeSessionCount);
+ throw new SessionCreationException(
+ "Concurrent session limit exceeded for user: " + userId);
+ }
+ }
+
+ /**
+ * Exception thrown when session creation fails
+ */
+ public static class SessionCreationException extends Exception {
+ public SessionCreationException(String message) {
+ super(message);
+ }
+
+ public SessionCreationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+}
diff --git a/Common/src/main/java/gov/uspto/session/lifecycle/SessionRenewalService.java b/Common/src/main/java/gov/uspto/session/lifecycle/SessionRenewalService.java
new file mode 100644
index 0000000..b403d31
--- /dev/null
+++ b/Common/src/main/java/gov/uspto/session/lifecycle/SessionRenewalService.java
@@ -0,0 +1,141 @@
+package gov.uspto.session.lifecycle;
+
+import gov.uspto.session.management.SessionStore;
+import gov.uspto.session.model.Session;
+import gov.uspto.session.model.SessionState;
+import gov.uspto.session.security.SessionIdGenerator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Optional;
+
+/**
+ * Service for renewing and refreshing sessions.
+ * Handles session extension and ID regeneration for security.
+ */
+public class SessionRenewalService {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(SessionRenewalService.class);
+
+ private final SessionStore sessionStore;
+ private final SessionIdGenerator idGenerator;
+
+ public SessionRenewalService(SessionStore sessionStore, SessionIdGenerator idGenerator) {
+ this.sessionStore = sessionStore;
+ this.idGenerator = idGenerator;
+ }
+
+ /**
+ * Renew a session by updating its last accessed time
+ * @param sessionId the session ID
+ * @return true if session was renewed
+ */
+ public boolean renewSession(String sessionId) {
+ Optional sessionOpt = sessionStore.findById(sessionId);
+
+ if (!sessionOpt.isPresent()) {
+ LOGGER.warn("Cannot renew session {}: not found", sessionId);
+ return false;
+ }
+
+ Session session = sessionOpt.get();
+
+ if (session.getState() != SessionState.ACTIVE &&
+ session.getState() != SessionState.REQUIRES_REAUTH) {
+ LOGGER.warn("Cannot renew session {}: invalid state {}", sessionId, session.getState());
+ return false;
+ }
+
+ session.updateLastAccessed();
+ sessionStore.save(session);
+
+ LOGGER.debug("Renewed session {}", sessionId);
+ return true;
+ }
+
+ /**
+ * Regenerate session ID for security (prevents fixation attacks)
+ * @param oldSessionId the old session ID
+ * @return the new session ID, or null if regeneration failed
+ */
+ public String regenerateSessionId(String oldSessionId) {
+ Optional sessionOpt = sessionStore.findById(oldSessionId);
+
+ if (!sessionOpt.isPresent()) {
+ LOGGER.warn("Cannot regenerate session ID for {}: not found", oldSessionId);
+ return null;
+ }
+
+ Session oldSession = sessionOpt.get();
+ String newSessionId = idGenerator.generateSessionId();
+
+ Session newSession = new Session(newSessionId, oldSession.getUserId());
+ newSession.setState(oldSession.getState());
+ newSession.setIpAddress(oldSession.getIpAddress());
+ newSession.setUserAgent(oldSession.getUserAgent());
+
+ for (String key : oldSession.getAttributes().keySet()) {
+ newSession.setAttribute(key, oldSession.getAttribute(key));
+ }
+
+ for (String key : oldSession.getSecurityAttributes().keySet()) {
+ newSession.setSecurityAttribute(key, oldSession.getSecurityAttribute(key));
+ }
+
+ sessionStore.save(newSession);
+ sessionStore.delete(oldSessionId);
+
+ LOGGER.info("Regenerated session ID: {} -> {}", oldSessionId, newSessionId);
+ return newSessionId;
+ }
+
+ /**
+ * Refresh session after re-authentication
+ * @param sessionId the session ID
+ * @return true if session was refreshed
+ */
+ public boolean refreshAfterReauth(String sessionId) {
+ Optional sessionOpt = sessionStore.findById(sessionId);
+
+ if (!sessionOpt.isPresent()) {
+ LOGGER.warn("Cannot refresh session {}: not found", sessionId);
+ return false;
+ }
+
+ Session session = sessionOpt.get();
+ session.markReauthenticated();
+ session.updateLastAccessed();
+ sessionStore.save(session);
+
+ LOGGER.info("Refreshed session {} after re-authentication", sessionId);
+ return true;
+ }
+
+ /**
+ * Extend session lifetime
+ * @param sessionId the session ID
+ * @return true if session was extended
+ */
+ public boolean extendSession(String sessionId) {
+ Optional sessionOpt = sessionStore.findById(sessionId);
+
+ if (!sessionOpt.isPresent()) {
+ LOGGER.warn("Cannot extend session {}: not found", sessionId);
+ return false;
+ }
+
+ Session session = sessionOpt.get();
+
+ if (session.getState() == SessionState.EXPIRED ||
+ session.getState() == SessionState.TERMINATED) {
+ LOGGER.warn("Cannot extend session {}: invalid state {}", sessionId, session.getState());
+ return false;
+ }
+
+ session.updateLastAccessed();
+ sessionStore.save(session);
+
+ LOGGER.debug("Extended session {}", sessionId);
+ return true;
+ }
+}
diff --git a/Common/src/main/java/gov/uspto/session/lifecycle/SessionTerminationService.java b/Common/src/main/java/gov/uspto/session/lifecycle/SessionTerminationService.java
new file mode 100644
index 0000000..5a34240
--- /dev/null
+++ b/Common/src/main/java/gov/uspto/session/lifecycle/SessionTerminationService.java
@@ -0,0 +1,105 @@
+package gov.uspto.session.lifecycle;
+
+import gov.uspto.session.management.SessionStore;
+import gov.uspto.session.model.Session;
+import gov.uspto.session.model.SessionState;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Optional;
+
+/**
+ * Service for terminating sessions and cleanup.
+ * Handles graceful session termination and resource cleanup.
+ */
+public class SessionTerminationService {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(SessionTerminationService.class);
+
+ private final SessionStore sessionStore;
+
+ public SessionTerminationService(SessionStore sessionStore) {
+ this.sessionStore = sessionStore;
+ }
+
+ /**
+ * Terminate a session
+ * @param sessionId the session ID
+ * @return true if session was terminated
+ */
+ public boolean terminateSession(String sessionId) {
+ Optional sessionOpt = sessionStore.findById(sessionId);
+
+ if (!sessionOpt.isPresent()) {
+ LOGGER.warn("Cannot terminate session {}: not found", sessionId);
+ return false;
+ }
+
+ Session session = sessionOpt.get();
+ session.setState(SessionState.TERMINATED);
+ sessionStore.save(session);
+
+ LOGGER.info("Terminated session {} for user {}", sessionId, session.getUserId());
+ return true;
+ }
+
+ /**
+ * Terminate all sessions for a user
+ * @param userId the user ID
+ * @return number of sessions terminated
+ */
+ public int terminateAllUserSessions(String userId) {
+ Session[] sessions = sessionStore.findByUserId(userId);
+ int terminatedCount = 0;
+
+ for (Session session : sessions) {
+ if (session.getState() != SessionState.TERMINATED) {
+ session.setState(SessionState.TERMINATED);
+ sessionStore.save(session);
+ terminatedCount++;
+ }
+ }
+
+ LOGGER.info("Terminated {} sessions for user {}", terminatedCount, userId);
+ return terminatedCount;
+ }
+
+ /**
+ * Delete a session from storage
+ * @param sessionId the session ID
+ */
+ public void deleteSession(String sessionId) {
+ sessionStore.delete(sessionId);
+ LOGGER.info("Deleted session {}", sessionId);
+ }
+
+ /**
+ * Delete all sessions for a user
+ * @param userId the user ID
+ */
+ public void deleteAllUserSessions(String userId) {
+ sessionStore.deleteByUserId(userId);
+ LOGGER.info("Deleted all sessions for user {}", userId);
+ }
+
+ /**
+ * Expire a session
+ * @param sessionId the session ID
+ * @return true if session was expired
+ */
+ public boolean expireSession(String sessionId) {
+ Optional sessionOpt = sessionStore.findById(sessionId);
+
+ if (!sessionOpt.isPresent()) {
+ LOGGER.warn("Cannot expire session {}: not found", sessionId);
+ return false;
+ }
+
+ Session session = sessionOpt.get();
+ session.setState(SessionState.EXPIRED);
+ sessionStore.save(session);
+
+ LOGGER.info("Expired session {} for user {}", sessionId, session.getUserId());
+ return true;
+ }
+}
diff --git a/Common/src/main/java/gov/uspto/session/management/SessionFactory.java b/Common/src/main/java/gov/uspto/session/management/SessionFactory.java
new file mode 100644
index 0000000..498a362
--- /dev/null
+++ b/Common/src/main/java/gov/uspto/session/management/SessionFactory.java
@@ -0,0 +1,42 @@
+package gov.uspto.session.management;
+
+import gov.uspto.session.model.Session;
+import gov.uspto.session.security.SessionIdGenerator;
+
+/**
+ * Factory for creating new Session instances.
+ * Handles session ID generation and initial session setup.
+ */
+public class SessionFactory {
+
+ private final SessionIdGenerator idGenerator;
+
+ public SessionFactory(SessionIdGenerator idGenerator) {
+ this.idGenerator = idGenerator;
+ }
+
+ /**
+ * Create a new session for a user
+ * @param userId the user ID
+ * @return new Session instance
+ */
+ public Session createSession(String userId) {
+ String sessionId = idGenerator.generateSessionId();
+ Session session = new Session(sessionId, userId);
+ return session;
+ }
+
+ /**
+ * Create a new session with security context
+ * @param userId the user ID
+ * @param ipAddress client IP address
+ * @param userAgent client user agent
+ * @return new Session instance with security context
+ */
+ public Session createSession(String userId, String ipAddress, String userAgent) {
+ Session session = createSession(userId);
+ session.setIpAddress(ipAddress);
+ session.setUserAgent(userAgent);
+ return session;
+ }
+}
diff --git a/Common/src/main/java/gov/uspto/session/management/SessionManager.java b/Common/src/main/java/gov/uspto/session/management/SessionManager.java
new file mode 100644
index 0000000..541262b
--- /dev/null
+++ b/Common/src/main/java/gov/uspto/session/management/SessionManager.java
@@ -0,0 +1,189 @@
+package gov.uspto.session.management;
+
+import gov.uspto.session.model.ReauthReason;
+import gov.uspto.session.model.Session;
+import gov.uspto.session.model.SessionState;
+import gov.uspto.session.reauth.ReauthenticationPolicy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Optional;
+
+/**
+ * Main session orchestrator.
+ * Coordinates session lifecycle, validation, and re-authentication.
+ * NIST 800-53 IA-11 compliant session management.
+ */
+public class SessionManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(SessionManager.class);
+
+ private final SessionStore sessionStore;
+ private final SessionFactory sessionFactory;
+ private final SessionValidator sessionValidator;
+ private final ReauthenticationPolicy reauthPolicy;
+
+ public SessionManager(SessionStore sessionStore,
+ SessionFactory sessionFactory,
+ SessionValidator sessionValidator,
+ ReauthenticationPolicy reauthPolicy) {
+ this.sessionStore = sessionStore;
+ this.sessionFactory = sessionFactory;
+ this.sessionValidator = sessionValidator;
+ this.reauthPolicy = reauthPolicy;
+ }
+
+ /**
+ * Create a new session for a user
+ * @param userId the user ID (placeholder for Part 1.2 integration)
+ * @return the created session
+ */
+ public Session createSession(String userId) {
+ Session session = sessionFactory.createSession(userId);
+ sessionStore.save(session);
+ LOGGER.info("Created session {} for user {}", session.getSessionId(), userId);
+ return session;
+ }
+
+ /**
+ * Create a new session with security context
+ * @param userId the user ID
+ * @param ipAddress client IP address
+ * @param userAgent client user agent
+ * @return the created session
+ */
+ public Session createSession(String userId, String ipAddress, String userAgent) {
+ Session session = sessionFactory.createSession(userId, ipAddress, userAgent);
+ sessionStore.save(session);
+ LOGGER.info("Created session {} for user {} from IP {}",
+ session.getSessionId(), userId, ipAddress);
+ return session;
+ }
+
+ /**
+ * Retrieve a session by ID
+ * @param sessionId the session ID
+ * @return Optional containing the session if found and valid
+ */
+ public Optional getSession(String sessionId) {
+ Optional sessionOpt = sessionStore.findById(sessionId);
+
+ if (!sessionOpt.isPresent()) {
+ LOGGER.debug("Session {} not found", sessionId);
+ return Optional.empty();
+ }
+
+ Session session = sessionOpt.get();
+
+ if (!sessionValidator.isValid(session)) {
+ LOGGER.info("Session {} is invalid or expired", sessionId);
+ session.setState(SessionState.EXPIRED);
+ sessionStore.save(session);
+ return Optional.empty();
+ }
+
+ return Optional.of(session);
+ }
+
+ /**
+ * Validate a session
+ * @param sessionId the session ID
+ * @return true if session is valid
+ */
+ public boolean validateSession(String sessionId) {
+ Optional sessionOpt = getSession(sessionId);
+ return sessionOpt.isPresent();
+ }
+
+ /**
+ * Update session access time
+ * @param sessionId the session ID
+ */
+ public void touchSession(String sessionId) {
+ Optional sessionOpt = sessionStore.findById(sessionId);
+ if (sessionOpt.isPresent()) {
+ Session session = sessionOpt.get();
+ session.updateLastAccessed();
+ sessionStore.save(session);
+ }
+ }
+
+ /**
+ * Trigger re-authentication for a session
+ * @param sessionId the session ID
+ * @param reason the re-authentication reason
+ */
+ public void triggerReauthentication(String sessionId, ReauthReason reason) {
+ Optional sessionOpt = sessionStore.findById(sessionId);
+ if (sessionOpt.isPresent()) {
+ Session session = sessionOpt.get();
+ session.addReauthReason(reason);
+ sessionStore.save(session);
+ LOGGER.info("Triggered re-authentication for session {} due to {}",
+ sessionId, reason);
+ }
+ }
+
+ /**
+ * Check if session requires re-authentication
+ * @param session the session
+ * @return true if re-authentication is required
+ */
+ public boolean isReauthenticationRequired(Session session) {
+ if (session.requiresReauthentication()) {
+ return true;
+ }
+
+ return reauthPolicy.requiresReauthentication(session);
+ }
+
+ /**
+ * Mark session as re-authenticated
+ * @param sessionId the session ID
+ */
+ public void markReauthenticated(String sessionId) {
+ Optional sessionOpt = sessionStore.findById(sessionId);
+ if (sessionOpt.isPresent()) {
+ Session session = sessionOpt.get();
+ session.markReauthenticated();
+ sessionStore.save(session);
+ LOGGER.info("Session {} re-authenticated", sessionId);
+ }
+ }
+
+ /**
+ * Terminate a session
+ * @param sessionId the session ID
+ */
+ public void terminateSession(String sessionId) {
+ Optional sessionOpt = sessionStore.findById(sessionId);
+ if (sessionOpt.isPresent()) {
+ Session session = sessionOpt.get();
+ session.setState(SessionState.TERMINATED);
+ sessionStore.save(session);
+ LOGGER.info("Terminated session {}", sessionId);
+ }
+ }
+
+ /**
+ * Terminate all sessions for a user
+ * @param userId the user ID
+ */
+ public void terminateAllUserSessions(String userId) {
+ Session[] sessions = sessionStore.findByUserId(userId);
+ for (Session session : sessions) {
+ session.setState(SessionState.TERMINATED);
+ sessionStore.save(session);
+ }
+ LOGGER.info("Terminated all sessions for user {}", userId);
+ }
+
+ /**
+ * Get count of active sessions for a user
+ * @param userId the user ID
+ * @return number of active sessions
+ */
+ public int getActiveSessionCount(String userId) {
+ return sessionStore.countActiveSessionsForUser(userId);
+ }
+}
diff --git a/Common/src/main/java/gov/uspto/session/management/SessionStore.java b/Common/src/main/java/gov/uspto/session/management/SessionStore.java
new file mode 100644
index 0000000..2171c8e
--- /dev/null
+++ b/Common/src/main/java/gov/uspto/session/management/SessionStore.java
@@ -0,0 +1,59 @@
+package gov.uspto.session.management;
+
+import gov.uspto.session.model.Session;
+
+import java.util.Optional;
+
+/**
+ * Interface for session persistence.
+ * Implementations can support Redis, database, in-memory, or other storage backends.
+ * Designed for future integration with Part 1.1 infrastructure.
+ */
+public interface SessionStore {
+
+ /**
+ * Store a session
+ * @param session the session to store
+ */
+ void save(Session session);
+
+ /**
+ * Retrieve a session by ID
+ * @param sessionId the session ID
+ * @return Optional containing the session if found
+ */
+ Optional findById(String sessionId);
+
+ /**
+ * Retrieve all sessions for a user
+ * @param userId the user ID
+ * @return array of sessions for the user
+ */
+ Session[] findByUserId(String userId);
+
+ /**
+ * Delete a session
+ * @param sessionId the session ID to delete
+ */
+ void delete(String sessionId);
+
+ /**
+ * Delete all sessions for a user
+ * @param userId the user ID
+ */
+ void deleteByUserId(String userId);
+
+ /**
+ * Check if a session exists
+ * @param sessionId the session ID
+ * @return true if session exists
+ */
+ boolean exists(String sessionId);
+
+ /**
+ * Get count of active sessions for a user
+ * @param userId the user ID
+ * @return number of active sessions
+ */
+ int countActiveSessionsForUser(String userId);
+}
diff --git a/Common/src/main/java/gov/uspto/session/management/SessionValidator.java b/Common/src/main/java/gov/uspto/session/management/SessionValidator.java
new file mode 100644
index 0000000..6c73436
--- /dev/null
+++ b/Common/src/main/java/gov/uspto/session/management/SessionValidator.java
@@ -0,0 +1,84 @@
+package gov.uspto.session.management;
+
+import gov.uspto.session.model.Session;
+import gov.uspto.session.model.SessionState;
+
+/**
+ * Validates session state and integrity.
+ * Checks session validity, expiration, and security constraints.
+ */
+public class SessionValidator {
+
+ private final long maxSessionAgeSeconds;
+ private final long maxInactivitySeconds;
+
+ public SessionValidator(long maxSessionAgeSeconds, long maxInactivitySeconds) {
+ this.maxSessionAgeSeconds = maxSessionAgeSeconds;
+ this.maxInactivitySeconds = maxInactivitySeconds;
+ }
+
+ /**
+ * Validate if session is still valid
+ * @param session the session to validate
+ * @return true if session is valid
+ */
+ public boolean isValid(Session session) {
+ if (session == null) {
+ return false;
+ }
+
+ if (session.getState() == SessionState.TERMINATED ||
+ session.getState() == SessionState.EXPIRED) {
+ return false;
+ }
+
+ if (isExpired(session)) {
+ return false;
+ }
+
+ if (isInactive(session)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Check if session has exceeded maximum age
+ * @param session the session to check
+ * @return true if session is expired
+ */
+ public boolean isExpired(Session session) {
+ long sessionAge = session.getSessionDurationSeconds();
+ return Math.abs(sessionAge) > maxSessionAgeSeconds;
+ }
+
+ /**
+ * Check if session has been inactive too long
+ * @param session the session to check
+ * @return true if session is inactive
+ */
+ public boolean isInactive(Session session) {
+ long inactivityTime = session.getTimeSinceLastAccessSeconds();
+ return inactivityTime > maxInactivitySeconds;
+ }
+
+ /**
+ * Validate session security context
+ * @param session the session
+ * @param currentIpAddress current request IP
+ * @param currentUserAgent current request user agent
+ * @return true if security context matches
+ */
+ public boolean validateSecurityContext(Session session, String currentIpAddress, String currentUserAgent) {
+ if (session.getIpAddress() != null && !session.getIpAddress().equals(currentIpAddress)) {
+ return false;
+ }
+
+ if (session.getUserAgent() != null && !session.getUserAgent().equals(currentUserAgent)) {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/Common/src/main/java/gov/uspto/session/model/ReauthReason.java b/Common/src/main/java/gov/uspto/session/model/ReauthReason.java
new file mode 100644
index 0000000..41d548b
--- /dev/null
+++ b/Common/src/main/java/gov/uspto/session/model/ReauthReason.java
@@ -0,0 +1,48 @@
+package gov.uspto.session.model;
+
+/**
+ * NIST 800-53 IA-11 compliant re-authentication reasons.
+ * Defines circumstances requiring user re-authentication.
+ */
+public enum ReauthReason {
+
+ /**
+ * Session timeout - time-based expiration
+ */
+ SESSION_TIMEOUT,
+
+ /**
+ * Privilege escalation - user attempting to access higher privilege resources
+ */
+ PRIVILEGE_ESCALATION,
+
+ /**
+ * Role change - user's role or permissions have changed
+ */
+ ROLE_CHANGE,
+
+ /**
+ * Security attribute change - security-relevant attributes modified
+ */
+ SECURITY_ATTRIBUTE_CHANGE,
+
+ /**
+ * Organization-defined circumstance - configurable policy trigger
+ */
+ ORGANIZATION_DEFINED,
+
+ /**
+ * Suspicious activity detected
+ */
+ SUSPICIOUS_ACTIVITY,
+
+ /**
+ * Manual re-authentication request
+ */
+ MANUAL_REQUEST,
+
+ /**
+ * Session renewal required
+ */
+ SESSION_RENEWAL
+}
diff --git a/Common/src/main/java/gov/uspto/session/model/Session.java b/Common/src/main/java/gov/uspto/session/model/Session.java
new file mode 100644
index 0000000..4cf4032
--- /dev/null
+++ b/Common/src/main/java/gov/uspto/session/model/Session.java
@@ -0,0 +1,179 @@
+package gov.uspto.session.model;
+
+import java.time.Instant;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Core session entity with NIST 800-53 IA-11 compliance fields.
+ * Represents an authenticated user session with re-authentication tracking.
+ */
+public class Session {
+
+ private final String sessionId;
+ private final String userId;
+ private final Instant createdAt;
+ private Instant lastAccessed;
+ private Instant lastReauthentication;
+ private SessionState state;
+
+ private final Map attributes;
+ private final Map securityAttributes;
+ private final Set pendingReauthReasons;
+
+ private String ipAddress;
+ private String userAgent;
+ private int accessCount;
+
+ public Session(String sessionId, String userId) {
+ this.sessionId = sessionId;
+ this.userId = userId;
+ this.createdAt = Instant.now();
+ this.lastAccessed = Instant.now();
+ this.lastReauthentication = Instant.now();
+ this.state = SessionState.ACTIVE;
+ this.attributes = new HashMap<>();
+ this.securityAttributes = new HashMap<>();
+ this.pendingReauthReasons = new HashSet<>();
+ this.accessCount = 0;
+ }
+
+ public String getSessionId() {
+ return sessionId;
+ }
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public Instant getCreatedAt() {
+ return createdAt;
+ }
+
+ public Instant getLastAccessed() {
+ return lastAccessed;
+ }
+
+ public void updateLastAccessed() {
+ this.lastAccessed = Instant.now();
+ this.accessCount++;
+ }
+
+ public Instant getLastReauthentication() {
+ return lastReauthentication;
+ }
+
+ public void markReauthenticated() {
+ this.lastReauthentication = Instant.now();
+ this.pendingReauthReasons.clear();
+ if (this.state == SessionState.REQUIRES_REAUTH) {
+ this.state = SessionState.ACTIVE;
+ }
+ }
+
+ public SessionState getState() {
+ return state;
+ }
+
+ public void setState(SessionState state) {
+ this.state = state;
+ }
+
+ public boolean isActive() {
+ return state == SessionState.ACTIVE;
+ }
+
+ public boolean requiresReauthentication() {
+ return state == SessionState.REQUIRES_REAUTH || !pendingReauthReasons.isEmpty();
+ }
+
+ public Set getPendingReauthReasons() {
+ return Collections.unmodifiableSet(pendingReauthReasons);
+ }
+
+ public void addReauthReason(ReauthReason reason) {
+ this.pendingReauthReasons.add(reason);
+ if (this.state == SessionState.ACTIVE) {
+ this.state = SessionState.REQUIRES_REAUTH;
+ }
+ }
+
+ public void clearReauthReasons() {
+ this.pendingReauthReasons.clear();
+ }
+
+ public Map getAttributes() {
+ return Collections.unmodifiableMap(attributes);
+ }
+
+ public Object getAttribute(String key) {
+ return attributes.get(key);
+ }
+
+ public void setAttribute(String key, Object value) {
+ attributes.put(key, value);
+ }
+
+ public void removeAttribute(String key) {
+ attributes.remove(key);
+ }
+
+ public Map getSecurityAttributes() {
+ return Collections.unmodifiableMap(securityAttributes);
+ }
+
+ public Object getSecurityAttribute(String key) {
+ return securityAttributes.get(key);
+ }
+
+ public void setSecurityAttribute(String key, Object value) {
+ securityAttributes.put(key, value);
+ }
+
+ public String getIpAddress() {
+ return ipAddress;
+ }
+
+ public void setIpAddress(String ipAddress) {
+ this.ipAddress = ipAddress;
+ }
+
+ public String getUserAgent() {
+ return userAgent;
+ }
+
+ public void setUserAgent(String userAgent) {
+ this.userAgent = userAgent;
+ }
+
+ public int getAccessCount() {
+ return accessCount;
+ }
+
+ public long getSessionDurationSeconds() {
+ return createdAt.getEpochSecond() - Instant.now().getEpochSecond();
+ }
+
+ public long getTimeSinceLastAccessSeconds() {
+ return Instant.now().getEpochSecond() - lastAccessed.getEpochSecond();
+ }
+
+ public long getTimeSinceLastReauthSeconds() {
+ return Instant.now().getEpochSecond() - lastReauthentication.getEpochSecond();
+ }
+
+ @Override
+ public String toString() {
+ return "Session{" +
+ "sessionId='" + sessionId + '\'' +
+ ", userId='" + userId + '\'' +
+ ", state=" + state +
+ ", createdAt=" + createdAt +
+ ", lastAccessed=" + lastAccessed +
+ ", requiresReauth=" + requiresReauthentication() +
+ '}';
+ }
+}
diff --git a/Common/src/main/java/gov/uspto/session/model/SessionState.java b/Common/src/main/java/gov/uspto/session/model/SessionState.java
new file mode 100644
index 0000000..77cc0b1
--- /dev/null
+++ b/Common/src/main/java/gov/uspto/session/model/SessionState.java
@@ -0,0 +1,32 @@
+package gov.uspto.session.model;
+
+/**
+ * Session lifecycle states
+ */
+public enum SessionState {
+
+ /**
+ * Session is active and valid
+ */
+ ACTIVE,
+
+ /**
+ * Session requires re-authentication
+ */
+ REQUIRES_REAUTH,
+
+ /**
+ * Session has expired
+ */
+ EXPIRED,
+
+ /**
+ * Session has been terminated
+ */
+ TERMINATED,
+
+ /**
+ * Session is suspended (temporarily inactive)
+ */
+ SUSPENDED
+}
diff --git a/Common/src/main/java/gov/uspto/session/reauth/PrivilegeChangeDetector.java b/Common/src/main/java/gov/uspto/session/reauth/PrivilegeChangeDetector.java
new file mode 100644
index 0000000..935ee29
--- /dev/null
+++ b/Common/src/main/java/gov/uspto/session/reauth/PrivilegeChangeDetector.java
@@ -0,0 +1,106 @@
+package gov.uspto.session.reauth;
+
+import gov.uspto.session.model.Session;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Detects privilege and role changes that require re-authentication.
+ * Placeholder for Part 1.2 (Authenticator Management) integration.
+ */
+public class PrivilegeChangeDetector {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(PrivilegeChangeDetector.class);
+
+ private final ReauthenticationTrigger reauthTrigger;
+
+ public PrivilegeChangeDetector(ReauthenticationTrigger reauthTrigger) {
+ this.reauthTrigger = reauthTrigger;
+ }
+
+ /**
+ * Check if user is attempting privilege escalation
+ * @param session the session
+ * @param requestedPrivilege the privilege being requested
+ * @return true if privilege escalation detected
+ */
+ public boolean detectPrivilegeEscalation(Session session, String requestedPrivilege) {
+ Set currentPrivileges = getCurrentPrivileges(session);
+
+ if (!currentPrivileges.contains(requestedPrivilege)) {
+ LOGGER.info("Privilege escalation detected for session {}: requesting {}",
+ session.getSessionId(), requestedPrivilege);
+ reauthTrigger.triggerPrivilegeEscalation(session);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if user's role has changed
+ * @param session the session
+ * @param newRole the new role
+ * @return true if role change detected
+ */
+ public boolean detectRoleChange(Session session, String newRole) {
+ String currentRole = getCurrentRole(session);
+
+ if (currentRole != null && !currentRole.equals(newRole)) {
+ LOGGER.info("Role change detected for session {}: {} -> {}",
+ session.getSessionId(), currentRole, newRole);
+ reauthTrigger.triggerRoleChange(session);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if security attributes have changed
+ * @param session the session
+ * @param attributeKey the security attribute key
+ * @param newValue the new value
+ * @return true if security attribute change detected
+ */
+ public boolean detectSecurityAttributeChange(Session session, String attributeKey, Object newValue) {
+ Object currentValue = session.getSecurityAttribute(attributeKey);
+
+ if (currentValue != null && !currentValue.equals(newValue)) {
+ LOGGER.info("Security attribute change detected for session {}: {} changed",
+ session.getSessionId(), attributeKey);
+ reauthTrigger.triggerSecurityAttributeChange(session);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Get current privileges for session
+ * Placeholder for Part 1.2 integration
+ * @param session the session
+ * @return set of current privileges
+ */
+ private Set getCurrentPrivileges(Session session) {
+ Object privileges = session.getSecurityAttribute("privileges");
+ if (privileges instanceof Set) {
+ return (Set) privileges;
+ }
+ return new HashSet<>();
+ }
+
+ /**
+ * Get current role for session
+ * Placeholder for Part 1.2 integration
+ * @param session the session
+ * @return current role
+ */
+ private String getCurrentRole(Session session) {
+ Object role = session.getSecurityAttribute("role");
+ return role != null ? role.toString() : null;
+ }
+}
diff --git a/Common/src/main/java/gov/uspto/session/reauth/ReauthenticationPolicy.java b/Common/src/main/java/gov/uspto/session/reauth/ReauthenticationPolicy.java
new file mode 100644
index 0000000..18dc894
--- /dev/null
+++ b/Common/src/main/java/gov/uspto/session/reauth/ReauthenticationPolicy.java
@@ -0,0 +1,127 @@
+package gov.uspto.session.reauth;
+
+import gov.uspto.session.model.ReauthReason;
+import gov.uspto.session.model.Session;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * NIST 800-53 IA-11 compliant re-authentication policy.
+ * Defines when re-authentication is required based on configurable rules.
+ */
+public class ReauthenticationPolicy {
+
+ private final long reauthTimeoutSeconds;
+ private final boolean requireReauthOnPrivilegeEscalation;
+ private final boolean requireReauthOnRoleChange;
+ private final boolean requireReauthOnSecurityAttributeChange;
+ private final Map organizationDefinedPolicies;
+
+ private ReauthenticationPolicy(Builder builder) {
+ this.reauthTimeoutSeconds = builder.reauthTimeoutSeconds;
+ this.requireReauthOnPrivilegeEscalation = builder.requireReauthOnPrivilegeEscalation;
+ this.requireReauthOnRoleChange = builder.requireReauthOnRoleChange;
+ this.requireReauthOnSecurityAttributeChange = builder.requireReauthOnSecurityAttributeChange;
+ this.organizationDefinedPolicies = builder.organizationDefinedPolicies;
+ }
+
+ /**
+ * Check if session requires re-authentication based on policy
+ * @param session the session to check
+ * @return true if re-authentication is required
+ */
+ public boolean requiresReauthentication(Session session) {
+ if (session.requiresReauthentication()) {
+ return true;
+ }
+
+ long timeSinceReauth = session.getTimeSinceLastReauthSeconds();
+ if (timeSinceReauth > reauthTimeoutSeconds) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if privilege escalation requires re-authentication
+ * @return true if policy requires re-auth on privilege escalation
+ */
+ public boolean requiresReauthOnPrivilegeEscalation() {
+ return requireReauthOnPrivilegeEscalation;
+ }
+
+ /**
+ * Check if role change requires re-authentication
+ * @return true if policy requires re-auth on role change
+ */
+ public boolean requiresReauthOnRoleChange() {
+ return requireReauthOnRoleChange;
+ }
+
+ /**
+ * Check if security attribute change requires re-authentication
+ * @return true if policy requires re-auth on security attribute change
+ */
+ public boolean requiresReauthOnSecurityAttributeChange() {
+ return requireReauthOnSecurityAttributeChange;
+ }
+
+ /**
+ * Get re-authentication timeout in seconds
+ * @return timeout in seconds
+ */
+ public long getReauthTimeoutSeconds() {
+ return reauthTimeoutSeconds;
+ }
+
+ /**
+ * Get organization-defined policy value
+ * @param key policy key
+ * @return policy value
+ */
+ public Object getOrganizationPolicy(String key) {
+ return organizationDefinedPolicies.get(key);
+ }
+
+ /**
+ * Builder for ReauthenticationPolicy
+ */
+ public static class Builder {
+ private long reauthTimeoutSeconds = 3600;
+ private boolean requireReauthOnPrivilegeEscalation = true;
+ private boolean requireReauthOnRoleChange = true;
+ private boolean requireReauthOnSecurityAttributeChange = true;
+ private Map organizationDefinedPolicies = new HashMap<>();
+
+ public Builder reauthTimeoutSeconds(long seconds) {
+ this.reauthTimeoutSeconds = seconds;
+ return this;
+ }
+
+ public Builder requireReauthOnPrivilegeEscalation(boolean require) {
+ this.requireReauthOnPrivilegeEscalation = require;
+ return this;
+ }
+
+ public Builder requireReauthOnRoleChange(boolean require) {
+ this.requireReauthOnRoleChange = require;
+ return this;
+ }
+
+ public Builder requireReauthOnSecurityAttributeChange(boolean require) {
+ this.requireReauthOnSecurityAttributeChange = require;
+ return this;
+ }
+
+ public Builder addOrganizationPolicy(String key, Object value) {
+ this.organizationDefinedPolicies.put(key, value);
+ return this;
+ }
+
+ public ReauthenticationPolicy build() {
+ return new ReauthenticationPolicy(this);
+ }
+ }
+}
diff --git a/Common/src/main/java/gov/uspto/session/reauth/ReauthenticationTrigger.java b/Common/src/main/java/gov/uspto/session/reauth/ReauthenticationTrigger.java
new file mode 100644
index 0000000..f8ba549
--- /dev/null
+++ b/Common/src/main/java/gov/uspto/session/reauth/ReauthenticationTrigger.java
@@ -0,0 +1,97 @@
+package gov.uspto.session.reauth;
+
+import gov.uspto.session.model.ReauthReason;
+import gov.uspto.session.model.Session;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Triggers re-authentication events based on various conditions.
+ * Monitors session activity and enforces NIST 800-53 IA-11 requirements.
+ */
+public class ReauthenticationTrigger {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ReauthenticationTrigger.class);
+
+ private final ReauthenticationPolicy policy;
+
+ public ReauthenticationTrigger(ReauthenticationPolicy policy) {
+ this.policy = policy;
+ }
+
+ /**
+ * Check if session requires re-authentication and trigger if needed
+ * @param session the session to check
+ * @return true if re-authentication was triggered
+ */
+ public boolean checkAndTrigger(Session session) {
+ if (session.requiresReauthentication()) {
+ return true;
+ }
+
+ long timeSinceReauth = session.getTimeSinceLastReauthSeconds();
+ if (timeSinceReauth > policy.getReauthTimeoutSeconds()) {
+ triggerReauth(session, ReauthReason.SESSION_TIMEOUT);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Trigger re-authentication for privilege escalation
+ * @param session the session
+ */
+ public void triggerPrivilegeEscalation(Session session) {
+ if (policy.requiresReauthOnPrivilegeEscalation()) {
+ triggerReauth(session, ReauthReason.PRIVILEGE_ESCALATION);
+ }
+ }
+
+ /**
+ * Trigger re-authentication for role change
+ * @param session the session
+ */
+ public void triggerRoleChange(Session session) {
+ if (policy.requiresReauthOnRoleChange()) {
+ triggerReauth(session, ReauthReason.ROLE_CHANGE);
+ }
+ }
+
+ /**
+ * Trigger re-authentication for security attribute change
+ * @param session the session
+ */
+ public void triggerSecurityAttributeChange(Session session) {
+ if (policy.requiresReauthOnSecurityAttributeChange()) {
+ triggerReauth(session, ReauthReason.SECURITY_ATTRIBUTE_CHANGE);
+ }
+ }
+
+ /**
+ * Trigger re-authentication for suspicious activity
+ * @param session the session
+ */
+ public void triggerSuspiciousActivity(Session session) {
+ triggerReauth(session, ReauthReason.SUSPICIOUS_ACTIVITY);
+ }
+
+ /**
+ * Trigger re-authentication for organization-defined reason
+ * @param session the session
+ */
+ public void triggerOrganizationDefined(Session session) {
+ triggerReauth(session, ReauthReason.ORGANIZATION_DEFINED);
+ }
+
+ /**
+ * Internal method to trigger re-authentication
+ * @param session the session
+ * @param reason the reason for re-authentication
+ */
+ private void triggerReauth(Session session, ReauthReason reason) {
+ session.addReauthReason(reason);
+ LOGGER.info("Triggered re-authentication for session {} due to {}",
+ session.getSessionId(), reason);
+ }
+}
diff --git a/Common/src/main/java/gov/uspto/session/reauth/SessionTimeoutManager.java b/Common/src/main/java/gov/uspto/session/reauth/SessionTimeoutManager.java
new file mode 100644
index 0000000..cd407ae
--- /dev/null
+++ b/Common/src/main/java/gov/uspto/session/reauth/SessionTimeoutManager.java
@@ -0,0 +1,113 @@
+package gov.uspto.session.reauth;
+
+import gov.uspto.session.model.ReauthReason;
+import gov.uspto.session.model.Session;
+import gov.uspto.session.model.SessionState;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Manages timeout-based re-authentication and session expiration.
+ * Enforces NIST 800-53 IA-11 time-based re-authentication requirements.
+ */
+public class SessionTimeoutManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(SessionTimeoutManager.class);
+
+ private final long sessionTimeoutSeconds;
+ private final long inactivityTimeoutSeconds;
+ private final long reauthTimeoutSeconds;
+
+ public SessionTimeoutManager(long sessionTimeoutSeconds,
+ long inactivityTimeoutSeconds,
+ long reauthTimeoutSeconds) {
+ this.sessionTimeoutSeconds = sessionTimeoutSeconds;
+ this.inactivityTimeoutSeconds = inactivityTimeoutSeconds;
+ this.reauthTimeoutSeconds = reauthTimeoutSeconds;
+ }
+
+ /**
+ * Check if session has exceeded maximum lifetime
+ * @param session the session to check
+ * @return true if session has timed out
+ */
+ public boolean isSessionTimedOut(Session session) {
+ long sessionAge = Math.abs(session.getSessionDurationSeconds());
+ return sessionAge > sessionTimeoutSeconds;
+ }
+
+ /**
+ * Check if session has been inactive too long
+ * @param session the session to check
+ * @return true if session is inactive
+ */
+ public boolean isSessionInactive(Session session) {
+ long inactivityTime = session.getTimeSinceLastAccessSeconds();
+ return inactivityTime > inactivityTimeoutSeconds;
+ }
+
+ /**
+ * Check if session requires re-authentication due to timeout
+ * @param session the session to check
+ * @return true if re-authentication timeout exceeded
+ */
+ public boolean requiresReauthDueToTimeout(Session session) {
+ long timeSinceReauth = session.getTimeSinceLastReauthSeconds();
+ return timeSinceReauth > reauthTimeoutSeconds;
+ }
+
+ /**
+ * Process session timeouts and update state
+ * @param session the session to process
+ * @return true if session state was changed
+ */
+ public boolean processTimeouts(Session session) {
+ boolean stateChanged = false;
+
+ if (isSessionTimedOut(session)) {
+ LOGGER.info("Session {} has exceeded maximum lifetime", session.getSessionId());
+ session.setState(SessionState.EXPIRED);
+ stateChanged = true;
+ } else if (isSessionInactive(session)) {
+ LOGGER.info("Session {} has been inactive too long", session.getSessionId());
+ session.setState(SessionState.EXPIRED);
+ stateChanged = true;
+ } else if (requiresReauthDueToTimeout(session)) {
+ LOGGER.info("Session {} requires re-authentication due to timeout", session.getSessionId());
+ session.addReauthReason(ReauthReason.SESSION_TIMEOUT);
+ stateChanged = true;
+ }
+
+ return stateChanged;
+ }
+
+ /**
+ * Get remaining time before session timeout
+ * @param session the session
+ * @return seconds remaining before timeout
+ */
+ public long getRemainingSessionTime(Session session) {
+ long sessionAge = Math.abs(session.getSessionDurationSeconds());
+ return Math.max(0, sessionTimeoutSeconds - sessionAge);
+ }
+
+ /**
+ * Get remaining time before inactivity timeout
+ * @param session the session
+ * @return seconds remaining before inactivity timeout
+ */
+ public long getRemainingInactivityTime(Session session) {
+ long inactivityTime = session.getTimeSinceLastAccessSeconds();
+ return Math.max(0, inactivityTimeoutSeconds - inactivityTime);
+ }
+
+ /**
+ * Get remaining time before re-authentication required
+ * @param session the session
+ * @return seconds remaining before re-authentication required
+ */
+ public long getRemainingReauthTime(Session session) {
+ long timeSinceReauth = session.getTimeSinceLastReauthSeconds();
+ return Math.max(0, reauthTimeoutSeconds - timeSinceReauth);
+ }
+}
diff --git a/Common/src/main/java/gov/uspto/session/security/SessionEncryption.java b/Common/src/main/java/gov/uspto/session/security/SessionEncryption.java
new file mode 100644
index 0000000..1303435
--- /dev/null
+++ b/Common/src/main/java/gov/uspto/session/security/SessionEncryption.java
@@ -0,0 +1,104 @@
+package gov.uspto.session.security;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.SecureRandom;
+import java.util.Base64;
+
+/**
+ * Encrypts and decrypts sensitive session data using AES-GCM.
+ * Provides authenticated encryption for session attributes.
+ */
+public class SessionEncryption {
+
+ private static final String ALGORITHM = "AES";
+ private static final String TRANSFORMATION = "AES/GCM/NoPadding";
+ private static final int KEY_SIZE = 256;
+ private static final int GCM_TAG_LENGTH = 128;
+ private static final int GCM_IV_LENGTH = 12;
+
+ private final SecretKey secretKey;
+ private final SecureRandom secureRandom;
+
+ public SessionEncryption(SecretKey secretKey) {
+ this.secretKey = secretKey;
+ this.secureRandom = new SecureRandom();
+ }
+
+ /**
+ * Create SessionEncryption with a new random key
+ * @return new SessionEncryption instance
+ */
+ public static SessionEncryption withRandomKey() throws Exception {
+ KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM);
+ keyGenerator.init(KEY_SIZE);
+ SecretKey key = keyGenerator.generateKey();
+ return new SessionEncryption(key);
+ }
+
+ /**
+ * Create SessionEncryption from base64-encoded key
+ * @param base64Key base64-encoded key
+ * @return new SessionEncryption instance
+ */
+ public static SessionEncryption fromBase64Key(String base64Key) {
+ byte[] keyBytes = Base64.getDecoder().decode(base64Key);
+ SecretKey key = new SecretKeySpec(keyBytes, ALGORITHM);
+ return new SessionEncryption(key);
+ }
+
+ /**
+ * Encrypt data
+ * @param plaintext the data to encrypt
+ * @return base64-encoded encrypted data with IV prepended
+ */
+ public String encrypt(String plaintext) throws Exception {
+ byte[] iv = new byte[GCM_IV_LENGTH];
+ secureRandom.nextBytes(iv);
+
+ Cipher cipher = Cipher.getInstance(TRANSFORMATION);
+ GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
+
+ byte[] ciphertext = cipher.doFinal(plaintext.getBytes("UTF-8"));
+
+ byte[] encryptedData = new byte[iv.length + ciphertext.length];
+ System.arraycopy(iv, 0, encryptedData, 0, iv.length);
+ System.arraycopy(ciphertext, 0, encryptedData, iv.length, ciphertext.length);
+
+ return Base64.getEncoder().encodeToString(encryptedData);
+ }
+
+ /**
+ * Decrypt data
+ * @param encryptedData base64-encoded encrypted data with IV prepended
+ * @return decrypted plaintext
+ */
+ public String decrypt(String encryptedData) throws Exception {
+ byte[] decoded = Base64.getDecoder().decode(encryptedData);
+
+ byte[] iv = new byte[GCM_IV_LENGTH];
+ System.arraycopy(decoded, 0, iv, 0, iv.length);
+
+ byte[] ciphertext = new byte[decoded.length - GCM_IV_LENGTH];
+ System.arraycopy(decoded, GCM_IV_LENGTH, ciphertext, 0, ciphertext.length);
+
+ Cipher cipher = Cipher.getInstance(TRANSFORMATION);
+ GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);
+
+ byte[] plaintext = cipher.doFinal(ciphertext);
+ return new String(plaintext, "UTF-8");
+ }
+
+ /**
+ * Get the encryption key as base64
+ * @return base64-encoded key
+ */
+ public String getKeyAsBase64() {
+ return Base64.getEncoder().encodeToString(secretKey.getEncoded());
+ }
+}
diff --git a/Common/src/main/java/gov/uspto/session/security/SessionHijackingPrevention.java b/Common/src/main/java/gov/uspto/session/security/SessionHijackingPrevention.java
new file mode 100644
index 0000000..37f3bd7
--- /dev/null
+++ b/Common/src/main/java/gov/uspto/session/security/SessionHijackingPrevention.java
@@ -0,0 +1,135 @@
+package gov.uspto.session.security;
+
+import gov.uspto.session.model.Session;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Implements anti-hijacking measures for session security.
+ * Provides session binding, fixation protection, and anomaly detection.
+ */
+public class SessionHijackingPrevention {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(SessionHijackingPrevention.class);
+
+ private final boolean enforceIpBinding;
+ private final boolean enforceUserAgentBinding;
+ private final int maxConcurrentSessions;
+
+ public SessionHijackingPrevention(boolean enforceIpBinding,
+ boolean enforceUserAgentBinding,
+ int maxConcurrentSessions) {
+ this.enforceIpBinding = enforceIpBinding;
+ this.enforceUserAgentBinding = enforceUserAgentBinding;
+ this.maxConcurrentSessions = maxConcurrentSessions;
+ }
+
+ /**
+ * Validate session binding to prevent hijacking
+ * @param session the session
+ * @param currentIpAddress current request IP
+ * @param currentUserAgent current request user agent
+ * @return true if session binding is valid
+ */
+ public boolean validateSessionBinding(Session session, String currentIpAddress, String currentUserAgent) {
+ if (enforceIpBinding && session.getIpAddress() != null) {
+ if (!session.getIpAddress().equals(currentIpAddress)) {
+ LOGGER.warn("Session {} IP mismatch: expected {}, got {}",
+ session.getSessionId(), session.getIpAddress(), currentIpAddress);
+ return false;
+ }
+ }
+
+ if (enforceUserAgentBinding && session.getUserAgent() != null) {
+ if (!session.getUserAgent().equals(currentUserAgent)) {
+ LOGGER.warn("Session {} User-Agent mismatch: expected {}, got {}",
+ session.getSessionId(), session.getUserAgent(), currentUserAgent);
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Detect session fixation attack
+ * @param session the session
+ * @return true if fixation attack detected
+ */
+ public boolean detectSessionFixation(Session session) {
+ if (session.getAccessCount() == 0 && session.getTimeSinceLastAccessSeconds() > 300) {
+ LOGGER.warn("Potential session fixation detected for session {}", session.getSessionId());
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Detect suspicious session activity
+ * @param session the session
+ * @param currentIpAddress current request IP
+ * @return true if suspicious activity detected
+ */
+ public boolean detectSuspiciousActivity(Session session, String currentIpAddress) {
+ if (session.getIpAddress() != null && !session.getIpAddress().equals(currentIpAddress)) {
+ String previousIp = session.getIpAddress();
+ if (!isSameSubnet(previousIp, currentIpAddress)) {
+ LOGGER.warn("Suspicious activity: Session {} accessed from different subnet: {} -> {}",
+ session.getSessionId(), previousIp, currentIpAddress);
+ return true;
+ }
+ }
+
+ if (session.getAccessCount() > 1000) {
+ LOGGER.warn("Suspicious activity: Session {} has excessive access count: {}",
+ session.getSessionId(), session.getAccessCount());
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if concurrent session limit is exceeded
+ * @param activeSessionCount number of active sessions for user
+ * @return true if limit exceeded
+ */
+ public boolean isConcurrentSessionLimitExceeded(int activeSessionCount) {
+ return activeSessionCount >= maxConcurrentSessions;
+ }
+
+ /**
+ * Regenerate session ID to prevent fixation
+ * @param oldSessionId the old session ID
+ * @param generator session ID generator
+ * @return new session ID
+ */
+ public String regenerateSessionId(String oldSessionId, SessionIdGenerator generator) {
+ String newSessionId = generator.generateSessionId();
+ LOGGER.info("Regenerated session ID: {} -> {}", oldSessionId, newSessionId);
+ return newSessionId;
+ }
+
+ /**
+ * Check if two IPs are in the same subnet (simple /24 check)
+ * @param ip1 first IP address
+ * @param ip2 second IP address
+ * @return true if same subnet
+ */
+ private boolean isSameSubnet(String ip1, String ip2) {
+ if (ip1 == null || ip2 == null) {
+ return false;
+ }
+
+ String[] parts1 = ip1.split("\\.");
+ String[] parts2 = ip2.split("\\.");
+
+ if (parts1.length != 4 || parts2.length != 4) {
+ return false;
+ }
+
+ return parts1[0].equals(parts2[0]) &&
+ parts1[1].equals(parts2[1]) &&
+ parts1[2].equals(parts2[2]);
+ }
+}
diff --git a/Common/src/main/java/gov/uspto/session/security/SessionIdGenerator.java b/Common/src/main/java/gov/uspto/session/security/SessionIdGenerator.java
new file mode 100644
index 0000000..5060b82
--- /dev/null
+++ b/Common/src/main/java/gov/uspto/session/security/SessionIdGenerator.java
@@ -0,0 +1,45 @@
+package gov.uspto.session.security;
+
+import java.security.SecureRandom;
+import java.util.Base64;
+
+/**
+ * Generates cryptographically secure session IDs.
+ * Uses SecureRandom for high-entropy random number generation.
+ */
+public class SessionIdGenerator {
+
+ private static final int DEFAULT_SESSION_ID_LENGTH = 32;
+ private final SecureRandom secureRandom;
+ private final int sessionIdLength;
+
+ public SessionIdGenerator() {
+ this(DEFAULT_SESSION_ID_LENGTH);
+ }
+
+ public SessionIdGenerator(int sessionIdLength) {
+ this.secureRandom = new SecureRandom();
+ this.sessionIdLength = sessionIdLength;
+ }
+
+ /**
+ * Generate a cryptographically secure session ID
+ * @return base64-encoded session ID
+ */
+ public String generateSessionId() {
+ byte[] randomBytes = new byte[sessionIdLength];
+ secureRandom.nextBytes(randomBytes);
+ return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
+ }
+
+ /**
+ * Generate a session ID with custom length
+ * @param length the length in bytes
+ * @return base64-encoded session ID
+ */
+ public String generateSessionId(int length) {
+ byte[] randomBytes = new byte[length];
+ secureRandom.nextBytes(randomBytes);
+ return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
+ }
+}
diff --git a/Common/src/main/java/gov/uspto/session/security/SessionToken.java b/Common/src/main/java/gov/uspto/session/security/SessionToken.java
new file mode 100644
index 0000000..6c420b7
--- /dev/null
+++ b/Common/src/main/java/gov/uspto/session/security/SessionToken.java
@@ -0,0 +1,129 @@
+package gov.uspto.session.security;
+
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Secure session token representation.
+ * Designed for both server-side sessions and potential JWT integration.
+ */
+public class SessionToken {
+
+ private final String tokenId;
+ private final String sessionId;
+ private final String userId;
+ private final Instant issuedAt;
+ private final Instant expiresAt;
+ private final Map claims;
+
+ private SessionToken(Builder builder) {
+ this.tokenId = builder.tokenId;
+ this.sessionId = builder.sessionId;
+ this.userId = builder.userId;
+ this.issuedAt = builder.issuedAt;
+ this.expiresAt = builder.expiresAt;
+ this.claims = builder.claims;
+ }
+
+ public String getTokenId() {
+ return tokenId;
+ }
+
+ public String getSessionId() {
+ return sessionId;
+ }
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public Instant getIssuedAt() {
+ return issuedAt;
+ }
+
+ public Instant getExpiresAt() {
+ return expiresAt;
+ }
+
+ public boolean isExpired() {
+ return Instant.now().isAfter(expiresAt);
+ }
+
+ public Map getClaims() {
+ return new HashMap<>(claims);
+ }
+
+ public Object getClaim(String key) {
+ return claims.get(key);
+ }
+
+ @Override
+ public String toString() {
+ return "SessionToken{" +
+ "tokenId='" + tokenId + '\'' +
+ ", sessionId='" + sessionId + '\'' +
+ ", userId='" + userId + '\'' +
+ ", issuedAt=" + issuedAt +
+ ", expiresAt=" + expiresAt +
+ ", expired=" + isExpired() +
+ '}';
+ }
+
+ /**
+ * Builder for SessionToken
+ */
+ public static class Builder {
+ private String tokenId;
+ private String sessionId;
+ private String userId;
+ private Instant issuedAt = Instant.now();
+ private Instant expiresAt;
+ private Map claims = new HashMap<>();
+
+ public Builder tokenId(String tokenId) {
+ this.tokenId = tokenId;
+ return this;
+ }
+
+ public Builder sessionId(String sessionId) {
+ this.sessionId = sessionId;
+ return this;
+ }
+
+ public Builder userId(String userId) {
+ this.userId = userId;
+ return this;
+ }
+
+ public Builder issuedAt(Instant issuedAt) {
+ this.issuedAt = issuedAt;
+ return this;
+ }
+
+ public Builder expiresAt(Instant expiresAt) {
+ this.expiresAt = expiresAt;
+ return this;
+ }
+
+ public Builder expiresInSeconds(long seconds) {
+ this.expiresAt = Instant.now().plusSeconds(seconds);
+ return this;
+ }
+
+ public Builder addClaim(String key, Object value) {
+ this.claims.put(key, value);
+ return this;
+ }
+
+ public SessionToken build() {
+ if (tokenId == null || sessionId == null || userId == null) {
+ throw new IllegalStateException("tokenId, sessionId, and userId are required");
+ }
+ if (expiresAt == null) {
+ expiresAt = Instant.now().plusSeconds(3600);
+ }
+ return new SessionToken(this);
+ }
+ }
+}
diff --git a/Common/src/test/java/gov/uspto/session/InMemorySessionStore.java b/Common/src/test/java/gov/uspto/session/InMemorySessionStore.java
new file mode 100644
index 0000000..e09cf7b
--- /dev/null
+++ b/Common/src/test/java/gov/uspto/session/InMemorySessionStore.java
@@ -0,0 +1,72 @@
+package gov.uspto.session;
+
+import gov.uspto.session.management.SessionStore;
+import gov.uspto.session.model.Session;
+import gov.uspto.session.model.SessionState;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * In-memory implementation of SessionStore for testing.
+ */
+public class InMemorySessionStore implements SessionStore {
+
+ private final Map sessions = new ConcurrentHashMap<>();
+
+ @Override
+ public void save(Session session) {
+ sessions.put(session.getSessionId(), session);
+ }
+
+ @Override
+ public Optional findById(String sessionId) {
+ return Optional.ofNullable(sessions.get(sessionId));
+ }
+
+ @Override
+ public Session[] findByUserId(String userId) {
+ List userSessions = new ArrayList<>();
+ for (Session session : sessions.values()) {
+ if (session.getUserId().equals(userId)) {
+ userSessions.add(session);
+ }
+ }
+ return userSessions.toArray(new Session[0]);
+ }
+
+ @Override
+ public void delete(String sessionId) {
+ sessions.remove(sessionId);
+ }
+
+ @Override
+ public void deleteByUserId(String userId) {
+ sessions.values().removeIf(session -> session.getUserId().equals(userId));
+ }
+
+ @Override
+ public boolean exists(String sessionId) {
+ return sessions.containsKey(sessionId);
+ }
+
+ @Override
+ public int countActiveSessionsForUser(String userId) {
+ int count = 0;
+ for (Session session : sessions.values()) {
+ if (session.getUserId().equals(userId) &&
+ (session.getState() == SessionState.ACTIVE ||
+ session.getState() == SessionState.REQUIRES_REAUTH)) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ public void clear() {
+ sessions.clear();
+ }
+}
diff --git a/Common/src/test/java/gov/uspto/session/ReauthenticationTest.java b/Common/src/test/java/gov/uspto/session/ReauthenticationTest.java
new file mode 100644
index 0000000..318ebfe
--- /dev/null
+++ b/Common/src/test/java/gov/uspto/session/ReauthenticationTest.java
@@ -0,0 +1,177 @@
+package gov.uspto.session;
+
+import gov.uspto.session.model.ReauthReason;
+import gov.uspto.session.model.Session;
+import gov.uspto.session.reauth.PrivilegeChangeDetector;
+import gov.uspto.session.reauth.ReauthenticationPolicy;
+import gov.uspto.session.reauth.ReauthenticationTrigger;
+import gov.uspto.session.reauth.SessionTimeoutManager;
+import org.junit.Test;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests for re-authentication functionality
+ */
+public class ReauthenticationTest {
+
+ @Test
+ public void testReauthenticationPolicyBuilder() {
+ ReauthenticationPolicy policy = new ReauthenticationPolicy.Builder()
+ .reauthTimeoutSeconds(1800)
+ .requireReauthOnPrivilegeEscalation(true)
+ .requireReauthOnRoleChange(true)
+ .requireReauthOnSecurityAttributeChange(false)
+ .addOrganizationPolicy("custom_rule", "value")
+ .build();
+
+ assertEquals(1800, policy.getReauthTimeoutSeconds());
+ assertTrue(policy.requiresReauthOnPrivilegeEscalation());
+ assertTrue(policy.requiresReauthOnRoleChange());
+ assertFalse(policy.requiresReauthOnSecurityAttributeChange());
+ assertEquals("value", policy.getOrganizationPolicy("custom_rule"));
+ }
+
+ @Test
+ public void testReauthenticationTrigger() {
+ ReauthenticationPolicy policy = new ReauthenticationPolicy.Builder()
+ .requireReauthOnPrivilegeEscalation(true)
+ .build();
+
+ ReauthenticationTrigger trigger = new ReauthenticationTrigger(policy);
+ Session session = new Session("session123", "user456");
+
+ assertFalse(session.requiresReauthentication());
+
+ trigger.triggerPrivilegeEscalation(session);
+
+ assertTrue(session.requiresReauthentication());
+ assertTrue(session.getPendingReauthReasons().contains(ReauthReason.PRIVILEGE_ESCALATION));
+ }
+
+ @Test
+ public void testReauthenticationTriggerRoleChange() {
+ ReauthenticationPolicy policy = new ReauthenticationPolicy.Builder()
+ .requireReauthOnRoleChange(true)
+ .build();
+
+ ReauthenticationTrigger trigger = new ReauthenticationTrigger(policy);
+ Session session = new Session("session123", "user456");
+
+ trigger.triggerRoleChange(session);
+
+ assertTrue(session.requiresReauthentication());
+ assertTrue(session.getPendingReauthReasons().contains(ReauthReason.ROLE_CHANGE));
+ }
+
+ @Test
+ public void testReauthenticationTriggerSecurityAttributeChange() {
+ ReauthenticationPolicy policy = new ReauthenticationPolicy.Builder()
+ .requireReauthOnSecurityAttributeChange(true)
+ .build();
+
+ ReauthenticationTrigger trigger = new ReauthenticationTrigger(policy);
+ Session session = new Session("session123", "user456");
+
+ trigger.triggerSecurityAttributeChange(session);
+
+ assertTrue(session.requiresReauthentication());
+ assertTrue(session.getPendingReauthReasons().contains(ReauthReason.SECURITY_ATTRIBUTE_CHANGE));
+ }
+
+ @Test
+ public void testPrivilegeChangeDetection() {
+ ReauthenticationPolicy policy = new ReauthenticationPolicy.Builder()
+ .requireReauthOnPrivilegeEscalation(true)
+ .build();
+
+ ReauthenticationTrigger trigger = new ReauthenticationTrigger(policy);
+ PrivilegeChangeDetector detector = new PrivilegeChangeDetector(trigger);
+
+ Session session = new Session("session123", "user456");
+ Set privileges = new HashSet<>();
+ privileges.add("read");
+ session.setSecurityAttribute("privileges", privileges);
+
+ boolean escalated = detector.detectPrivilegeEscalation(session, "write");
+
+ assertTrue(escalated);
+ assertTrue(session.requiresReauthentication());
+ }
+
+ @Test
+ public void testRoleChangeDetection() {
+ ReauthenticationPolicy policy = new ReauthenticationPolicy.Builder()
+ .requireReauthOnRoleChange(true)
+ .build();
+
+ ReauthenticationTrigger trigger = new ReauthenticationTrigger(policy);
+ PrivilegeChangeDetector detector = new PrivilegeChangeDetector(trigger);
+
+ Session session = new Session("session123", "user456");
+ session.setSecurityAttribute("role", "user");
+
+ boolean changed = detector.detectRoleChange(session, "admin");
+
+ assertTrue(changed);
+ assertTrue(session.requiresReauthentication());
+ }
+
+ @Test
+ public void testSecurityAttributeChangeDetection() {
+ ReauthenticationPolicy policy = new ReauthenticationPolicy.Builder()
+ .requireReauthOnSecurityAttributeChange(true)
+ .build();
+
+ ReauthenticationTrigger trigger = new ReauthenticationTrigger(policy);
+ PrivilegeChangeDetector detector = new PrivilegeChangeDetector(trigger);
+
+ Session session = new Session("session123", "user456");
+ session.setSecurityAttribute("clearance", "secret");
+
+ boolean changed = detector.detectSecurityAttributeChange(session, "clearance", "top-secret");
+
+ assertTrue(changed);
+ assertTrue(session.requiresReauthentication());
+ }
+
+ @Test
+ public void testSessionTimeoutManager() {
+ SessionTimeoutManager timeoutManager = new SessionTimeoutManager(3600, 1800, 900);
+
+ Session session = new Session("session123", "user456");
+
+ assertFalse(timeoutManager.isSessionTimedOut(session));
+ assertFalse(timeoutManager.isSessionInactive(session));
+ assertFalse(timeoutManager.requiresReauthDueToTimeout(session));
+ }
+
+ @Test
+ public void testSessionTimeoutProcessing() {
+ SessionTimeoutManager timeoutManager = new SessionTimeoutManager(3600, 1800, 900);
+
+ Session session = new Session("session123", "user456");
+
+ boolean stateChanged = timeoutManager.processTimeouts(session);
+
+ assertFalse(stateChanged);
+ }
+
+ @Test
+ public void testRemainingTimeCalculations() {
+ SessionTimeoutManager timeoutManager = new SessionTimeoutManager(3600, 1800, 900);
+
+ Session session = new Session("session123", "user456");
+
+ long remainingSessionTime = timeoutManager.getRemainingSessionTime(session);
+ long remainingInactivityTime = timeoutManager.getRemainingInactivityTime(session);
+ long remainingReauthTime = timeoutManager.getRemainingReauthTime(session);
+
+ assertTrue(remainingSessionTime > 0);
+ assertTrue(remainingInactivityTime > 0);
+ assertTrue(remainingReauthTime > 0);
+ }
+}
diff --git a/Common/src/test/java/gov/uspto/session/SessionLifecycleTest.java b/Common/src/test/java/gov/uspto/session/SessionLifecycleTest.java
new file mode 100644
index 0000000..a8f27d1
--- /dev/null
+++ b/Common/src/test/java/gov/uspto/session/SessionLifecycleTest.java
@@ -0,0 +1,253 @@
+package gov.uspto.session;
+
+import gov.uspto.session.lifecycle.ConcurrentSessionManager;
+import gov.uspto.session.lifecycle.SessionCreationService;
+import gov.uspto.session.lifecycle.SessionRenewalService;
+import gov.uspto.session.lifecycle.SessionTerminationService;
+import gov.uspto.session.management.SessionFactory;
+import gov.uspto.session.model.Session;
+import gov.uspto.session.model.SessionState;
+import gov.uspto.session.security.SessionHijackingPrevention;
+import gov.uspto.session.security.SessionIdGenerator;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests for session lifecycle management
+ */
+public class SessionLifecycleTest {
+
+ private InMemorySessionStore sessionStore;
+ private SessionIdGenerator idGenerator;
+ private SessionFactory sessionFactory;
+
+ @Before
+ public void setUp() {
+ sessionStore = new InMemorySessionStore();
+ idGenerator = new SessionIdGenerator();
+ sessionFactory = new SessionFactory(idGenerator);
+ }
+
+ @Test
+ public void testSessionCreation() throws Exception {
+ SessionHijackingPrevention hijackingPrevention = new SessionHijackingPrevention(true, true, 5);
+ SessionCreationService creationService = new SessionCreationService(
+ sessionFactory, sessionStore, hijackingPrevention);
+
+ Session session = creationService.createSession("user123");
+
+ assertNotNull(session);
+ assertEquals("user123", session.getUserId());
+ assertTrue(sessionStore.exists(session.getSessionId()));
+ }
+
+ @Test
+ public void testSessionCreationWithSecurityContext() throws Exception {
+ SessionHijackingPrevention hijackingPrevention = new SessionHijackingPrevention(true, true, 5);
+ SessionCreationService creationService = new SessionCreationService(
+ sessionFactory, sessionStore, hijackingPrevention);
+
+ Session session = creationService.createSession("user123", "192.168.1.100", "Mozilla/5.0");
+
+ assertNotNull(session);
+ assertEquals("user123", session.getUserId());
+ assertEquals("192.168.1.100", session.getIpAddress());
+ assertEquals("Mozilla/5.0", session.getUserAgent());
+ }
+
+ @Test
+ public void testConcurrentSessionLimitEnforcement() throws Exception {
+ SessionHijackingPrevention hijackingPrevention = new SessionHijackingPrevention(true, true, 2);
+ SessionCreationService creationService = new SessionCreationService(
+ sessionFactory, sessionStore, hijackingPrevention);
+
+ creationService.createSession("user123");
+ creationService.createSession("user123");
+
+ try {
+ creationService.createSession("user123");
+ fail("Expected SessionCreationException due to concurrent session limit");
+ } catch (SessionCreationService.SessionCreationException e) {
+ assertTrue(e.getMessage().contains("Concurrent session limit exceeded"));
+ }
+ }
+
+ @Test
+ public void testSessionTermination() {
+ Session session = sessionFactory.createSession("user123");
+ sessionStore.save(session);
+
+ SessionTerminationService terminationService = new SessionTerminationService(sessionStore);
+
+ boolean terminated = terminationService.terminateSession(session.getSessionId());
+
+ assertTrue(terminated);
+ assertEquals(SessionState.TERMINATED, sessionStore.findById(session.getSessionId()).get().getState());
+ }
+
+ @Test
+ public void testTerminateAllUserSessions() {
+ Session session1 = sessionFactory.createSession("user123");
+ Session session2 = sessionFactory.createSession("user123");
+ Session session3 = sessionFactory.createSession("user123");
+ sessionStore.save(session1);
+ sessionStore.save(session2);
+ sessionStore.save(session3);
+
+ SessionTerminationService terminationService = new SessionTerminationService(sessionStore);
+
+ int terminatedCount = terminationService.terminateAllUserSessions("user123");
+
+ assertEquals(3, terminatedCount);
+
+ Session[] sessions = sessionStore.findByUserId("user123");
+ for (Session session : sessions) {
+ assertEquals(SessionState.TERMINATED, session.getState());
+ }
+ }
+
+ @Test
+ public void testSessionDeletion() {
+ Session session = sessionFactory.createSession("user123");
+ sessionStore.save(session);
+
+ SessionTerminationService terminationService = new SessionTerminationService(sessionStore);
+
+ assertTrue(sessionStore.exists(session.getSessionId()));
+
+ terminationService.deleteSession(session.getSessionId());
+
+ assertFalse(sessionStore.exists(session.getSessionId()));
+ }
+
+ @Test
+ public void testSessionRenewal() {
+ Session session = sessionFactory.createSession("user123");
+ sessionStore.save(session);
+
+ SessionRenewalService renewalService = new SessionRenewalService(sessionStore, idGenerator);
+
+ int initialAccessCount = session.getAccessCount();
+
+ boolean renewed = renewalService.renewSession(session.getSessionId());
+
+ assertTrue(renewed);
+ assertTrue(sessionStore.findById(session.getSessionId()).get().getAccessCount() > initialAccessCount);
+ }
+
+ @Test
+ public void testSessionIdRegeneration() {
+ Session session = sessionFactory.createSession("user123");
+ session.setAttribute("key1", "value1");
+ session.setSecurityAttribute("role", "admin");
+ sessionStore.save(session);
+
+ SessionRenewalService renewalService = new SessionRenewalService(sessionStore, idGenerator);
+
+ String oldSessionId = session.getSessionId();
+ String newSessionId = renewalService.regenerateSessionId(oldSessionId);
+
+ assertNotNull(newSessionId);
+ assertNotEquals(oldSessionId, newSessionId);
+ assertFalse(sessionStore.exists(oldSessionId));
+ assertTrue(sessionStore.exists(newSessionId));
+
+ Session newSession = sessionStore.findById(newSessionId).get();
+ assertEquals("value1", newSession.getAttribute("key1"));
+ assertEquals("admin", newSession.getSecurityAttribute("role"));
+ }
+
+ @Test
+ public void testRefreshAfterReauth() {
+ Session session = sessionFactory.createSession("user123");
+ session.addReauthReason(gov.uspto.session.model.ReauthReason.PRIVILEGE_ESCALATION);
+ sessionStore.save(session);
+
+ SessionRenewalService renewalService = new SessionRenewalService(sessionStore, idGenerator);
+
+ assertTrue(session.requiresReauthentication());
+
+ boolean refreshed = renewalService.refreshAfterReauth(session.getSessionId());
+
+ assertTrue(refreshed);
+ assertFalse(sessionStore.findById(session.getSessionId()).get().requiresReauthentication());
+ }
+
+ @Test
+ public void testConcurrentSessionManagement() {
+ ConcurrentSessionManager concurrentManager = new ConcurrentSessionManager(sessionStore, 3);
+
+ Session session1 = sessionFactory.createSession("user123");
+ Session session2 = sessionFactory.createSession("user123");
+ Session session3 = sessionFactory.createSession("user123");
+ sessionStore.save(session1);
+ sessionStore.save(session2);
+ sessionStore.save(session3);
+
+ Session[] activeSessions = concurrentManager.getActiveSessions("user123");
+ assertEquals(3, activeSessions.length);
+
+ int activeCount = concurrentManager.getActiveSessionCount("user123");
+ assertEquals(3, activeCount);
+
+ assertTrue(concurrentManager.hasReachedLimit("user123"));
+ }
+
+ @Test
+ public void testTerminateOldestSession() {
+ ConcurrentSessionManager concurrentManager = new ConcurrentSessionManager(sessionStore, 2);
+
+ Session session1 = sessionFactory.createSession("user123");
+ sessionStore.save(session1);
+
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+
+ Session session2 = sessionFactory.createSession("user123");
+ sessionStore.save(session2);
+
+ boolean terminated = concurrentManager.terminateOldestIfLimitExceeded("user123");
+
+ assertTrue(terminated);
+ assertEquals(1, concurrentManager.getActiveSessionCount("user123"));
+ }
+
+ @Test
+ public void testTerminateAllExcept() {
+ ConcurrentSessionManager concurrentManager = new ConcurrentSessionManager(sessionStore, 5);
+
+ Session session1 = sessionFactory.createSession("user123");
+ Session session2 = sessionFactory.createSession("user123");
+ Session session3 = sessionFactory.createSession("user123");
+ sessionStore.save(session1);
+ sessionStore.save(session2);
+ sessionStore.save(session3);
+
+ int terminatedCount = concurrentManager.terminateAllExcept("user123", session2.getSessionId());
+
+ assertEquals(2, terminatedCount);
+ assertEquals(1, concurrentManager.getActiveSessionCount("user123"));
+ assertEquals(SessionState.ACTIVE, sessionStore.findById(session2.getSessionId()).get().getState());
+ }
+
+ @Test
+ public void testGetSessionInfo() {
+ ConcurrentSessionManager concurrentManager = new ConcurrentSessionManager(sessionStore, 5);
+
+ Session session1 = sessionFactory.createSession("user123", "192.168.1.100", "Mozilla/5.0");
+ Session session2 = sessionFactory.createSession("user123", "192.168.1.101", "Chrome/90.0");
+ sessionStore.save(session1);
+ sessionStore.save(session2);
+
+ String[] info = concurrentManager.getSessionInfo("user123");
+
+ assertEquals(2, info.length);
+ assertTrue(info[0].contains("Session"));
+ assertTrue(info[1].contains("Session"));
+ }
+}
diff --git a/Common/src/test/java/gov/uspto/session/SessionManagerTest.java b/Common/src/test/java/gov/uspto/session/SessionManagerTest.java
new file mode 100644
index 0000000..462552f
--- /dev/null
+++ b/Common/src/test/java/gov/uspto/session/SessionManagerTest.java
@@ -0,0 +1,171 @@
+package gov.uspto.session;
+
+import gov.uspto.session.management.SessionFactory;
+import gov.uspto.session.management.SessionManager;
+import gov.uspto.session.management.SessionValidator;
+import gov.uspto.session.model.ReauthReason;
+import gov.uspto.session.model.Session;
+import gov.uspto.session.model.SessionState;
+import gov.uspto.session.reauth.ReauthenticationPolicy;
+import gov.uspto.session.security.SessionIdGenerator;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Optional;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests for SessionManager
+ */
+public class SessionManagerTest {
+
+ private InMemorySessionStore sessionStore;
+ private SessionManager sessionManager;
+
+ @Before
+ public void setUp() {
+ sessionStore = new InMemorySessionStore();
+ SessionIdGenerator idGenerator = new SessionIdGenerator();
+ SessionFactory sessionFactory = new SessionFactory(idGenerator);
+ SessionValidator sessionValidator = new SessionValidator(3600, 1800);
+ ReauthenticationPolicy reauthPolicy = new ReauthenticationPolicy.Builder()
+ .reauthTimeoutSeconds(3600)
+ .build();
+
+ sessionManager = new SessionManager(sessionStore, sessionFactory, sessionValidator, reauthPolicy);
+ }
+
+ @Test
+ public void testCreateSession() {
+ Session session = sessionManager.createSession("user123");
+
+ assertNotNull(session);
+ assertEquals("user123", session.getUserId());
+ assertEquals(SessionState.ACTIVE, session.getState());
+ assertTrue(sessionStore.exists(session.getSessionId()));
+ }
+
+ @Test
+ public void testCreateSessionWithSecurityContext() {
+ Session session = sessionManager.createSession("user123", "192.168.1.100", "Mozilla/5.0");
+
+ assertNotNull(session);
+ assertEquals("user123", session.getUserId());
+ assertEquals("192.168.1.100", session.getIpAddress());
+ assertEquals("Mozilla/5.0", session.getUserAgent());
+ assertTrue(sessionStore.exists(session.getSessionId()));
+ }
+
+ @Test
+ public void testGetSession() {
+ Session created = sessionManager.createSession("user123");
+
+ Optional retrieved = sessionManager.getSession(created.getSessionId());
+
+ assertTrue(retrieved.isPresent());
+ assertEquals(created.getSessionId(), retrieved.get().getSessionId());
+ assertEquals(created.getUserId(), retrieved.get().getUserId());
+ }
+
+ @Test
+ public void testGetNonExistentSession() {
+ Optional retrieved = sessionManager.getSession("nonexistent");
+
+ assertFalse(retrieved.isPresent());
+ }
+
+ @Test
+ public void testValidateSession() {
+ Session session = sessionManager.createSession("user123");
+
+ assertTrue(sessionManager.validateSession(session.getSessionId()));
+ assertFalse(sessionManager.validateSession("nonexistent"));
+ }
+
+ @Test
+ public void testTouchSession() {
+ Session session = sessionManager.createSession("user123");
+ int initialAccessCount = session.getAccessCount();
+
+ sessionManager.touchSession(session.getSessionId());
+
+ Optional retrieved = sessionStore.findById(session.getSessionId());
+ assertTrue(retrieved.isPresent());
+ assertTrue(retrieved.get().getAccessCount() > initialAccessCount);
+ }
+
+ @Test
+ public void testTriggerReauthentication() {
+ Session session = sessionManager.createSession("user123");
+
+ assertFalse(session.requiresReauthentication());
+
+ sessionManager.triggerReauthentication(session.getSessionId(), ReauthReason.PRIVILEGE_ESCALATION);
+
+ Optional retrieved = sessionStore.findById(session.getSessionId());
+ assertTrue(retrieved.isPresent());
+ assertTrue(retrieved.get().requiresReauthentication());
+ assertTrue(retrieved.get().getPendingReauthReasons().contains(ReauthReason.PRIVILEGE_ESCALATION));
+ }
+
+ @Test
+ public void testMarkReauthenticated() {
+ Session session = sessionManager.createSession("user123");
+ sessionManager.triggerReauthentication(session.getSessionId(), ReauthReason.ROLE_CHANGE);
+
+ Optional beforeReauth = sessionStore.findById(session.getSessionId());
+ assertTrue(beforeReauth.isPresent());
+ assertTrue(beforeReauth.get().requiresReauthentication());
+
+ sessionManager.markReauthenticated(session.getSessionId());
+
+ Optional afterReauth = sessionStore.findById(session.getSessionId());
+ assertTrue(afterReauth.isPresent());
+ assertFalse(afterReauth.get().requiresReauthentication());
+ assertEquals(0, afterReauth.get().getPendingReauthReasons().size());
+ }
+
+ @Test
+ public void testTerminateSession() {
+ Session session = sessionManager.createSession("user123");
+
+ assertEquals(SessionState.ACTIVE, session.getState());
+
+ sessionManager.terminateSession(session.getSessionId());
+
+ Optional retrieved = sessionStore.findById(session.getSessionId());
+ assertTrue(retrieved.isPresent());
+ assertEquals(SessionState.TERMINATED, retrieved.get().getState());
+ }
+
+ @Test
+ public void testTerminateAllUserSessions() {
+ sessionManager.createSession("user123");
+ sessionManager.createSession("user123");
+ sessionManager.createSession("user123");
+
+ assertEquals(3, sessionStore.findByUserId("user123").length);
+
+ sessionManager.terminateAllUserSessions("user123");
+
+ Session[] sessions = sessionStore.findByUserId("user123");
+ assertEquals(3, sessions.length);
+ for (Session session : sessions) {
+ assertEquals(SessionState.TERMINATED, session.getState());
+ }
+ }
+
+ @Test
+ public void testGetActiveSessionCount() {
+ sessionManager.createSession("user123");
+ sessionManager.createSession("user123");
+ Session session3 = sessionManager.createSession("user123");
+
+ assertEquals(3, sessionManager.getActiveSessionCount("user123"));
+
+ sessionManager.terminateSession(session3.getSessionId());
+
+ assertEquals(2, sessionManager.getActiveSessionCount("user123"));
+ }
+}
diff --git a/Common/src/test/java/gov/uspto/session/SessionSecurityTest.java b/Common/src/test/java/gov/uspto/session/SessionSecurityTest.java
new file mode 100644
index 0000000..2cc4f2d
--- /dev/null
+++ b/Common/src/test/java/gov/uspto/session/SessionSecurityTest.java
@@ -0,0 +1,170 @@
+package gov.uspto.session;
+
+import gov.uspto.session.model.Session;
+import gov.uspto.session.security.SessionEncryption;
+import gov.uspto.session.security.SessionHijackingPrevention;
+import gov.uspto.session.security.SessionIdGenerator;
+import gov.uspto.session.security.SessionToken;
+import org.junit.Test;
+
+import java.time.Instant;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.Assert.*;
+
+/**
+ * Security-specific tests for session management
+ */
+public class SessionSecurityTest {
+
+ @Test
+ public void testSessionIdGeneration() {
+ SessionIdGenerator generator = new SessionIdGenerator();
+
+ String id1 = generator.generateSessionId();
+ String id2 = generator.generateSessionId();
+
+ assertNotNull(id1);
+ assertNotNull(id2);
+ assertNotEquals(id1, id2);
+ assertTrue(id1.length() > 20);
+ }
+
+ @Test
+ public void testSessionIdUniqueness() {
+ SessionIdGenerator generator = new SessionIdGenerator();
+ Set ids = new HashSet<>();
+
+ for (int i = 0; i < 1000; i++) {
+ String id = generator.generateSessionId();
+ assertFalse("Duplicate session ID generated", ids.contains(id));
+ ids.add(id);
+ }
+ }
+
+ @Test
+ public void testSessionTokenCreation() {
+ SessionToken token = new SessionToken.Builder()
+ .tokenId("token123")
+ .sessionId("session456")
+ .userId("user789")
+ .expiresInSeconds(3600)
+ .addClaim("role", "admin")
+ .build();
+
+ assertNotNull(token);
+ assertEquals("token123", token.getTokenId());
+ assertEquals("session456", token.getSessionId());
+ assertEquals("user789", token.getUserId());
+ assertNotNull(token.getIssuedAt());
+ assertNotNull(token.getExpiresAt());
+ assertFalse(token.isExpired());
+ assertEquals("admin", token.getClaim("role"));
+ }
+
+ @Test
+ public void testSessionTokenExpiration() {
+ SessionToken token = new SessionToken.Builder()
+ .tokenId("token123")
+ .sessionId("session456")
+ .userId("user789")
+ .expiresAt(Instant.now().minusSeconds(10))
+ .build();
+
+ assertTrue(token.isExpired());
+ }
+
+ @Test
+ public void testSessionEncryption() throws Exception {
+ SessionEncryption encryption = SessionEncryption.withRandomKey();
+
+ String plaintext = "sensitive session data";
+ String encrypted = encryption.encrypt(plaintext);
+
+ assertNotNull(encrypted);
+ assertNotEquals(plaintext, encrypted);
+
+ String decrypted = encryption.decrypt(encrypted);
+ assertEquals(plaintext, decrypted);
+ }
+
+ @Test
+ public void testSessionEncryptionWithKey() throws Exception {
+ SessionEncryption encryption1 = SessionEncryption.withRandomKey();
+ String key = encryption1.getKeyAsBase64();
+
+ SessionEncryption encryption2 = SessionEncryption.fromBase64Key(key);
+
+ String plaintext = "test data";
+ String encrypted = encryption1.encrypt(plaintext);
+ String decrypted = encryption2.decrypt(encrypted);
+
+ assertEquals(plaintext, decrypted);
+ }
+
+ @Test
+ public void testSessionBindingValidation() {
+ SessionHijackingPrevention prevention = new SessionHijackingPrevention(true, true, 5);
+
+ Session session = new Session("session123", "user456");
+ session.setIpAddress("192.168.1.100");
+ session.setUserAgent("Mozilla/5.0");
+
+ assertTrue(prevention.validateSessionBinding(session, "192.168.1.100", "Mozilla/5.0"));
+ assertFalse(prevention.validateSessionBinding(session, "192.168.1.200", "Mozilla/5.0"));
+ assertFalse(prevention.validateSessionBinding(session, "192.168.1.100", "Chrome/90.0"));
+ }
+
+ @Test
+ public void testSessionBindingWithoutEnforcement() {
+ SessionHijackingPrevention prevention = new SessionHijackingPrevention(false, false, 5);
+
+ Session session = new Session("session123", "user456");
+ session.setIpAddress("192.168.1.100");
+ session.setUserAgent("Mozilla/5.0");
+
+ assertTrue(prevention.validateSessionBinding(session, "192.168.1.200", "Chrome/90.0"));
+ }
+
+ @Test
+ public void testSuspiciousActivityDetection() {
+ SessionHijackingPrevention prevention = new SessionHijackingPrevention(true, true, 5);
+
+ Session session = new Session("session123", "user456");
+ session.setIpAddress("192.168.1.100");
+
+ assertFalse(prevention.detectSuspiciousActivity(session, "192.168.1.101"));
+ assertTrue(prevention.detectSuspiciousActivity(session, "10.0.0.1"));
+ }
+
+ @Test
+ public void testConcurrentSessionLimit() {
+ SessionHijackingPrevention prevention = new SessionHijackingPrevention(true, true, 3);
+
+ assertFalse(prevention.isConcurrentSessionLimitExceeded(2));
+ assertTrue(prevention.isConcurrentSessionLimitExceeded(3));
+ assertTrue(prevention.isConcurrentSessionLimitExceeded(5));
+ }
+
+ @Test
+ public void testSessionIdRegeneration() {
+ SessionHijackingPrevention prevention = new SessionHijackingPrevention(true, true, 5);
+ SessionIdGenerator generator = new SessionIdGenerator();
+
+ String oldId = "oldSession123";
+ String newId = prevention.regenerateSessionId(oldId, generator);
+
+ assertNotNull(newId);
+ assertNotEquals(oldId, newId);
+ }
+
+ @Test
+ public void testSessionFixationDetection() {
+ SessionHijackingPrevention prevention = new SessionHijackingPrevention(true, true, 5);
+
+ Session session = new Session("session123", "user456");
+
+ assertFalse(prevention.detectSessionFixation(session));
+ }
+}
diff --git a/Common/src/test/java/gov/uspto/session/SessionTest.java b/Common/src/test/java/gov/uspto/session/SessionTest.java
new file mode 100644
index 0000000..50760ee
--- /dev/null
+++ b/Common/src/test/java/gov/uspto/session/SessionTest.java
@@ -0,0 +1,117 @@
+package gov.uspto.session;
+
+import gov.uspto.session.model.ReauthReason;
+import gov.uspto.session.model.Session;
+import gov.uspto.session.model.SessionState;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests for Session entity
+ */
+public class SessionTest {
+
+ @Test
+ public void testSessionCreation() {
+ Session session = new Session("session123", "user456");
+
+ assertNotNull(session);
+ assertEquals("session123", session.getSessionId());
+ assertEquals("user456", session.getUserId());
+ assertEquals(SessionState.ACTIVE, session.getState());
+ assertNotNull(session.getCreatedAt());
+ assertNotNull(session.getLastAccessed());
+ assertNotNull(session.getLastReauthentication());
+ }
+
+ @Test
+ public void testSessionAttributes() {
+ Session session = new Session("session123", "user456");
+
+ session.setAttribute("key1", "value1");
+ session.setAttribute("key2", 42);
+
+ assertEquals("value1", session.getAttribute("key1"));
+ assertEquals(42, session.getAttribute("key2"));
+ assertEquals(2, session.getAttributes().size());
+
+ session.removeAttribute("key1");
+ assertNull(session.getAttribute("key1"));
+ assertEquals(1, session.getAttributes().size());
+ }
+
+ @Test
+ public void testSecurityAttributes() {
+ Session session = new Session("session123", "user456");
+
+ session.setSecurityAttribute("role", "admin");
+ session.setSecurityAttribute("clearance", "secret");
+
+ assertEquals("admin", session.getSecurityAttribute("role"));
+ assertEquals("secret", session.getSecurityAttribute("clearance"));
+ assertEquals(2, session.getSecurityAttributes().size());
+ }
+
+ @Test
+ public void testReauthenticationTracking() {
+ Session session = new Session("session123", "user456");
+
+ assertFalse(session.requiresReauthentication());
+ assertEquals(0, session.getPendingReauthReasons().size());
+
+ session.addReauthReason(ReauthReason.PRIVILEGE_ESCALATION);
+ assertTrue(session.requiresReauthentication());
+ assertEquals(1, session.getPendingReauthReasons().size());
+ assertTrue(session.getPendingReauthReasons().contains(ReauthReason.PRIVILEGE_ESCALATION));
+ assertEquals(SessionState.REQUIRES_REAUTH, session.getState());
+
+ session.addReauthReason(ReauthReason.ROLE_CHANGE);
+ assertEquals(2, session.getPendingReauthReasons().size());
+
+ session.markReauthenticated();
+ assertFalse(session.requiresReauthentication());
+ assertEquals(0, session.getPendingReauthReasons().size());
+ assertEquals(SessionState.ACTIVE, session.getState());
+ }
+
+ @Test
+ public void testSessionState() {
+ Session session = new Session("session123", "user456");
+
+ assertTrue(session.isActive());
+ assertEquals(SessionState.ACTIVE, session.getState());
+
+ session.setState(SessionState.EXPIRED);
+ assertFalse(session.isActive());
+ assertEquals(SessionState.EXPIRED, session.getState());
+
+ session.setState(SessionState.TERMINATED);
+ assertFalse(session.isActive());
+ assertEquals(SessionState.TERMINATED, session.getState());
+ }
+
+ @Test
+ public void testLastAccessedUpdate() {
+ Session session = new Session("session123", "user456");
+
+ assertEquals(0, session.getAccessCount());
+
+ session.updateLastAccessed();
+ assertEquals(1, session.getAccessCount());
+
+ session.updateLastAccessed();
+ assertEquals(2, session.getAccessCount());
+ }
+
+ @Test
+ public void testSecurityContext() {
+ Session session = new Session("session123", "user456");
+
+ session.setIpAddress("192.168.1.100");
+ session.setUserAgent("Mozilla/5.0");
+
+ assertEquals("192.168.1.100", session.getIpAddress());
+ assertEquals("Mozilla/5.0", session.getUserAgent());
+ }
+}