diff --git a/focused/security/pom.xml b/focused/security/pom.xml
index 89048bd0..d5892223 100644
--- a/focused/security/pom.xml
+++ b/focused/security/pom.xml
@@ -36,6 +36,7 @@
restBasicAuthCustomStoreHandler
restFormAuthCustomStore
restCustomAuthCustomStore
+ restFormAuthCustomStoreRememberMe
basicAuth
customFormWithJsf
filter
diff --git a/focused/security/restFormAuthCustomStoreRememberMe/README.md b/focused/security/restFormAuthCustomStoreRememberMe/README.md
new file mode 100644
index 00000000..d6c243fb
--- /dev/null
+++ b/focused/security/restFormAuthCustomStoreRememberMe/README.md
@@ -0,0 +1,4 @@
+# A RESTful form authentication with custom identity store and remember-me example
+
+This example demonstrates how to use Jakarta Security to secure a REST endpoint with form authentication
+a custom (user provided) identity store, and remember-me.
\ No newline at end of file
diff --git a/focused/security/restFormAuthCustomStoreRememberMe/pom.xml b/focused/security/restFormAuthCustomStoreRememberMe/pom.xml
new file mode 100644
index 00000000..1b1025ae
--- /dev/null
+++ b/focused/security/restFormAuthCustomStoreRememberMe/pom.xml
@@ -0,0 +1,37 @@
+
+
+
+ 4.0.0
+
+
+ jakarta.examples.focused.eesecurity
+ project
+ 10-SNAPSHOT
+
+
+ restFormAuthCustomStoreRememberMe
+ war
+
+ A Jakarta Security RESTful form authentication with custom identity store example and remember-me.
+
+
+
+ jakarta.platform
+ jakarta.jakartaee-web-api
+ provided
+
+
+
diff --git a/focused/security/restFormAuthCustomStoreRememberMe/src/main/java/jakartaee/examples/focused/security/restformauthcustomatorerememberme/ApplicationConfig.java b/focused/security/restFormAuthCustomStoreRememberMe/src/main/java/jakartaee/examples/focused/security/restformauthcustomatorerememberme/ApplicationConfig.java
new file mode 100644
index 00000000..317bac58
--- /dev/null
+++ b/focused/security/restFormAuthCustomStoreRememberMe/src/main/java/jakartaee/examples/focused/security/restformauthcustomatorerememberme/ApplicationConfig.java
@@ -0,0 +1,42 @@
+/*
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR(S) DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
+ * RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+ * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE
+ * USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+package jakartaee.examples.focused.security.restformauthcustomatorerememberme;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension;
+import jakarta.enterprise.inject.build.compatible.spi.ClassConfig;
+import jakarta.enterprise.inject.build.compatible.spi.Enhancement;
+import jakarta.security.enterprise.authentication.mechanism.http.FormAuthenticationMechanismDefinition;
+import jakarta.security.enterprise.authentication.mechanism.http.HttpAuthenticationMechanism;
+import jakarta.security.enterprise.authentication.mechanism.http.LoginToContinue;
+import jakarta.security.enterprise.authentication.mechanism.http.RememberMe;
+import jakarta.ws.rs.ApplicationPath;
+import jakarta.ws.rs.core.Application;
+
+@ApplicationScoped
+@FormAuthenticationMechanismDefinition(
+ loginToContinue = @LoginToContinue(
+ loginPage="/login.html",
+ errorPage="/login-error.html"
+ )
+)
+@ApplicationPath("/rest")
+public class ApplicationConfig extends Application implements BuildCompatibleExtension {
+
+ @Enhancement(types = HttpAuthenticationMechanism.class, withSubtypes = true)
+ public void addRememberMe(ClassConfig httpAuthenticationMechanism) {
+ httpAuthenticationMechanism.addAnnotation(
+ RememberMe.Literal.INSTANCE);
+ }
+
+}
diff --git a/focused/security/restFormAuthCustomStoreRememberMe/src/main/java/jakartaee/examples/focused/security/restformauthcustomatorerememberme/CustomIdentityStore.java b/focused/security/restFormAuthCustomStoreRememberMe/src/main/java/jakartaee/examples/focused/security/restformauthcustomatorerememberme/CustomIdentityStore.java
new file mode 100644
index 00000000..8e763252
--- /dev/null
+++ b/focused/security/restFormAuthCustomStoreRememberMe/src/main/java/jakartaee/examples/focused/security/restformauthcustomatorerememberme/CustomIdentityStore.java
@@ -0,0 +1,44 @@
+/*
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR(S) DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
+ * RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+ * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE
+ * USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+package jakartaee.examples.focused.security.restformauthcustomatorerememberme;
+
+import static jakarta.security.enterprise.identitystore.CredentialValidationResult.INVALID_RESULT;
+
+import java.util.Set;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.security.enterprise.credential.UsernamePasswordCredential;
+import jakarta.security.enterprise.identitystore.CredentialValidationResult;
+import jakarta.security.enterprise.identitystore.IdentityStore;
+
+/**
+ * A custom identity store that will be picked up automatically by Jakarta Security.
+ *
+ *
+ * Jakarta Security picks up any enabled CDI bean that implements IdentityStore
.
+ *
+ * @author Arjan Tijms
+ *
+ */
+@ApplicationScoped
+public class CustomIdentityStore implements IdentityStore {
+
+ public CredentialValidationResult validate(UsernamePasswordCredential usernamePasswordCredential) {
+ if (usernamePasswordCredential.compareTo("john", "secret1")) {
+ return new CredentialValidationResult("john", Set.of("user", "caller"));
+ }
+
+ return INVALID_RESULT;
+ }
+
+}
diff --git a/focused/security/restFormAuthCustomStoreRememberMe/src/main/java/jakartaee/examples/focused/security/restformauthcustomatorerememberme/CustomRememberMeIdentityStore.java b/focused/security/restFormAuthCustomStoreRememberMe/src/main/java/jakartaee/examples/focused/security/restformauthcustomatorerememberme/CustomRememberMeIdentityStore.java
new file mode 100644
index 00000000..7dd7d64e
--- /dev/null
+++ b/focused/security/restFormAuthCustomStoreRememberMe/src/main/java/jakartaee/examples/focused/security/restformauthcustomatorerememberme/CustomRememberMeIdentityStore.java
@@ -0,0 +1,67 @@
+/*
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR(S) DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
+ * RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+ * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE
+ * USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+package jakartaee.examples.focused.security.restformauthcustomatorerememberme;
+
+
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.security.enterprise.CallerPrincipal;
+import jakarta.security.enterprise.credential.RememberMeCredential;
+import jakarta.security.enterprise.identitystore.CredentialValidationResult;
+import jakarta.security.enterprise.identitystore.RememberMeIdentityStore;
+
+import static jakarta.security.enterprise.identitystore.CredentialValidationResult.INVALID_RESULT;
+
+/**
+ * A custom remember-me identity store that will be picked up automatically by Jakarta Security.
+ *
+ *
+ * Jakarta Security picks up any enabled CDI bean that implements RememberMeIdentityStore
.
+ *
+ * @author Arjan Tijms
+ *
+ */
+@ApplicationScoped
+public class CustomRememberMeIdentityStore implements RememberMeIdentityStore {
+
+ private final Map tokenToIdentityMap = new ConcurrentHashMap<>();
+
+ @Override
+ public CredentialValidationResult validate(RememberMeCredential credential) {
+ if (tokenToIdentityMap.containsKey(credential.getToken())) {
+ return tokenToIdentityMap.get(credential.getToken());
+ }
+
+ return INVALID_RESULT;
+ }
+
+ @Override
+ public String generateLoginToken(CallerPrincipal callerPrincipal, Set groups) {
+ var token = UUID.randomUUID().toString();
+
+ tokenToIdentityMap.put(token, new CredentialValidationResult(callerPrincipal, groups));
+
+ return token;
+ }
+
+ @Override
+ public void removeLoginToken(String token) {
+ tokenToIdentityMap.remove(token);
+ }
+
+}
diff --git a/focused/security/restFormAuthCustomStoreRememberMe/src/main/java/jakartaee/examples/focused/security/restformauthcustomatorerememberme/Resource.java b/focused/security/restFormAuthCustomStoreRememberMe/src/main/java/jakartaee/examples/focused/security/restformauthcustomatorerememberme/Resource.java
new file mode 100644
index 00000000..70885e5c
--- /dev/null
+++ b/focused/security/restFormAuthCustomStoreRememberMe/src/main/java/jakartaee/examples/focused/security/restformauthcustomatorerememberme/Resource.java
@@ -0,0 +1,39 @@
+/*
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR(S) DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
+ * RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+ * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE
+ * USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+package jakartaee.examples.focused.security.restformauthcustomatorerememberme;
+
+import static jakarta.ws.rs.core.MediaType.TEXT_PLAIN;
+
+import jakarta.enterprise.context.RequestScoped;
+import jakarta.inject.Inject;
+import jakarta.security.enterprise.SecurityContext;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+
+@Path("/resource")
+@RequestScoped
+public class Resource {
+
+ @Inject
+ private SecurityContext securityContext;
+
+ @GET
+ @Produces(TEXT_PLAIN)
+ public String getCallerAndRole() {
+ return
+ securityContext.getCallerPrincipal().getName() + " : " +
+ securityContext.isCallerInRole("user");
+ }
+
+}
diff --git a/focused/security/restFormAuthCustomStoreRememberMe/src/main/resources/META-INF/services/jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension b/focused/security/restFormAuthCustomStoreRememberMe/src/main/resources/META-INF/services/jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension
new file mode 100644
index 00000000..4f3b120c
--- /dev/null
+++ b/focused/security/restFormAuthCustomStoreRememberMe/src/main/resources/META-INF/services/jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension
@@ -0,0 +1 @@
+jakartaee.examples.focused.security.restformauthcustomatorerememberme.ApplicationConfig
\ No newline at end of file
diff --git a/focused/security/restFormAuthCustomStoreRememberMe/src/main/webapp/WEB-INF/beans.xml b/focused/security/restFormAuthCustomStoreRememberMe/src/main/webapp/WEB-INF/beans.xml
new file mode 100644
index 00000000..7830aa4a
--- /dev/null
+++ b/focused/security/restFormAuthCustomStoreRememberMe/src/main/webapp/WEB-INF/beans.xml
@@ -0,0 +1,21 @@
+
+
+
+
\ No newline at end of file
diff --git a/focused/security/restFormAuthCustomStoreRememberMe/src/main/webapp/WEB-INF/web.xml b/focused/security/restFormAuthCustomStoreRememberMe/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000..fc80f463
--- /dev/null
+++ b/focused/security/restFormAuthCustomStoreRememberMe/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+ protected
+ /rest/*
+
+
+ user
+
+
+
+ user
+
+
+
diff --git a/focused/security/restFormAuthCustomStoreRememberMe/src/main/webapp/login-error.html b/focused/security/restFormAuthCustomStoreRememberMe/src/main/webapp/login-error.html
new file mode 100644
index 00000000..c6751e6f
--- /dev/null
+++ b/focused/security/restFormAuthCustomStoreRememberMe/src/main/webapp/login-error.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ Failure
+
+
+ Login failed!
+ Try again
+
+
\ No newline at end of file
diff --git a/focused/security/restFormAuthCustomStoreRememberMe/src/main/webapp/login.html b/focused/security/restFormAuthCustomStoreRememberMe/src/main/webapp/login.html
new file mode 100644
index 00000000..14633363
--- /dev/null
+++ b/focused/security/restFormAuthCustomStoreRememberMe/src/main/webapp/login.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+ Login
+
+
+ Login to continue
+
+
+
+
+
diff --git a/focused/security/restFormAuthCustomStoreRememberMe/src/test/java/jakartaee/examples/focused/security/restformauthcustomatorerememberme/RestFormAuthCustomStoreRememberMeIT.java b/focused/security/restFormAuthCustomStoreRememberMe/src/test/java/jakartaee/examples/focused/security/restformauthcustomatorerememberme/RestFormAuthCustomStoreRememberMeIT.java
new file mode 100644
index 00000000..6ee37a31
--- /dev/null
+++ b/focused/security/restFormAuthCustomStoreRememberMe/src/test/java/jakartaee/examples/focused/security/restformauthcustomatorerememberme/RestFormAuthCustomStoreRememberMeIT.java
@@ -0,0 +1,83 @@
+/*
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR(S) DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
+ * RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+ * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE
+ * USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+package jakartaee.examples.focused.security.restformauthcustomatorerememberme;
+
+import java.net.URL;
+
+import org.jboss.arquillian.container.test.api.RunAsClient;
+import org.jboss.arquillian.junit.Arquillian;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import com.gargoylesoftware.htmlunit.TextPage;
+import com.gargoylesoftware.htmlunit.html.HtmlForm;
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import com.gargoylesoftware.htmlunit.util.Cookie;
+
+import jakartaee.examples.utils.ITBase;
+
+
+/**
+ * The integration test for the REST with form authentication, custom store and remember-me example
+ *
+ */
+@RunWith(Arquillian.class)
+@RunAsClient
+public class RestFormAuthCustomStoreRememberMeIT extends ITBase {
+
+ @ArquillianResource
+ private URL baseUrl;
+
+ /**
+ * Test the call to a protected REST service
+ *
+ * @throws Exception when a serious error occurs.
+ */
+ @RunAsClient
+ @Test
+ public void testRestCall() throws Exception {
+ // Initial request
+ HtmlPage loginPage = webClient.getPage(baseUrl + "/rest/resource");
+ System.out.println(loginPage.asXml());
+
+ // Response is login form, so we can authenticate
+ HtmlForm form = loginPage.getForms()
+ .get(0);
+
+ form.getInputByName("j_username")
+ .setValueAttribute("john");
+
+ form.getInputByName("j_password")
+ .setValueAttribute("secret1");
+
+ // After logging in, we should get the actual resource response
+ TextPage page = form.getInputByValue("Submit")
+ .click();
+
+ System.out.println(page.getContent());
+
+ // Remove all cookies (specially the JSESSONID), except for the
+ // JREMEMBERMEID cookie which carries the token to login again
+ for (Cookie cookie : webClient.getCookieManager().getCookies()) {
+ if (!"JREMEMBERMEID".equals(cookie.getName())) {
+ webClient.getCookieManager().removeCookie(cookie);
+ }
+ }
+
+ // Should get the resource response, and not the login form
+ TextPage pageAgain = webClient.getPage(baseUrl + "/rest/resource");
+
+ System.out.println(pageAgain.getContent());
+ }
+}