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()); + } +}