Browse Source

cookie handling refactoring and samesite cookie attribute

jrivard@gmail.com 6 years ago
parent
commit
db3f78a81c

+ 2 - 1
server/src/main/java/password/pwm/AppProperty.java

@@ -111,6 +111,7 @@ public enum AppProperty
     HTTP_RESOURCES_NONCE_PATH_PREFIX                ( "http.resources.pathNoncePrefix" ),
     HTTP_RESOURCES_ZIP_FILES                        ( "http.resources.zipFiles" ),
     HTTP_COOKIE_DEFAULT_SECURE_FLAG                 ( "http.cookie.default.secureFlag" ),
+    HTTP_COOKIE_HTTPONLY_ENABLE                     ( "http.cookie.httponly.enable" ),
     HTTP_COOKIE_THEME_NAME                          ( "http.cookie.theme.name" ),
     HTTP_COOKIE_THEME_AGE                           ( "http.cookie.theme.age" ),
     HTTP_COOKIE_LOCALE_NAME                         ( "http.cookie.locale.name" ),
@@ -122,6 +123,7 @@ public enum AppProperty
     HTTP_COOKIE_LOGIN_NAME                          ( "http.cookie.login.name" ),
     HTTP_COOKIE_NONCE_NAME                          ( "http.cookie.nonce.name" ),
     HTTP_COOKIE_NONCE_LENGTH                        ( "http.cookie.nonce.length" ),
+    HTTP_COOKIE_SAMESITE_VALUE                      ( "http.cookie.sameSite.value" ),
     HTTP_BASIC_AUTH_CHARSET                         ( "http.basicAuth.charset" ),
     HTTP_BODY_MAXREAD_LENGTH                        ( "http.body.maxReadLength" ),
     HTTP_CLIENT_ALWAYS_LOG_ENTITIES                 ( "http.client.alwaysLogEntities" ),
@@ -165,7 +167,6 @@ public enum AppProperty
     HTTP_PARAM_OAUTH_GRANT_TYPE                     ( "http.parameter.oauth.grantType" ),
     HTTP_DOWNLOAD_BUFFER_SIZE                       ( "http.download.buffer.size" ),
     HTTP_SESSION_RECYCLE_AT_AUTH                    ( "http.session.recycleAtAuth" ),
-    HTTP_SESSION_VALIDATION_KEY_LENGTH              ( "http.session.validationKeyLength" ),
     HTTP_SERVLET_ENABLE_POST_REDIRECT_GET           ( "http.servlet.enablePostRedirectGet" ),
     L10N_RTL_REGEX                                  ( "l10n.rtl.regex" ),
     LOCALDB_AGGRESSIVE_COMPACT_ENABLED              ( "localdb.aggressiveCompact.enabled" ),

+ 5 - 15
server/src/main/java/password/pwm/bean/LocalSessionStateBean.java

@@ -23,12 +23,12 @@
 package password.pwm.bean;
 
 import lombok.Data;
-import password.pwm.PwmApplication;
 import password.pwm.ldap.UserInfoBean;
 
 import java.io.Serializable;
 import java.time.Instant;
 import java.util.Locale;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * <p>Only information that is particular to the http session is stored in the
@@ -65,30 +65,20 @@ public class LocalSessionStateBean implements Serializable
     private boolean passwordModified;
     private boolean privateUrlAccessed;
 
-    private int intruderAttempts;
+    private final AtomicInteger intruderAttempts = new AtomicInteger( 0 );
     private boolean oauthInProgress;
 
-    private int sessionVerificationKeyLength;
     private boolean sessionIdRecycleNeeded;
-
-    public LocalSessionStateBean( final int sessionVerificationKeyLength )
-    {
-        this.sessionVerificationKeyLength = sessionVerificationKeyLength;
-    }
+    private boolean sameSiteCookieRecycleRequested;
 
     public void incrementIntruderAttempts( )
     {
-        intruderAttempts++;
+        intruderAttempts.incrementAndGet();
     }
 
     public void clearIntruderAttempts( )
     {
-        intruderAttempts = 0;
-    }
-
-    public void regenerateSessionVerificationKey( final PwmApplication pwmApplication )
-    {
-        sessionVerificationKey = pwmApplication.getSecureService().pwmRandom().alphaNumericString( sessionVerificationKeyLength ) + Long.toHexString( System.currentTimeMillis() );
+        intruderAttempts.set( 0 );
     }
 }
 

+ 23 - 3
server/src/main/java/password/pwm/http/PwmHttpResponseWrapper.java

@@ -23,9 +23,13 @@
 package password.pwm.http;
 
 import password.pwm.AppProperty;
+import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.config.Configuration;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.filter.CookieManagementFilter;
 import password.pwm.util.Validator;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 
@@ -35,7 +39,6 @@ import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.PrintWriter;
-import java.util.Arrays;
 
 public class PwmHttpResponseWrapper
 {
@@ -174,7 +177,8 @@ public class PwmHttpResponseWrapper
             }
         }
 
-        final boolean httpOnly = flags == null || !Arrays.asList( flags ).contains( Flag.NonHttpOnly );
+        final boolean httpOnlyEnabled = Boolean.parseBoolean( configuration.readAppProperty( AppProperty.HTTP_COOKIE_HTTPONLY_ENABLE ) );
+        final boolean httpOnly = httpOnlyEnabled && JavaHelper.enumArrayContainsValue( flags, Flag.NonHttpOnly );
 
         final String value;
         {
@@ -184,7 +188,7 @@ public class PwmHttpResponseWrapper
             }
             else
             {
-                if ( flags != null && Arrays.asList( flags ).contains( Flag.BypassSanitation ) )
+                if ( JavaHelper.enumArrayContainsValue( flags, Flag.BypassSanitation ) )
                 {
                     value = StringUtil.urlEncode( cookieValue );
                 }
@@ -208,6 +212,22 @@ public class PwmHttpResponseWrapper
             LOGGER.warn( "writing large cookie to response: cookieName=" + cookieName + ", length=" + value.length() );
         }
         this.getHttpServletResponse().addCookie( theCookie );
+        addSameSiteCookieAttribute();
+    }
+
+    void addSameSiteCookieAttribute( )
+    {
+        final PwmApplication pwmApplication;
+        try
+        {
+            pwmApplication = ContextManager.getPwmApplication( this.httpServletRequest );
+            final String value = pwmApplication.getConfig().readAppProperty( AppProperty.HTTP_COOKIE_SAMESITE_VALUE );
+            CookieManagementFilter.addSameSiteCookieAttribute( httpServletResponse, value );
+        }
+        catch ( PwmUnrecoverableException e )
+        {
+            LOGGER.trace( () -> "unable to load application configuration while checking samesite cookie attribute config", e );
+        }
     }
 
     public void removeCookie( final String cookieName, final CookiePath path )

+ 2 - 22
server/src/main/java/password/pwm/http/PwmSession.java

@@ -38,7 +38,6 @@ import password.pwm.ldap.UserInfoBean;
 import password.pwm.ldap.UserInfoFactory;
 import password.pwm.ldap.auth.AuthenticationType;
 import password.pwm.svc.stats.Statistic;
-import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
@@ -48,7 +47,6 @@ import password.pwm.util.secure.PwmRandom;
 import password.pwm.util.secure.PwmSecurityKey;
 
 import java.io.Serializable;
-import java.math.BigInteger;
 import java.time.Instant;
 import java.util.Date;
 import java.util.LinkedHashMap;
@@ -61,7 +59,6 @@ import java.util.Map;
  */
 public class PwmSession implements Serializable
 {
-
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmSession.class );
 
     private final LocalSessionStateBean sessionStateBean;
@@ -92,25 +89,8 @@ public class PwmSession implements Serializable
             throw new IllegalStateException( "PwmApplication must be available during session creation" );
         }
 
-        final int sessionValidationKeyLength = Integer.parseInt( pwmApplication.getConfig().readAppProperty( AppProperty.HTTP_SESSION_VALIDATION_KEY_LENGTH ) );
-        sessionStateBean = new LocalSessionStateBean( sessionValidationKeyLength );
-        sessionStateBean.regenerateSessionVerificationKey( pwmApplication );
-        this.sessionStateBean.setSessionID( null );
-
-        final StatisticsManager statisticsManager = pwmApplication.getStatisticsManager();
-        if ( statisticsManager != null )
-        {
-            String nextID = pwmApplication.getStatisticsManager().getStatBundleForKey( StatisticsManager.KEY_CUMULATIVE ).getStatistic( Statistic.HTTP_SESSIONS );
-            try
-            {
-                nextID = new BigInteger( nextID ).toString();
-            }
-            catch ( NumberFormatException e )
-            {
-                LOGGER.debug( this, () -> "error generating sessionID: " + e.getMessage(), e );
-            }
-            this.getSessionStateBean().setSessionID( nextID );
-        }
+        sessionStateBean = new LocalSessionStateBean();
+        this.sessionStateBean.setSessionID( pwmApplication.getSessionTrackService().generateNewSessionID() );
 
         this.sessionStateBean.setSessionLastAccessedTime( Instant.now() );
 

+ 124 - 90
server/src/main/java/password/pwm/http/filter/ConfigAccessFilter.java

@@ -31,6 +31,7 @@ import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.ConfigurationReader;
+import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfigurationImpl;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
@@ -48,6 +49,7 @@ import password.pwm.svc.intruder.RecordType;
 import password.pwm.svc.sessiontrack.UserAgentUtils;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.PwmHashAlgorithm;
@@ -104,8 +106,7 @@ public class ConfigAccessFilter extends AbstractPwmFilter
         return pwmURL.isConfigManagerURL();
     }
 
-    @SuppressWarnings( "checkstyle:MethodLength" )
-    static ProcessStatus checkAuthentication(
+    private static ProcessStatus checkAuthentication(
             final PwmRequest pwmRequest,
             final ConfigManagerBean configManagerBean
     )
@@ -116,33 +117,7 @@ public class ConfigAccessFilter extends AbstractPwmFilter
         final ConfigurationReader runningConfigReader = ContextManager.getContextManager( pwmRequest.getHttpServletRequest().getSession() ).getConfigReader();
         final StoredConfigurationImpl storedConfig = runningConfigReader.getStoredConfiguration();
 
-        boolean authRequired = false;
-        if ( storedConfig.hasPassword() )
-        {
-            authRequired = true;
-        }
-
-        if ( PwmApplicationMode.RUNNING == pwmRequest.getPwmApplication().getApplicationMode() )
-        {
-            if ( !pwmRequest.isAuthenticated() )
-            {
-                throw new PwmUnrecoverableException( PwmError.ERROR_AUTHENTICATION_REQUIRED );
-            }
-
-            if ( !pwmRequest.getPwmSession().getSessionManager().checkPermission( pwmRequest.getPwmApplication(), Permission.PWMADMIN ) )
-            {
-                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_UNAUTHORIZED );
-                denyAndError( pwmRequest, errorInformation );
-                return ProcessStatus.Halt;
-            }
-        }
-
-        if ( PwmApplicationMode.CONFIGURATION != pwmRequest.getPwmApplication().getApplicationMode() )
-        {
-            authRequired = true;
-        }
-
-        if ( !authRequired )
+        if ( !checkIfAuthIsRequired( pwmRequest, storedConfig ) )
         {
             return ProcessStatus.Continue;
         }
@@ -163,68 +138,9 @@ public class ConfigAccessFilter extends AbstractPwmFilter
             return ProcessStatus.Continue;
         }
 
-        String persistentLoginValue = null;
-        boolean persistentLoginAccepted = false;
-        boolean persistentLoginEnabled = false;
-        if ( pwmRequest.getConfig().isDefaultValue( PwmSetting.PWM_SECURITY_KEY ) )
-        {
-            LOGGER.debug( pwmRequest, () -> "security not available, persistent login not possible." );
-        }
-        else
-        {
-            persistentLoginEnabled = true;
-            final PwmSecurityKey securityKey = pwmRequest.getConfig().getSecurityKey();
+        final boolean persistentLoginEnabled = persistentLoginEnabled( pwmRequest );
+        final boolean persistentLoginAccepted = checkPersistentLoginCookie( pwmRequest, storedConfig );
 
-            if ( PwmApplicationMode.RUNNING == pwmRequest.getPwmApplication().getApplicationMode() )
-            {
-                persistentLoginValue = SecureEngine.hash(
-                        storedConfig.readConfigProperty( ConfigurationProperty.PASSWORD_HASH )
-                                + pwmSession.getUserInfo().getUserIdentity().toDelimitedKey(),
-                        PwmHashAlgorithm.SHA512 );
-
-            }
-            else
-            {
-                persistentLoginValue = SecureEngine.hash(
-                        storedConfig.readConfigProperty( ConfigurationProperty.PASSWORD_HASH ),
-                        PwmHashAlgorithm.SHA512 );
-            }
-
-            {
-                final String cookieStr = pwmRequest.readCookie( PwmConstants.COOKIE_PERSISTENT_CONFIG_LOGIN );
-                if ( securityKey != null && cookieStr != null && !cookieStr.isEmpty() )
-                {
-                    try
-                    {
-                        final String jsonStr = pwmApplication.getSecureService().decryptStringValue( cookieStr );
-                        final PersistentLoginInfo persistentLoginInfo = JsonUtil.deserialize( jsonStr, PersistentLoginInfo.class );
-                        if ( persistentLoginInfo != null && persistentLoginValue != null )
-                        {
-                            if ( persistentLoginInfo.getExpireDate().isAfter( Instant.now() ) )
-                            {
-                                if ( persistentLoginValue.equals( persistentLoginInfo.getPassword() ) )
-                                {
-                                    persistentLoginAccepted = true;
-                                    LOGGER.debug( pwmRequest, () -> "accepting persistent config login from cookie (expires "
-                                            + JavaHelper.toIsoDate( persistentLoginInfo.getExpireDate() )
-                                            + ")"
-                                    );
-                                }
-                            }
-                        }
-                    }
-                    catch ( Exception e )
-                    {
-                        LOGGER.error( pwmRequest, "error examining persistent config login cookie: " + e.getMessage() );
-                    }
-                    if ( !persistentLoginAccepted )
-                    {
-                        pwmRequest.getPwmResponse().removeCookie( PwmConstants.COOKIE_PERSISTENT_CONFIG_LOGIN, null );
-                        LOGGER.debug( pwmRequest, () -> "removing non-working persistent config login cookie" );
-                    }
-                }
-            }
-        }
 
 
         final String password = pwmRequest.readParameterAsString( "password" );
@@ -256,12 +172,14 @@ public class ConfigAccessFilter extends AbstractPwmFilter
             configManagerBean.setPasswordVerified( true );
             pwmApplication.getIntruderManager().convenience().clearAddressAndSession( pwmSession );
             pwmApplication.getIntruderManager().clear( RecordType.USERNAME, PwmConstants.CONFIGMANAGER_INTRUDER_USERNAME );
+            pwmRequest.getPwmSession().getSessionStateBean().setSessionIdRecycleNeeded( true );
             if ( persistentLoginEnabled && !persistentLoginAccepted && "on".equals( pwmRequest.readParameterAsString( "remember" ) ) )
             {
                 final int persistentSeconds = figureMaxLoginSeconds( pwmRequest );
                 if ( persistentSeconds > 0 )
                 {
                     final Instant expirationDate = Instant.ofEpochMilli( System.currentTimeMillis() + ( persistentSeconds * 1000 ) );
+                    final String persistentLoginValue = makePersistentLoginValue( pwmRequest, storedConfig );
                     final PersistentLoginInfo persistentLoginInfo = new PersistentLoginInfo( expirationDate, persistentLoginValue );
                     final String jsonPersistentLoginInfo = JsonUtil.serialize( persistentLoginInfo );
                     final String cookieValue = pwmApplication.getSecureService().encryptToString( jsonPersistentLoginInfo );
@@ -293,6 +211,122 @@ public class ConfigAccessFilter extends AbstractPwmFilter
         return ProcessStatus.Halt;
     }
 
+    private static boolean checkPersistentLoginCookie(
+            final PwmRequest pwmRequest,
+            final StoredConfiguration storedConfig
+
+    )
+            throws PwmUnrecoverableException
+    {
+        final PwmSecurityKey securityKey = pwmRequest.getConfig().getSecurityKey();
+        final String cookieStr = pwmRequest.readCookie( PwmConstants.COOKIE_PERSISTENT_CONFIG_LOGIN );
+        if ( securityKey != null && cookieStr != null && !cookieStr.isEmpty() )
+        {
+            try
+            {
+                final String persistentLoginValue = makePersistentLoginValue( pwmRequest, storedConfig );
+
+                final String jsonStr = pwmRequest.getPwmApplication().getSecureService().decryptStringValue( cookieStr );
+                final PersistentLoginInfo persistentLoginInfo = JsonUtil.deserialize( jsonStr, PersistentLoginInfo.class );
+                if ( persistentLoginInfo != null && persistentLoginValue != null )
+                {
+                    if ( persistentLoginInfo.getExpireDate().isAfter( Instant.now() ) )
+                    {
+
+                        if ( persistentLoginValue.equals( persistentLoginInfo.getPassword() ) )
+                        {
+                            LOGGER.debug( pwmRequest, () -> "accepting persistent config login from cookie (expires "
+                                    + JavaHelper.toIsoDate( persistentLoginInfo.getExpireDate() )
+                                    + ")"
+                            );
+                            return true;
+                        }
+                    }
+                }
+            }
+            catch ( Exception e )
+            {
+                LOGGER.error( pwmRequest, "error examining persistent config login cookie: " + e.getMessage() );
+            }
+            if ( !StringUtil.isEmpty( cookieStr ) )
+            {
+                pwmRequest.getPwmResponse().removeCookie( PwmConstants.COOKIE_PERSISTENT_CONFIG_LOGIN, null );
+                LOGGER.debug( pwmRequest, () -> "removing non-working persistent config login cookie" );
+            }
+        }
+
+        return false;
+    }
+
+
+    private static boolean checkIfAuthIsRequired(
+            final PwmRequest pwmRequest,
+            final StoredConfigurationImpl storedConfig
+    )
+            throws PwmUnrecoverableException
+    {
+        if ( storedConfig.hasPassword() )
+        {
+            return true;
+        }
+
+        if ( PwmApplicationMode.RUNNING == pwmRequest.getPwmApplication().getApplicationMode() )
+        {
+            if ( !pwmRequest.isAuthenticated() )
+            {
+                throw new PwmUnrecoverableException( PwmError.ERROR_AUTHENTICATION_REQUIRED );
+            }
+
+            if ( !pwmRequest.getPwmSession().getSessionManager().checkPermission( pwmRequest.getPwmApplication(), Permission.PWMADMIN ) )
+            {
+                throw new PwmUnrecoverableException( PwmError.ERROR_UNAUTHORIZED );
+            }
+        }
+
+        if ( PwmApplicationMode.CONFIGURATION != pwmRequest.getPwmApplication().getApplicationMode() )
+        {
+            return true;
+        }
+
+        return false;
+    }
+
+    private static boolean persistentLoginEnabled(
+            final PwmRequest pwmRequest
+    )
+    {
+        if ( pwmRequest.getConfig().isDefaultValue( PwmSetting.PWM_SECURITY_KEY ) )
+        {
+            LOGGER.debug( pwmRequest, () -> "security not available, persistent login not possible." );
+            return false;
+        }
+
+        return true;
+    }
+
+    private static String makePersistentLoginValue(
+            final PwmRequest pwmRequest,
+            final StoredConfiguration storedConfig
+    )
+            throws PwmUnrecoverableException
+    {
+        if ( PwmApplicationMode.RUNNING == pwmRequest.getPwmApplication().getApplicationMode() )
+        {
+            final PwmSession pwmSession = pwmRequest.getPwmSession();
+            return SecureEngine.hash(
+                    storedConfig.readConfigProperty( ConfigurationProperty.PASSWORD_HASH )
+                            + pwmSession.getUserInfo().getUserIdentity().toDelimitedKey(),
+                    PwmHashAlgorithm.SHA512 );
+
+        }
+        else
+        {
+            return SecureEngine.hash(
+                    storedConfig.readConfigProperty( ConfigurationProperty.PASSWORD_HASH ),
+                    PwmHashAlgorithm.SHA512 );
+        }
+    }
+
     private static void forwardToJsp( final PwmRequest pwmRequest )
             throws ServletException, PwmUnrecoverableException, IOException
     {

+ 148 - 0
server/src/main/java/password/pwm/http/filter/CookieManagementFilter.java

@@ -0,0 +1,148 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.http.filter;
+
+import password.pwm.AppProperty;
+import password.pwm.PwmApplication;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.ContextManager;
+import password.pwm.http.HttpHeader;
+import password.pwm.http.PwmSession;
+import password.pwm.http.PwmSessionWrapper;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.logging.PwmLogger;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.io.IOException;
+import java.util.Collection;
+
+public class CookieManagementFilter implements Filter
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( CookieManagementFilter.class );
+
+    private String value;
+
+    @Override
+    public void init( final FilterConfig filterConfig )
+            throws ServletException
+    {
+        final PwmApplication pwmApplication;
+        try
+        {
+            pwmApplication = ContextManager.getPwmApplication( filterConfig.getServletContext() );
+            value = pwmApplication.getConfig().readAppProperty( AppProperty.HTTP_COOKIE_SAMESITE_VALUE );
+        }
+        catch ( PwmUnrecoverableException e )
+        {
+            LOGGER.trace( () -> "unable to load application configuration while checking samesite cookie attribute config", e );
+        }
+    }
+
+    @Override
+    public void destroy()
+    {
+
+    }
+
+    @Override
+    public void doFilter( final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain )
+            throws IOException, ServletException
+    {
+        filterChain.doFilter( servletRequest, servletResponse );
+        addSameSiteCookieAttribute( ( HttpServletResponse ) servletResponse, value );
+        markSessionForRecycle( ( HttpServletRequest ) servletRequest );
+    }
+
+    private void markSessionForRecycle( final HttpServletRequest httpServletRequest )
+    {
+        if ( StringUtil.isEmpty( value ) )
+        {
+            return;
+        }
+
+        final HttpSession httpSession = httpServletRequest.getSession( false );
+        if ( httpSession != null )
+        {
+            PwmSession pwmSession = null;
+            try
+            {
+                pwmSession = PwmSessionWrapper.readPwmSession( httpSession );
+            }
+            catch ( PwmUnrecoverableException e )
+            {
+                LOGGER.trace( () -> "unable to load session while checking samesite cookie attribute config", e );
+            }
+
+            if ( pwmSession != null )
+            {
+                if ( !pwmSession.getSessionStateBean().isSameSiteCookieRecycleRequested() )
+                {
+                    pwmSession.getSessionStateBean().setSameSiteCookieRecycleRequested( true );
+                    pwmSession.getSessionStateBean().setSessionIdRecycleNeeded( true );
+                }
+            }
+        }
+    }
+
+    public static void addSameSiteCookieAttribute( final HttpServletResponse response, final String value )
+    {
+        if ( StringUtil.isEmpty( value ) )
+        {
+            return;
+        }
+
+        final Collection<String> headers = response.getHeaders( HttpHeader.SetCookie.getHttpName() );
+        boolean firstHeader = true;
+
+        for ( final String header : headers )
+        {
+            final String newHeader;
+            if ( !header.contains( "SameSite" ) )
+            {
+                newHeader = header + "; SameSite=" + value;
+            }
+            else
+            {
+                newHeader = header;
+            }
+
+            if ( firstHeader )
+            {
+                response.setHeader( HttpHeader.SetCookie.getHttpName(), newHeader );
+                firstHeader = false;
+            }
+            else
+            {
+                response.addHeader( HttpHeader.SetCookie.getHttpName(), newHeader );
+            }
+        }
+    }
+}

+ 3 - 42
server/src/main/java/password/pwm/http/filter/RequestInitializationFilter.java

@@ -69,8 +69,6 @@ import java.math.BigInteger;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.time.Instant;
-import java.util.Enumeration;
-import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
@@ -303,49 +301,12 @@ public class RequestInitializationFilter implements Filter
     private void checkIfSessionRecycleNeeded( final PwmRequest pwmRequest )
             throws IOException, ServletException
     {
-        if ( !pwmRequest.getPwmSession().getSessionStateBean().isSessionIdRecycleNeeded() )
+        if ( pwmRequest.getPwmSession().getSessionStateBean().isSessionIdRecycleNeeded() )
         {
-            return;
+            pwmRequest.getHttpServletRequest().changeSessionId();
+            pwmRequest.getPwmSession().getSessionStateBean().setSessionIdRecycleNeeded( false );
         }
 
-        final boolean recycleEnabled = Boolean.parseBoolean( pwmRequest.getConfig().readAppProperty( AppProperty.HTTP_SESSION_RECYCLE_AT_AUTH ) );
-
-        if ( !recycleEnabled )
-        {
-            return;
-        }
-        LOGGER.debug( pwmRequest, () -> "forcing new http session due to authentication" );
-
-        final HttpServletRequest req = pwmRequest.getHttpServletRequest();
-
-        // read the old session data
-        final HttpSession oldSession = req.getSession( true );
-        final int oldMaxInactiveInterval = oldSession.getMaxInactiveInterval();
-        final Map<String, Object> sessionAttributes = new HashMap<>();
-        final Enumeration oldSessionAttrNames = oldSession.getAttributeNames();
-        while ( oldSessionAttrNames.hasMoreElements() )
-        {
-            final String attrName = ( String ) oldSessionAttrNames.nextElement();
-            sessionAttributes.put( attrName, oldSession.getAttribute( attrName ) );
-        }
-
-        for ( final String attrName : sessionAttributes.keySet() )
-        {
-            oldSession.removeAttribute( attrName );
-        }
-
-        //invalidate the old session
-        oldSession.invalidate();
-
-        // make a new session
-        final HttpSession newSession = req.getSession( true );
-
-        // write back all the session data
-        sessionAttributes.keySet().forEach( attrName -> newSession.setAttribute( attrName, sessionAttributes.get( attrName ) ) );
-
-        newSession.setMaxInactiveInterval( oldMaxInactiveInterval );
-
-        pwmRequest.getPwmSession().getSessionStateBean().setSessionIdRecycleNeeded( false );
     }
 
     public static void addPwmResponseHeaders(

+ 8 - 6
server/src/main/java/password/pwm/http/filter/SessionFilter.java

@@ -59,7 +59,6 @@ import java.net.UnknownHostException;
 import java.time.Instant;
 import java.util.Arrays;
 import java.util.Enumeration;
-import java.util.Iterator;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -288,7 +287,6 @@ public class SessionFilter extends AbstractPwmFilter
     )
             throws IOException, ServletException, PwmUnrecoverableException
     {
-        final LocalSessionStateBean ssBean = pwmRequest.getPwmSession().getSessionStateBean();
         final HttpServletRequest req = pwmRequest.getHttpServletRequest();
         final PwmResponse pwmResponse = pwmRequest.getPwmResponse();
 
@@ -312,12 +310,17 @@ public class SessionFilter extends AbstractPwmFilter
             return ProcessStatus.Continue;
         }
 
+        final LocalSessionStateBean ssBean = pwmRequest.getPwmSession().getSessionStateBean();
         final String verificationParamName = pwmRequest.getConfig().readAppProperty( AppProperty.HTTP_PARAM_SESSION_VERIFICATION );
         final String keyFromRequest = pwmRequest.readParameterAsString( verificationParamName, PwmHttpRequestWrapper.Flag.BypassValidation );
 
         // request doesn't have key, so make a new one, store it in the session, and redirect back here with the new key.
-        if ( keyFromRequest == null || keyFromRequest.length() < 1 )
+        if ( StringUtil.isEmpty( keyFromRequest ) )
         {
+            if ( StringUtil.isEmpty( ssBean.getSessionVerificationKey() ) )
+            {
+                ssBean.setSessionVerificationKey( pwmRequest.getPwmApplication().getSecureService().pwmRandom().randomUUID().toString() );
+            }
 
             final String returnURL = figureValidationURL( pwmRequest, ssBean.getSessionVerificationKey() );
 
@@ -327,7 +330,7 @@ public class SessionFilter extends AbstractPwmFilter
                 final String httpVersion = pwmRequest.getHttpServletRequest().getProtocol();
                 if ( "HTTP/1.0".equals( httpVersion ) || "HTTP/1.1".equals( httpVersion ) )
                 {
-                    // better chance of detecting un-sticky sessions this way
+                    // better chance of detecting un-sticky sessions this way (closing connection not available in HTTP/2)
                     pwmResponse.setHeader( HttpHeader.Connection, "close" );
                 }
             }
@@ -383,9 +386,8 @@ public class SessionFilter extends AbstractPwmFilter
                 {
                     final List<String> paramValues = Arrays.asList( req.getParameterValues( paramName ) );
 
-                    for ( final Iterator<String> valueIter = paramValues.iterator(); valueIter.hasNext(); )
+                    for ( final String value : paramValues )
                     {
-                        final String value = valueIter.next();
                         redirectURL = PwmURL.appendAndEncodeUrlParameters( redirectURL, paramName, value );
                     }
                 }

+ 8 - 4
server/src/main/java/password/pwm/http/state/CryptoCookieBeanImpl.java

@@ -30,9 +30,11 @@ import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmRequestAttribute;
 import password.pwm.http.bean.PwmSessionBean;
 import password.pwm.util.PasswordData;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.PwmSecurityKey;
+import password.pwm.util.secure.SecureService;
 
 import java.io.Serializable;
 import java.util.HashMap;
@@ -56,7 +58,7 @@ class CryptoCookieBeanImpl implements SessionBeanProvider
         }
 
         final String sessionGuid = pwmRequest.getPwmSession().getLoginInfoBean().getGuid();
-        final String cookieName = nameForClass( theClass );
+        final String cookieName = nameForClass( pwmRequest, theClass );
 
         try
         {
@@ -139,7 +141,7 @@ class CryptoCookieBeanImpl implements SessionBeanProvider
                     for ( final Map.Entry<Class<? extends PwmSessionBean>, PwmSessionBean> entry : beansInRequest.entrySet() )
                     {
                         final Class<? extends PwmSessionBean> theClass = entry.getKey();
-                        final String cookieName = nameForClass( theClass );
+                        final String cookieName = nameForClass( pwmRequest, theClass );
                         final PwmSessionBean bean = entry.getValue();
                         if ( bean == null )
                         {
@@ -180,9 +182,11 @@ class CryptoCookieBeanImpl implements SessionBeanProvider
         return ( Map<Class<? extends PwmSessionBean>, PwmSessionBean> ) sessionBeans;
     }
 
-    private static String nameForClass( final Class<? extends PwmSessionBean> theClass )
+    private static String nameForClass( final PwmRequest pwmRequest, final Class<? extends PwmSessionBean> theClass )
+            throws PwmUnrecoverableException
     {
-        return theClass.getSimpleName();
+        final SecureService secureService = pwmRequest.getPwmApplication().getSecureService();
+        return "b-" + StringUtil.truncate( secureService.hash( theClass.getName() ), 8 );
     }
 
     @Override

+ 12 - 1
server/src/main/java/password/pwm/http/tag/value/PwmValue.java

@@ -62,7 +62,8 @@ public enum PwmValue
     localeDir( new LocaleDirOutput() ),
     localeFlagFile( new LocaleFlagFileOutput() ),
     localeName( new LocaleNameOutput() ),
-    inactiveTimeRemaining( new InactiveTimeRemainingOutput() ),;
+    inactiveTimeRemaining( new InactiveTimeRemainingOutput() ),
+    sessionID( new SessionIDValue() ),;
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmValueTag.class );
 
@@ -295,4 +296,14 @@ public enum PwmValue
             return IdleTimeoutCalculator.idleTimeoutForRequest( pwmRequest ).asLongString();
         }
     }
+
+    static class SessionIDValue implements ValueOutput
+    {
+        @Override
+        public String valueOutput( final PwmRequest pwmRequest, final PageContext pageContext )
+                throws PwmUnrecoverableException
+        {
+            return pwmRequest.getPwmSession().getSessionStateBean().getSessionID();
+        }
+    }
 }

+ 2 - 1
server/src/main/java/password/pwm/svc/intruder/IntruderManager.java

@@ -564,7 +564,7 @@ public class IntruderManager implements PwmService
                 final String subject = pwmSession.getSessionStateBean().getSrcAddress();
                 check( RecordType.ADDRESS, subject );
                 final int maxAllowedAttempts = ( int ) pwmApplication.getConfig().readSettingAsLong( PwmSetting.INTRUDER_SESSION_MAX_ATTEMPTS );
-                if ( maxAllowedAttempts != 0 && pwmSession.getSessionStateBean().getIntruderAttempts() > maxAllowedAttempts )
+                if ( maxAllowedAttempts != 0 && pwmSession.getSessionStateBean().getIntruderAttempts().get() > maxAllowedAttempts )
                 {
                     throw new PwmUnrecoverableException( PwmError.ERROR_INTRUDER_SESSION );
                 }
@@ -579,6 +579,7 @@ public class IntruderManager implements PwmService
                 final String subject = pwmSession.getSessionStateBean().getSrcAddress();
                 clear( RecordType.ADDRESS, subject );
                 pwmSession.getSessionStateBean().clearIntruderAttempts();
+                pwmSession.getSessionStateBean().setSessionIdRecycleNeeded( true );
             }
         }
 

+ 18 - 3
server/src/main/java/password/pwm/svc/sessiontrack/SessionTrackService.java

@@ -42,6 +42,7 @@ import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.secure.PwmRandom;
 
 import java.io.IOException;
 import java.io.OutputStream;
@@ -61,7 +62,7 @@ public class SessionTrackService implements PwmService
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( SessionTrackService.class );
 
-    private final transient Map<PwmSession, Boolean> pwmSessions = new ConcurrentHashMap<>();
+    private final Map<PwmSession, String> pwmSessions = new ConcurrentHashMap<>();
 
     private final Cache<UserIdentity, Object> recentLoginCache = Caffeine.newBuilder()
             .maximumSize( 10 )
@@ -108,7 +109,7 @@ public class SessionTrackService implements PwmService
 
     public void addSessionData( final PwmSession pwmSession )
     {
-        pwmSessions.put( pwmSession, Boolean.FALSE );
+        pwmSessions.put( pwmSession, pwmSession.getSessionStateBean().getSessionID() );
     }
 
     public void removeSessionData( final PwmSession pwmSession )
@@ -263,7 +264,7 @@ public class SessionTrackService implements PwmService
         sessionStateInfoBean.setSrcAddress( loopSsBean.getSrcAddress() );
         sessionStateInfoBean.setSrcHost( loopSsBean.getSrcHostname() );
         sessionStateInfoBean.setLastUrl( loopSsBean.getLastRequestURL() );
-        sessionStateInfoBean.setIntruderAttempts( loopSsBean.getIntruderAttempts() );
+        sessionStateInfoBean.setIntruderAttempts( loopSsBean.getIntruderAttempts().get() );
 
         if ( loopSession.isAuthenticated() )
         {
@@ -306,5 +307,19 @@ public class SessionTrackService implements PwmService
         return Collections.unmodifiableList( new ArrayList<>( recentLoginCache.asMap().keySet() ) );
     }
 
+    public String generateNewSessionID()
+    {
+        final PwmRandom pwmRandom = pwmApplication.getSecureService().pwmRandom();
 
+        for ( int safetyCounter = 0; safetyCounter < 1000; safetyCounter++ )
+        {
+            final String newValue = pwmRandom.alphaNumericString( 5 );
+            if ( !pwmSessions.containsValue( newValue ) )
+            {
+                return newValue;
+            }
+        }
+
+        throw new IllegalStateException( "unable to generate unique sessionID value" );
+    }
 }

+ 3 - 1
server/src/main/java/password/pwm/util/CaptchaUtility.java

@@ -153,6 +153,7 @@ public class CaptchaUtility
                 {
                     writeCaptchaSkipCookie( pwmRequest );
                     LOGGER.trace( pwmRequest, () -> "captcha verification passed" );
+                    StatisticsManager.incrementStat( pwmRequest, Statistic.CAPTCHA_SUCCESSES );
                     return true;
                 }
 
@@ -179,6 +180,7 @@ public class CaptchaUtility
         }
 
         LOGGER.trace( pwmRequest, () -> "captcha verification failed" );
+        StatisticsManager.incrementStat( pwmRequest, Statistic.CAPTCHA_FAILURES );
         return false;
     }
 
@@ -356,7 +358,7 @@ public class CaptchaUtility
             return true;
         }
 
-        final int currentSessionAttempts = pwmRequest.getPwmSession().getSessionStateBean().getIntruderAttempts();
+        final int currentSessionAttempts = pwmRequest.getPwmSession().getSessionStateBean().getIntruderAttempts().get();
         if ( currentSessionAttempts >= maxIntruderCount )
         {
             LOGGER.debug( pwmRequest, () -> "session intruder attempt count '" + currentSessionAttempts + "', therefore captcha will be required" );

+ 5 - 0
server/src/main/java/password/pwm/util/java/TimeDuration.java

@@ -526,5 +526,10 @@ public class TimeDuration implements Comparable, Serializable
             days = ( ( ( totalSeconds / 60 ) / 60 ) / 24 );
         }
     }
+
+    public boolean isZero()
+    {
+        return ms <= 0;
+    }
 }
 

+ 2 - 1
server/src/main/resources/password/pwm/AppProperty.properties

@@ -137,6 +137,7 @@ http.header.sendXXSSProtection=true
 http.header.noise.length=128
 http.header.csp.nonce.bytes=24
 http.cookie.default.secureFlag=auto
+http.cookie.httponly.enable=true
 http.cookie.theme.name=theme
 http.cookie.theme.age=604800
 http.cookie.locale.name=locale
@@ -148,6 +149,7 @@ http.cookie.captchaSkip.age=86400
 http.cookie.login.name=SESSION
 http.cookie.nonce.name=ID
 http.cookie.nonce.length=32
+http.cookie.sameSite.value=Strict
 http.parameter.forward=forwardURL
 http.parameter.logout=logoutURL
 http.parameter.theme=theme
@@ -170,7 +172,6 @@ http.parameter.oauth.state=state
 http.parameter.oauth.grantType=grant_type
 http.download.buffer.size=102400
 http.session.recycleAtAuth=true
-http.session.validationKeyLength=32
 http.servlet.enablePostRedirectGet=true
 intruder.retentionTimeMS=86400000
 intruder.cleanupFrequencyMS=3603000

+ 1 - 2
server/src/main/resources/password/pwm/config/PwmSetting.xml

@@ -1630,8 +1630,7 @@
     <setting hidden="false" key="security.cspHeader" level="2">
         <default>
             <!--<value><![CDATA[]]></value>-->
-            <value><![CDATA[default-src 'self'; object-src 'none'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ 'self' 'unsafe-eval' 'unsafe-inline' 'nonce-%NONCE%' ; frame-src https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/; report-uri /sspr/public/command/cspReport]]></value>
-            <!-- 'unsafe-inline' on script-src is included for backward compatibility of CSP Level1 browsers.  CSP2 and future ignore it when the nonce is specified -->
+            <value><![CDATA[default-src 'self'; object-src 'none'; img-src 'self' data:; style-src 'self'; script-src https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ 'self' 'nonce-%NONCE%' ; frame-src https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/; report-uri /sspr/public/command/cspReport]]></value>
         </default>
     </setting>
     <setting hidden="false" key="email.adminAlert.toAddress" level="1">

+ 1 - 0
webapp/src/main/webapp/WEB-INF/jsp/fragment/header-common.jsp

@@ -39,6 +39,7 @@
 <meta id="application-info" name="application-name" content="<%=PwmConstants.PWM_APP_NAME%> Password Self Service"
       <pwm:if test="<%=PwmIfTest.showVersionHeader%>">data-<%=PwmConstants.PWM_APP_NAME.toLowerCase()%>-version="<%=PwmConstants.BUILD_VERSION%>" data-<%=PwmConstants.PWM_APP_NAME.toLowerCase()%>-build="<%=PwmConstants.BUILD_NUMBER%>"</pwm:if>
       data-<%=PwmConstants.PWM_APP_NAME.toLowerCase()%>-instance="<pwm:value name="<%=PwmValue.instanceID%>"/>"
+      data-session-id="<pwm:value name="<%=PwmValue.sessionID%>"/>"
       data-jsp-name="<pwm:value name="<%=PwmValue.currentJspFilename%>"/>"
       data-url-context="<pwm:context/>"
       data-pwmFormID="<pwm:FormID/>"

+ 8 - 0
webapp/src/main/webapp/WEB-INF/web.xml

@@ -117,6 +117,10 @@
         <url-pattern>/proxyCallback</url-pattern>
     </filter-mapping>
     End CAS Config -->
+    <filter>
+        <filter-name>CookieUpdateFilter</filter-name>
+        <filter-class>password.pwm.http.filter.CookieManagementFilter</filter-class>
+    </filter>
     <filter>
         <filter-name>GZIPFilter</filter-name>
         <filter-class>password.pwm.http.filter.GZIPFilter</filter-class>
@@ -149,6 +153,10 @@
         <filter-name>ConfigAuthorizationFilter</filter-name>
         <filter-class>password.pwm.http.filter.ConfigAccessFilter</filter-class>
     </filter>
+    <filter-mapping>
+        <filter-name>CookieUpdateFilter</filter-name>
+        <url-pattern>/*</url-pattern>
+    </filter-mapping>
     <filter-mapping>
         <filter-name>GZIPFilter</filter-name>
         <url-pattern>/*</url-pattern>