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