SLING-10290 Every request renews sling.formauth token (#1)

plus integration tests to verify the fix
diff --git a/pom.xml b/pom.xml
index 56035b5..bded430 100644
--- a/pom.xml
+++ b/pom.xml
@@ -33,6 +33,9 @@
 	<properties>
 		<site.jira.version.id>12314785</site.jira.version.id>
 		<site.javadoc.exclude>**.impl.**</site.javadoc.exclude>
+		<org.ops4j.pax.exam.version>4.13.3</org.ops4j.pax.exam.version>
+		<!-- To debug the pax process, override this with -D -->
+		<pax.vm.options>-Xmx512M</pax.vm.options>
 	</properties>
 
 	<scm>
@@ -42,6 +45,47 @@
 	  <tag>HEAD</tag>
   </scm>
 
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.servicemix.tooling</groupId>
+				<artifactId>depends-maven-plugin</artifactId>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-failsafe-plugin</artifactId>
+				<executions>
+					<execution>
+						<id>integration-test</id>
+						<phase>integration-test</phase>
+						<goals>
+							<goal>integration-test</goal>
+						</goals>
+					</execution>
+					<execution>
+						<id>verify</id>
+						<phase>integration-test</phase>
+						<goals>
+							<goal>verify</goal>
+						</goals>
+					</execution>
+				</executions>
+				<configuration>
+					<redirectTestOutputToFile>true</redirectTestOutputToFile>
+					<systemProperties>
+						<property>
+							<name>bundle.filename</name>
+							<value>${basedir}/target/${project.build.finalName}.jar</value>
+						</property>
+						<property>
+							<name>pax.vm.options</name>
+							<value>${pax.vm.options}</value>
+						</property>
+					</systemProperties>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
 
 	<dependencies>
 		<dependency>
@@ -79,6 +123,12 @@
 			<scope>provided</scope>
 		</dependency>
 		<dependency>
+			<groupId>org.apache.sling</groupId>
+			<artifactId>org.apache.sling.jcr.resource</artifactId>
+			<version>3.0.22</version>
+			<scope>provided</scope>
+		</dependency>
+		<dependency>
 			<groupId>org.osgi</groupId>
 			<artifactId>osgi.core</artifactId>
 		</dependency>
@@ -153,5 +203,74 @@
 			<version>1.6.4</version>
 			<scope>test</scope>
 		</dependency>
+		<dependency>
+			<groupId>org.ops4j.pax.exam</groupId>
+			<artifactId>pax-exam</artifactId>
+			<version>${org.ops4j.pax.exam.version}</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.ops4j.pax.exam</groupId>
+			<artifactId>pax-exam-cm</artifactId>
+			<version>${org.ops4j.pax.exam.version}</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.ops4j.pax.exam</groupId>
+			<artifactId>pax-exam-container-forked</artifactId>
+			<version>${org.ops4j.pax.exam.version}</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.ops4j.pax.exam</groupId>
+			<artifactId>pax-exam-junit4</artifactId>
+			<version>${org.ops4j.pax.exam.version}</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.ops4j.pax.exam</groupId>
+			<artifactId>pax-exam-link-mvn</artifactId>
+			<version>${org.ops4j.pax.exam.version}</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.sling</groupId>
+			<artifactId>org.apache.sling.testing.paxexam</artifactId>
+			<version>3.1.0</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.httpcomponents</groupId>
+			<artifactId>httpcore</artifactId>
+			<version>4.4.13</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.httpcomponents</groupId>
+			<artifactId>httpclient</artifactId>
+			<version>4.5.13</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.felix</groupId>
+			<artifactId>org.apache.felix.healthcheck.api</artifactId>
+			<version>2.0.4</version>
+			<scope>test</scope>
+		</dependency>
+	   <dependency>
+			<groupId>org.awaitility</groupId>
+			<artifactId>awaitility</artifactId>
+			<version>4.0.0</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>javax.inject</groupId>
+			<artifactId>javax.inject</artifactId>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.felix</groupId>
+			<artifactId>org.apache.felix.framework</artifactId>
+			<version>7.0.0</version>
+			<scope>test</scope>
+		</dependency>
 	</dependencies>
 </project>
diff --git a/src/main/java/org/apache/sling/auth/form/impl/FormAuthenticationHandler.java b/src/main/java/org/apache/sling/auth/form/impl/FormAuthenticationHandler.java
index e09e2d0..d00db01 100644
--- a/src/main/java/org/apache/sling/auth/form/impl/FormAuthenticationHandler.java
+++ b/src/main/java/org/apache/sling/auth/form/impl/FormAuthenticationHandler.java
@@ -48,6 +48,7 @@
 import org.apache.sling.auth.form.FormReason;
 import org.apache.sling.auth.form.impl.jaas.FormCredentials;
 import org.apache.sling.auth.form.impl.jaas.JaasHelper;
+import org.apache.sling.jcr.resource.api.JcrResourceConstants;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.ServiceRegistration;
 import org.osgi.service.component.ComponentContext;
@@ -502,8 +503,7 @@
 		final AuthenticationInfo info = new AuthenticationInfo(HttpServletRequest.FORM_AUTH, userId);
 
 		if (jaasHelper.enabled()) {
-			// JcrResourceConstants.AUTHENTICATION_INFO_CREDENTIALS
-			info.put("user.jcr.credentials", new FormCredentials(userId, authData));
+			info.put(JcrResourceConstants.AUTHENTICATION_INFO_CREDENTIALS, new FormCredentials(userId, authData));
 		} else {
 			info.put(attrCookieAuthData, authData);
 		}
@@ -512,11 +512,19 @@
 	}
 
 	private String getCookieAuthData(final AuthenticationInfo info) {
-		Object data = info.get(attrCookieAuthData);
-		if (data instanceof String) {
-			return (String) data;
+		String authData = null;
+		if (jaasHelper.enabled()) {
+			Object credentials = info.get(JcrResourceConstants.AUTHENTICATION_INFO_CREDENTIALS);
+			if (credentials instanceof Credentials) {
+				authData = getCookieAuthData((Credentials)credentials);
+			}
+		} else {
+			Object data = info.get(attrCookieAuthData);
+			if (data instanceof String) {
+				authData = (String) data;
+			}
 		}
-		return null;
+		return authData;
 	}
 
 	// ---------- LoginModulePlugin support
diff --git a/src/test/java/org/apache/sling/auth/form/it/AuthFormTestSupport.java b/src/test/java/org/apache/sling/auth/form/it/AuthFormTestSupport.java
new file mode 100644
index 0000000..a41b7c2
--- /dev/null
+++ b/src/test/java/org/apache/sling/auth/form/it/AuthFormTestSupport.java
@@ -0,0 +1,251 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.auth.form.it;
+
+import static org.apache.felix.hc.api.FormattingResultLog.msHumanReadable;
+import static org.apache.sling.testing.paxexam.SlingOptions.awaitility;
+import static org.apache.sling.testing.paxexam.SlingOptions.slingQuickstartOakTar;
+import static org.apache.sling.testing.paxexam.SlingOptions.versionResolver;
+import static org.awaitility.Awaitility.await;
+import static org.junit.Assert.assertNotNull;
+import static org.ops4j.pax.exam.CoreOptions.composite;
+import static org.ops4j.pax.exam.CoreOptions.junitBundles;
+import static org.ops4j.pax.exam.CoreOptions.mavenBundle;
+import static org.ops4j.pax.exam.CoreOptions.options;
+import static org.ops4j.pax.exam.CoreOptions.streamBundle;
+import static org.ops4j.pax.exam.CoreOptions.vmOption;
+import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.factoryConfiguration;
+import static org.ops4j.pax.tinybundles.core.TinyBundles.withBnd;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringWriter;
+import java.text.SimpleDateFormat;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import javax.inject.Inject;
+
+import org.apache.felix.hc.api.Result;
+import org.apache.felix.hc.api.ResultLog;
+import org.apache.felix.hc.api.execution.HealthCheckExecutionResult;
+import org.apache.felix.hc.api.execution.HealthCheckExecutor;
+import org.apache.felix.hc.api.execution.HealthCheckSelector;
+import org.apache.sling.testing.paxexam.TestSupport;
+import org.ops4j.pax.exam.Configuration;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.options.ModifiableCompositeOption;
+import org.ops4j.pax.exam.options.extra.VMOption;
+import org.ops4j.pax.tinybundles.core.TinyBundle;
+import org.ops4j.pax.tinybundles.core.TinyBundles;
+import org.osgi.framework.Constants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class AuthFormTestSupport extends TestSupport {
+    private static final String BUNDLE_SYMBOLICNAME = "TEST-CONTENT-BUNDLE";
+    private static final String SLING_BUNDLE_RESOURCES_HEADER = "Sling-Bundle-Resources";
+
+    protected static final String FORM_AUTH_VERIFY_USER = "form-auth-user";
+    protected static final String FORM_AUTH_VERIFY_PWD = "testing";
+
+    protected final Logger logger = LoggerFactory.getLogger(getClass());
+
+    @Inject
+    private HealthCheckExecutor hcExecutor;
+
+
+    @Configuration
+    public Option[] configuration() throws IOException {
+        return options(
+            composite(
+                super.baseConfiguration(),
+                vmOption(System.getProperty("pax.vm.options")),
+                optionalRemoteDebug(),
+                slingQuickstart(),
+                testBundle("bundle.filename"),
+                // testing - add a user to use to login and verify the content loading has happened
+                factoryConfiguration("org.apache.sling.jcr.repoinit.RepositoryInitializer")
+                    .put("scripts", new String[] {
+                            "create user " + FORM_AUTH_VERIFY_USER + " with password " + FORM_AUTH_VERIFY_PWD +"\n"
+                            })
+                    .asOption(),
+                junitBundles(),
+                awaitility()
+            ).add(
+                additionalOptions()
+            ).remove(
+                // remove our bundle under test to avoid duplication
+                mavenBundle().groupId("org.apache.sling").artifactId("org.apache.sling.auth.form").version(versionResolver)
+            )
+        );
+    }
+
+    protected Option[] additionalOptions() throws IOException {
+        return new Option[]{};
+    }
+
+    protected Option slingQuickstart() {
+        final String workingDirectory = workingDirectory();
+        final int httpPort = findFreePort();
+        return composite(
+            slingQuickstartOakTar(workingDirectory, httpPort)
+        );
+    }
+
+    public String getTestFileUrl(String path) {
+        return getClass().getResource(path).toExternalForm();
+    }
+
+    /**
+     * Optionally configure remote debugging on the port supplied by the "debugPort"
+     * system property.
+     */
+    protected ModifiableCompositeOption optionalRemoteDebug() {
+        VMOption option = null;
+        String property = System.getProperty("debugPort");
+        if (property != null) {
+            option = vmOption(String.format("-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=%s", property));
+        }
+        return composite(option);
+    }
+
+    /**
+     * Wait for the health check to be ok
+     * 
+     * @param timeoutMsec the max time to wait for the health check to be ok
+     * @param nextIterationDelay the sleep time between the check attempts
+     */
+    protected void waitForServerReady(long timeoutMsec, long nextIterationDelay) {
+        // retry until the exec call returns true and doesn't throw any exception
+        await().atMost(timeoutMsec, TimeUnit.MILLISECONDS)
+                .pollInterval(nextIterationDelay, TimeUnit.MILLISECONDS)
+                .until(this::doHealthCheck); 
+    }
+
+    /**
+     * @return true if health checks are ok
+     */
+    protected boolean doHealthCheck() throws IOException {
+        boolean isOk = true;
+        logger.info("Performing health check");
+        HealthCheckSelector hcs = HealthCheckSelector.tags("systemalive");
+        List<HealthCheckExecutionResult> results = hcExecutor.execute(hcs);
+        logger.info("systemalive health check got {} results", results.size());
+        isOk &= !results.isEmpty();
+        for (final HealthCheckExecutionResult exR : results) {
+            final Result r = exR.getHealthCheckResult();
+            if (logger.isInfoEnabled()) {
+                logger.info("systemalive health check: {}", toHealthCheckResultInfo(exR, false));
+            }
+            isOk &= r.isOk();
+            if (!isOk) {
+                break; // found a failure so stop checking further
+            }
+        }
+
+        if (isOk) {
+            hcs = HealthCheckSelector.tags("bundles");
+            results = hcExecutor.execute(hcs);
+            logger.info("bundles health check got {} results", results.size());
+            isOk &= !results.isEmpty();
+            for (final HealthCheckExecutionResult exR : results) {
+                final Result r = exR.getHealthCheckResult();
+                if (logger.isInfoEnabled()) {
+                    logger.info("bundles health check: {}", toHealthCheckResultInfo(exR, false));
+                }
+                isOk &= r.isOk();
+                if (!isOk) {
+                    break; // found a failure so stop checking further
+                }
+            }
+        }
+        return isOk;
+    }
+
+    /**
+     * Produce a human readable report of the health check results that is suitable for
+     * debugging or writing to a log
+     */
+    protected String toHealthCheckResultInfo(final HealthCheckExecutionResult exResult, final boolean debug)  throws IOException {
+        String value = null;
+        try (StringWriter resultWriter = new StringWriter(); BufferedWriter writer = new BufferedWriter(resultWriter)) {
+            final Result result = exResult.getHealthCheckResult();
+
+            writer.append('"').append(exResult.getHealthCheckMetadata().getTitle()).append('"');
+            writer.append(" result is: ").append(result.getStatus().toString());
+            writer.newLine();
+            writer.append("   Finished: ").append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(exResult.getFinishedAt()) + " after "
+                    + msHumanReadable(exResult.getElapsedTimeInMs()));
+
+            for (final ResultLog.Entry e : result) {
+                if (!debug && e.isDebug()) {
+                    continue;
+                }
+                writer.newLine();
+                writer.append("   ");
+                writer.append(e.getStatus().toString());
+                writer.append(' ');
+                writer.append(e.getMessage());
+                if (e.getException() != null) {
+                    writer.append(" ");
+                    writer.append(e.getException().toString());
+                }
+            }
+            writer.flush();
+            value = resultWriter.toString();
+        }
+        return value;
+    }
+
+    /**
+     * Add content to our test bundle
+     */
+    protected void addContent(final TinyBundle bundle, String resourcePath) throws IOException {
+        String pathInBundle = resourcePath;
+        resourcePath = "/content" + resourcePath;
+        try (final InputStream is = getClass().getResourceAsStream(resourcePath)) {
+            assertNotNull("Expecting resource to be found:" + resourcePath, is);
+            logger.info("Adding resource to bundle, path={}, resource={}", pathInBundle, resourcePath);
+            bundle.add(pathInBundle, is);
+        }
+    }
+
+    /**
+     * Build a test bundle containing the specified bundle resources
+     * 
+     * @param header the value for the {@link #SLING_BUNDLE_RESOURCES_HEADER} header
+     * @param content the collection of files to embed in the tinybundle
+     * @return the tinybundle Option
+     */
+    protected Option buildBundleResourcesBundle(final String header, final Collection<String> content) throws IOException {
+        final TinyBundle bundle = TinyBundles.bundle();
+        bundle.set(Constants.BUNDLE_SYMBOLICNAME, BUNDLE_SYMBOLICNAME);
+        bundle.set(SLING_BUNDLE_RESOURCES_HEADER, header);
+        bundle.set("Require-Capability", "osgi.extender;filter:=\"(&(osgi.extender=org.apache.sling.bundleresource)(version<=1.1.0)(!(version>=2.0.0)))\"");
+
+        for (final String entry : content) {
+            addContent(bundle, entry);
+        }
+        return streamBundle(
+            bundle.build(withBnd())
+        ).start();
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/auth/form/it/SLING10290IT.java b/src/test/java/org/apache/sling/auth/form/it/SLING10290IT.java
new file mode 100644
index 0000000..c4561a4
--- /dev/null
+++ b/src/test/java/org/apache/sling/auth/form/it/SLING10290IT.java
@@ -0,0 +1,369 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.auth.form.it;
+
+import static org.apache.sling.testing.paxexam.SlingOptions.slingScriptingSightly;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.factoryConfiguration;
+import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.newConfiguration;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Dictionary;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.http.Header;
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.cookie.Cookie;
+import org.apache.http.cookie.CookieOrigin;
+import org.apache.http.cookie.CookieSpec;
+import org.apache.http.cookie.MalformedCookieException;
+import org.apache.http.impl.client.BasicCookieStore;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.impl.cookie.DefaultCookieSpec;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerClass;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+
+/**
+ * integration tests to verify fix for SLING-10290
+ */
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerClass.class)
+public class SLING10290IT extends AuthFormTestSupport {
+
+    @Inject
+    private ConfigurationAdmin cm;
+
+    private static final String COOKIE_SLING_FORMAUTH = "sling.formauth";
+    private static final String HEADER_SET_COOKIE = "Set-Cookie";
+
+    private URI baseServerUri;
+    private HttpClientContext httpContext;
+    private CloseableHttpClient httpClient;
+
+    @Override
+    protected Option[] additionalOptions() throws IOException {
+        // create a tinybundle that contains a test script
+        final List<String> resourcePaths = Arrays.asList("/apps/sling/OrderedFolder/SLING10290IT.html");
+        final String bundleResourcesHeader = String.join(",", resourcePaths);
+        final Option bundle = buildBundleResourcesBundle(bundleResourcesHeader, resourcePaths);
+
+        return new Option[]{
+            // add sightly support for the test script
+            slingScriptingSightly(),
+
+            // add the test script tinybundle
+            bundle,
+
+            // change the formauth timeout to 1 minute so we don't have to wait a long
+            //   time for the testRefreshCookieOnRequestAfterHalfExpirationDuration test
+            newConfiguration("org.apache.sling.auth.form.FormAuthenticationHandler")
+                .put("form.auth.timeout", "1")
+            .asOption(),
+
+            // enable the healthcheck configuration for checking when the server is ready to
+            //  receive http requests.  (adapted from the starter healthcheck.json configuration)
+            factoryConfiguration("org.apache.felix.hc.generalchecks.FrameworkStartCheck")
+                .put("hc.tags", new String[] {"systemalive"})
+                .put("targetStartLevel", 5)
+                .asOption(),
+            factoryConfiguration("org.apache.felix.hc.generalchecks.ServicesCheck")
+                .put("hc.tags", new String[] {"systemalive"})
+                .put("services.list", new String[] {
+                        "org.apache.sling.jcr.api.SlingRepository",
+                        "org.apache.sling.engine.auth.Authenticator",
+                        "org.apache.sling.api.resource.ResourceResolverFactory",
+                        "org.apache.sling.api.servlets.ServletResolver",
+                        "javax.script.ScriptEngineManager"
+                })
+                .asOption(),
+            factoryConfiguration("org.apache.felix.hc.generalchecks.BundlesStartedCheck")
+                .put("hc.tags", new String[] {"bundles"})
+                .asOption(),
+            factoryConfiguration("org.apache.sling.jcr.contentloader.hc.BundleContentLoadedCheck")
+                .put("hc.tags", new String[] {"bundles"})
+                .asOption(),
+        };
+    }
+
+    @Before
+    public void before() throws IOException, URISyntaxException {
+        // wait for the health checks to be OK
+        waitForServerReady(Duration.ofMinutes(1).toMillis(), 500);
+
+        // calculate the address of the http server
+        baseServerUri = getBaseServerUri();
+        assertNotNull(baseServerUri);
+
+        // prepare the http client for the test user
+        httpContext = HttpClientContext.create();
+        httpContext.setCookieStore(new BasicCookieStore());
+        httpClient = HttpClients.custom()
+                .disableRedirectHandling()
+                .build();
+    }
+
+    @After
+    public void after() throws IOException {
+        // close/cleanup the test user http client
+        if (httpClient != null) {
+            httpClient.close();
+            httpClient = null;
+        }
+
+        // clear out other state
+        httpContext = null;
+        baseServerUri = null;
+    }
+
+    /**
+     * Verify that the formauth cookie is sent appropriately after login
+     */
+    @Test
+    public void testSetCookieOnFirstRequestAfterLogin() throws MalformedCookieException, IOException {
+        doFormsLogin();
+    }
+
+    /**
+     * Verify that the formauth cookie is not re-sent on each request after login
+     */
+    @Test
+    public void testNoSetCookieOnSecondRequestAfterLogin() throws MalformedCookieException, IOException {
+        // 1. login as the test user
+        doFormsLogin();
+
+        // 2. do another request
+        HttpGet request = new HttpGet(whoamiUri());
+        try (CloseableHttpResponse response = httpClient.execute(request, httpContext)) {
+            assertEquals(HttpServletResponse.SC_OK, response.getStatusLine().getStatusCode());
+            Cookie parsedFormauthCookie = parseFormAuthCookieFromHeaders(response);
+            assertNull("Did not expect a formauth cookie in the response", parsedFormauthCookie);
+        }
+    }
+
+    /**
+     * Verify that the formauth cookie is refreshed on the first request after half the session duration
+     * has occurred
+     */
+    @Test
+    public void testRefreshCookieOnRequestAfterHalfExpirationDuration() throws InterruptedException, MalformedCookieException, IOException {
+        // 1. login as the test user
+        doFormsLogin();
+
+        // 2. wait for half the session timeout expiration duration
+        Thread.sleep((Duration.ofMinutes(1).toMillis() / 2) + 1); // NOSONAR
+
+        // 3. do another request to trigger the cookie refresh
+        HttpGet request = new HttpGet(whoamiUri());
+        try (CloseableHttpResponse response = httpClient.execute(request, httpContext)) {
+            assertEquals(HttpServletResponse.SC_OK, response.getStatusLine().getStatusCode());
+            Cookie parsedFormauthCookie = parseFormAuthCookieFromHeaders(response);
+            assertNotNull("Expected a refreshed formauth cookie in the response", parsedFormauthCookie);
+        }
+
+        // 4. do another request to verify that subsequent request after
+        //    the cookie refresh do not send an additional formauth cookie
+        try (CloseableHttpResponse response = httpClient.execute(request, httpContext)) {
+            assertEquals(HttpServletResponse.SC_OK, response.getStatusLine().getStatusCode());
+            Cookie parsedFormauthCookie2 = parseFormAuthCookieFromHeaders(response);
+            assertNull("Did not expect a formauth cookie in the response", parsedFormauthCookie2);
+        }
+    }
+
+    /**
+     * Calculate the base server URI from the current configuration of the
+     * httpservice
+     */
+    private URI getBaseServerUri() throws IOException, URISyntaxException {
+        assertNotNull(cm);
+        Configuration httpServiceConfiguration = cm.getConfiguration("org.apache.felix.http");
+        Dictionary<String, Object> properties = httpServiceConfiguration.getProperties();
+
+        String host;
+        Object hostObj = properties.get("org.apache.felix.http.host");
+        if (hostObj == null) {
+            host = "localhost";
+        } else {
+            assertTrue(hostObj instanceof String);
+            host = (String)hostObj;
+        }
+        assertNotNull(host);
+
+        String scheme = null;
+        Object portObj = null;
+        Object httpsEnableObj = properties.get("org.apache.felix.https.enable");
+        if ("true".equals(httpsEnableObj)) {
+            scheme = "https";
+            portObj = properties.get("org.osgi.service.http.port.secure");
+        } else {
+            Object httpEnableObj = properties.get("org.apache.felix.http.enable");
+            if (httpEnableObj == null || "true".equals(httpEnableObj)) {
+                scheme = "http";
+                portObj = properties.get("org.osgi.service.http.port");
+            } else {
+                fail("Expected either http or https to be enabled");
+            }
+        }
+        int port = -1;
+        if (portObj instanceof Number) {
+            port = ((Number)portObj).intValue();
+        }
+        assertTrue(port > 0);
+
+        return new URI(String.format("%s://%s:%d", scheme, host, port));
+    }
+
+    /**
+     * @return the address of the whoami script
+     */
+    private String whoamiUri() {
+        return String.format("%s/content.SLING10290IT.html", baseServerUri);
+    }
+
+    /**
+     * Perform the http calls to login the test user via the forms based login
+     */
+    private void doFormsLogin() throws MalformedCookieException, IOException {
+        // before login, there should be no formauth cookie in the cookie store
+        Cookie formauthCookie = getFormAuthCookieFromCookieStore();
+        assertNull("Did not expect formauth cookie in the cookie store", formauthCookie);
+
+        // verify that the script shows us as not logged in
+        HttpGet whoamiRequest = new HttpGet(whoamiUri());
+        try (CloseableHttpResponse whoamiResponse = httpClient.execute(whoamiRequest, httpContext)) {
+            assertEquals(HttpServletResponse.SC_OK, whoamiResponse.getStatusLine().getStatusCode());
+            String content = EntityUtils.toString(whoamiResponse.getEntity());
+            assertTrue(content.contains("whoAmI"));
+            assertTrue(content.contains("anonymous"));
+        }
+
+        // send the form login request
+        List<NameValuePair> parameters = new ArrayList<>();
+        parameters.add(new BasicNameValuePair("j_username", FORM_AUTH_VERIFY_USER));
+        parameters.add(new BasicNameValuePair("j_password", FORM_AUTH_VERIFY_PWD));
+        parameters.add(new BasicNameValuePair("_charset_", StandardCharsets.UTF_8.name()));
+        parameters.add(new BasicNameValuePair("resource", "/content.SLING10290IT.html"));
+        HttpPost request = new HttpPost(String.format("%s/j_security_check", baseServerUri));
+        request.setEntity(new UrlEncodedFormEntity(parameters));
+        Header locationHeader = null; 
+        try (CloseableHttpResponse response = httpClient.execute(request, httpContext)) {
+            assertEquals(HttpServletResponse.SC_MOVED_TEMPORARILY, response.getStatusLine().getStatusCode());
+            locationHeader = response.getFirstHeader("Location");
+
+            // verify that the expected set-cookie header arrived
+            Cookie parsedFormauthCookie = parseFormAuthCookieFromHeaders(response);
+            assertNotNull("Expected a formauth cookie in the response", parsedFormauthCookie);
+        }
+
+        // after login, there should be now be a cookie in the cookie store
+        Cookie formauthCookie2 = getFormAuthCookieFromCookieStore();
+        assertNotNull("Expected a formauth cookie in the cookie store", formauthCookie2);
+
+        // and then follow the redirect
+        assertNotNull("Expected a 'Location' header", locationHeader);
+        // verify that the script shows us logged in as the test user
+        HttpGet followedRequest = new HttpGet(locationHeader.getValue());
+        try (CloseableHttpResponse followedResponse = httpClient.execute(followedRequest, httpContext)) {
+            assertEquals(HttpServletResponse.SC_OK, followedResponse.getStatusLine().getStatusCode());
+            String content = EntityUtils.toString(followedResponse.getEntity());
+            assertTrue(content.contains("whoAmI"));
+            assertTrue(content.contains(FORM_AUTH_VERIFY_USER));
+
+            // there should be no new formauth cookie on the followed response
+            Cookie parsedFormauthCookie2 = parseFormAuthCookieFromHeaders(followedResponse);
+            assertNull("Did not expect a formauth cookie in the response", parsedFormauthCookie2);
+        }
+    }
+
+    /**
+     * Retrieve the formauth cookie from the cookie store
+     * 
+     * @return the formauth cookie or null if not found
+     */
+    private Cookie getFormAuthCookieFromCookieStore() {
+        Cookie formauthCookie = null;
+        List<Cookie> cookies = httpContext.getCookieStore().getCookies();
+        if (cookies != null) {
+            for (Cookie c : cookies) {
+                if (COOKIE_SLING_FORMAUTH.equals(c.getName())) {
+                    formauthCookie = c;
+                }
+            }
+        }
+        return formauthCookie;
+    }
+
+    /**
+     * Parse the formauth cookie out of the headers sent on the response
+     *
+     * @param response the response from the http request
+     * @return the found cookie or null if not found
+     */
+    private Cookie parseFormAuthCookieFromHeaders(HttpResponse response) throws MalformedCookieException {
+        Header [] cookieHeaders = response.getHeaders(HEADER_SET_COOKIE);
+        assertNotNull(cookieHeaders);
+
+        Cookie parsedFormauthCookie = null;
+        CookieSpec cookieSpec = new DefaultCookieSpec();
+        CookieOrigin origin = new CookieOrigin(baseServerUri.getHost(), baseServerUri.getPort(),
+                baseServerUri.getPath(), "https".equals(baseServerUri.getScheme()));
+        for (Header cookieHeader : cookieHeaders) {
+            List<Cookie> parsedCookies = cookieSpec.parse(cookieHeader, origin);
+            for (Cookie c : parsedCookies) {
+                if (COOKIE_SLING_FORMAUTH.equals(c.getName())) {
+                    if (parsedFormauthCookie != null) {
+                        fail(String.format("Did not expect more than one %s cookie", c.getName()));
+                    }
+                    parsedFormauthCookie = c;
+                }
+            }
+        }
+        return parsedFormauthCookie;
+    }
+
+}
diff --git a/src/test/resources/content/apps/sling/OrderedFolder/SLING10290IT.html b/src/test/resources/content/apps/sling/OrderedFolder/SLING10290IT.html
new file mode 100644
index 0000000..1d0402d
--- /dev/null
+++ b/src/test/resources/content/apps/sling/OrderedFolder/SLING10290IT.html
@@ -0,0 +1,26 @@
+<!--/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+*/--><!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>Who Am I?</title>
+</head>
+<body>
+  <p> whoAmI: ${request.remoteUser == null ? "anonymous" : request.remoteUser}</p>
+</body>
+</html>
\ No newline at end of file