You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tomee.apache.org by jl...@apache.org on 2021/06/24 13:01:19 UTC

[tomee] branch master updated: TOMEE-3761 Example for Jakarta Security with a custom IdentityStore

This is an automated email from the ASF dual-hosted git repository.

jlmonteiro pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tomee.git


The following commit(s) were added to refs/heads/master by this push:
     new 9dcad5a  TOMEE-3761 Example for Jakarta Security with a custom IdentityStore
9dcad5a is described below

commit 9dcad5a1bac2ed2115c81e86e38e25498c1c1a2a
Author: Jean-Louis Monteiro <jl...@tomitribe.com>
AuthorDate: Thu Jun 24 15:00:56 2021 +0200

    TOMEE-3761 Example for Jakarta Security with a custom IdentityStore
---
 examples/pom.xml                                   |   1 +
 examples/security-custom-identitystore/README.adoc | 142 +++++++++++++++++++++
 examples/security-custom-identitystore/pom.xml     |  82 ++++++++++++
 .../java/org/superbiz/security/MovieServlet.java   |  74 +++++++++++
 .../org/superbiz/security/TestIdentityStore.java   |  51 ++++++++
 .../src/main/resources/META-INF/beans.xml          |  18 +++
 .../test/java/org/superbiz/security/MovieTest.java | 110 ++++++++++++++++
 7 files changed, 478 insertions(+)

diff --git a/examples/pom.xml b/examples/pom.xml
index 6c7b962..4c21424 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -219,6 +219,7 @@
 
     <!-- Jakarta Security Examples -->
     <module>security-tomcat-user-identitystore</module>
+    <module>security-custom-identitystore</module>
 
   </modules>
   <dependencies>
diff --git a/examples/security-custom-identitystore/README.adoc b/examples/security-custom-identitystore/README.adoc
new file mode 100644
index 0000000..eb0dfe2
--- /dev/null
+++ b/examples/security-custom-identitystore/README.adoc
@@ -0,0 +1,142 @@
+:index-group: Jakarta Security
+:jbake-type: page
+:jbake-status: status=published
+= Jakarta Security with a custom identity store
+
+TomEE has its own independent Jakarta Security implementation https://eclipse-ee4j.github.io/security-api/ .
+
+[NOTE]
+====
+Jakarta Security defines a standard for creating secure Jakarta EE applications in modern application paradigms. It defines an overarching (end-user targeted) Security API for Jakarta EE Applications.
+
+Jakarta Security builds on the lower level Security SPIs defined by Jakarta Authentication and Jakarta Authorization, which are both not end-end targeted.
+====
+
+As the specification requires, TomEE supports by default JDBC and JDAP identity stores. It also has a default support for Tomcat's 'tomcat-users.xml' (See security-tomcat-user-identitystore example).
+
+This example will show how you can leverage your own identity store to authenticate users.
+This is very often required for integrating your systems.
+
+== Implement a simple servlet
+
+This movie servlet, is a very simple example that defines a BasicAuthenticationMechanism, some roles and security constraints.
+
+[source,java]
+----
+@WebServlet("/movies")
+@DeclareRoles({"foo","bar","kaz"})
+@ServletSecurity(@HttpConstraint(rolesAllowed = "foo"))
+@BasicAuthenticationMechanismDefinition
+public class MovieServlet extends HttpServlet {
+
+    private static final long serialVersionUID = 1L;
+
+    @Override
+    public void doGet(final HttpServletRequest request, final HttpServletResponse response)
+        throws ServletException, IOException {
+
+        String webName = null;
+        if (request.getUserPrincipal() != null) {
+            webName = request.getUserPrincipal().getName();
+        }
+
+        response.getWriter().write(
+            "<html><body> Welcome to Movie servlet <br><br>\n" +
+
+            "web username: " + webName + "<br><br>\n" +
+
+            "web user has role \"foo\": " + request.isUserInRole("foo") + "<br>\n" +
+            "web user has role \"bar\": " + request.isUserInRole("bar") + "<br>\n" +
+            "web user has role \"kaz\": " + request.isUserInRole("kaz") + "<br><br>");
+    }
+
+}
+----
+
+IMPORTANT:
+====
+In TomEE, Jakarta Security is wired in all layers, you can use
+
+* `javax.ws.rs.core.SecurityContext#getUserPrincipal` and `isUserInRole` to get the User Principal and check if the user has a given role
+* `javax.security.enterprise.SecurityContext#getCallerPrincipal` and `isCallerInRole` to get the Caller Principal (notice the difference in terms of naming) and check if a caller has a given role
+* `javax.servlet.http.HttpServletRequest#getUserPrincipal` and `isUserInRole`
+* `javax.ejb.SessionContext#getCallerPrincipal` and `isCallerInRole`
+* the `Subject` from the `PolicyContext` but this is less used
+====
+
+A lot of different APIs to retrieve the principal and check whereas it has a given role.
+It's all wired in and consistent in TomEE. No special configuration is needed.
+
+== Create your own IdentityStore implementation
+
+For the sake of keeping this example as simple as possible, the `TestIdentityStore` is very simple.
+
+It recognizes only 2 users and only one of them has the right roles to call the servlet.
+
+[source,java]
+----
+@ApplicationScoped
+public class TestIdentityStore implements IdentityStore {
+
+    public CredentialValidationResult validate(Credential credential) {
+
+        if (!(credential instanceof UsernamePasswordCredential)) {
+            return INVALID_RESULT;
+        }
+
+        final UsernamePasswordCredential usernamePasswordCredential = (UsernamePasswordCredential) credential;
+        if (usernamePasswordCredential.compareTo("jon", "doe")) {
+            return new CredentialValidationResult("jon", new HashSet<>(asList("foo", "bar")));
+        }
+
+        if (usernamePasswordCredential.compareTo("iron", "man")) {
+            return new CredentialValidationResult("iron", new HashSet<>(Collections.singletonList("avengers")));
+        }
+
+        return INVALID_RESULT;
+    }
+
+}
+----
+
+There is nothing else to configure or do.
+The identity store must implement the IdentityStore interface.
+It must be a CDI bean and then TomEE will pick it up automatically and delegate user authentication.
+
+== Running
+
+Were we to run the above Main class or Test Case we'd see output like the following:
+
+[source,bash]
+----
+....
+INFOS: Starting ProtocolHandler ["http-nio-54313"]
+juin 24, 2021 2:58:42 PM sun.reflect.DelegatingMethodAccessorImpl invoke
+INFOS: Server startup in [4703] milliseconds
+juin 24, 2021 2:58:42 PM sun.reflect.DelegatingMethodAccessorImpl invoke
+INFOS: Full bootstrap in [7638] milliseconds
+
+
+Calling MovieServlet without any credentials provided.
+juin 24, 2021 2:58:43 PM com.gargoylesoftware.htmlunit.WebClient printContentIfNecessary
+INFOS: statusCode=[401] contentType=[text/html]
+juin 24, 2021 2:58:43 PM com.gargoylesoftware.htmlunit.WebClient printContentIfNecessary
+INFOS: <!doctype html><html lang="en"><head><title>HTTP Status 401 – Unauthorized</title><style type="text/css">body {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>HTTP Status 401 – Unauthorized</h1><hr class="line" /><p><b>Type</b> Status Report</p><p><b>Descripti [...]
+
+
+Calling MovieServlet with a valid user and valid permissions.
+
+
+Calling MovieServlet with the wrong credentials.
+juin 24, 2021 2:58:44 PM com.gargoylesoftware.htmlunit.WebClient printContentIfNecessary
+INFOS: statusCode=[401] contentType=[text/html]
+juin 24, 2021 2:58:44 PM com.gargoylesoftware.htmlunit.WebClient printContentIfNecessary
+INFOS: <!doctype html><html lang="en"><head><title>HTTP Status 401 – Unauthorized</title><style type="text/css">body {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>HTTP Status 401 – Unauthorized</h1><hr class="line" /><p><b>Type</b> Status Report</p><p><b>Descripti [...]
+
+
+Calling MovieServlet with a valid user but without required permissions.
+juin 24, 2021 2:58:44 PM com.gargoylesoftware.htmlunit.WebClient printContentIfNecessary
+INFOS: statusCode=[403] contentType=[text/html]
+juin 24, 2021 2:58:44 PM com.gargoylesoftware.htmlunit.WebClient printContentIfNecessary
+INFOS: <!doctype html><html lang="en"><head><title>HTTP Status 403 – Forbidden</title><style type="text/css">body {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>HTTP Status 403 – Forbidden</h1><hr class="line" /><p><b>Type</b> Status Report</p><p><b>Message</b> Acc [...]
+----
diff --git a/examples/security-custom-identitystore/pom.xml b/examples/security-custom-identitystore/pom.xml
new file mode 100644
index 0000000..bcd061c
--- /dev/null
+++ b/examples/security-custom-identitystore/pom.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>org.superbiz</groupId>
+  <artifactId>security-custom-identitystore</artifactId>
+  <version>8.0.8-SNAPSHOT</version>
+
+  <name>TomEE :: Examples :: Jakarta Security custom Identity Store</name>
+  
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <version.tomee>8.0.8-SNAPSHOT</version.tomee>
+  </properties>
+  <dependencies>
+    
+    <dependency>
+      <groupId>org.apache.tomee.bom</groupId>
+      <artifactId>tomee-microprofile</artifactId>
+      <version>${version.tomee}</version>
+    </dependency>
+    
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.13.1</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>net.sourceforge.htmlunit</groupId>
+      <artifactId>htmlunit</artifactId>
+      <version>2.50.0</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>3.5.1</version>
+        <configuration>
+          <source>1.8</source>
+          <target>1.8</target>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+  <!--
+  This section allows you to configure where to publish libraries for sharing.
+  It is not required and may be deleted.  For more information see:
+  http://maven.apache.org/plugins/maven-deploy-plugin/
+  -->
+  <distributionManagement>
+    <repository>
+      <id>localhost</id>
+      <url>file://${basedir}/target/repo/</url>
+    </repository>
+    <snapshotRepository>
+      <id>localhost</id>
+      <url>file://${basedir}/target/snapshot-repo/</url>
+    </snapshotRepository>
+  </distributionManagement>
+</project>
diff --git a/examples/security-custom-identitystore/src/main/java/org/superbiz/security/MovieServlet.java b/examples/security-custom-identitystore/src/main/java/org/superbiz/security/MovieServlet.java
new file mode 100644
index 0000000..4aebe9d
--- /dev/null
+++ b/examples/security-custom-identitystore/src/main/java/org/superbiz/security/MovieServlet.java
@@ -0,0 +1,74 @@
+/*
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.superbiz.security;
+
+import javax.annotation.security.DeclareRoles;
+import javax.security.enterprise.authentication.mechanism.http.BasicAuthenticationMechanismDefinition;
+import javax.servlet.ServletException;
+import javax.servlet.annotation.HttpConstraint;
+import javax.servlet.annotation.ServletSecurity;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+@WebServlet("/movies")
+@DeclareRoles({"foo","bar","kaz"})
+@ServletSecurity(@HttpConstraint(rolesAllowed = "foo"))
+@BasicAuthenticationMechanismDefinition
+public class MovieServlet extends HttpServlet {
+
+    private static final long serialVersionUID = 1L;
+
+    @Override
+    public void doGet(final HttpServletRequest request, final HttpServletResponse response)
+        throws ServletException, IOException {
+
+        String webName = null;
+        if (request.getUserPrincipal() != null) {
+            webName = request.getUserPrincipal().getName();
+        }
+
+        response.getWriter().write(
+            "<html><body> Welcome to Movie servlet <br><br>\n" +
+
+            "web username: " + webName + "<br><br>\n" +
+
+            "web user has role \"foo\": " + request.isUserInRole("foo") + "<br>\n" +
+            "web user has role \"bar\": " + request.isUserInRole("bar") + "<br>\n" +
+            "web user has role \"kaz\": " + request.isUserInRole("kaz") + "<br><br>\n" +
+
+
+            "<form method=\"POST\">" +
+            "<input type=\"hidden\" name=\"logout\" value=\"true\"  >" +
+            "<input type=\"submit\" value=\"Logout\">" +
+            "</form>" +
+            "</body></html>");
+    }
+
+    @Override
+    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+        if ("true".equals(request.getParameter("logout"))) {
+            request.logout();
+            request.getSession().invalidate();
+        }
+
+        doGet(request, response);
+    }
+
+}
\ No newline at end of file
diff --git a/examples/security-custom-identitystore/src/main/java/org/superbiz/security/TestIdentityStore.java b/examples/security-custom-identitystore/src/main/java/org/superbiz/security/TestIdentityStore.java
new file mode 100644
index 0000000..0fd8bbd
--- /dev/null
+++ b/examples/security-custom-identitystore/src/main/java/org/superbiz/security/TestIdentityStore.java
@@ -0,0 +1,51 @@
+/*
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.superbiz.security;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.security.enterprise.credential.Credential;
+import javax.security.enterprise.credential.UsernamePasswordCredential;
+import javax.security.enterprise.identitystore.CredentialValidationResult;
+import javax.security.enterprise.identitystore.IdentityStore;
+import java.util.Collections;
+import java.util.HashSet;
+
+import static java.util.Arrays.asList;
+import static javax.security.enterprise.identitystore.CredentialValidationResult.INVALID_RESULT;
+
+@ApplicationScoped
+public class TestIdentityStore implements IdentityStore {
+
+    public CredentialValidationResult validate(Credential credential) {
+
+        if (!(credential instanceof UsernamePasswordCredential)) {
+            return INVALID_RESULT;
+        }
+
+        final UsernamePasswordCredential usernamePasswordCredential = (UsernamePasswordCredential) credential;
+        if (usernamePasswordCredential.compareTo("jon", "doe")) {
+            return new CredentialValidationResult("jon", new HashSet<>(asList("foo", "bar")));
+        }
+
+        if (usernamePasswordCredential.compareTo("iron", "man")) {
+            return new CredentialValidationResult("iron", new HashSet<>(Collections.singletonList("avengers")));
+        }
+
+        return INVALID_RESULT;
+    }
+
+}
\ No newline at end of file
diff --git a/examples/security-custom-identitystore/src/main/resources/META-INF/beans.xml b/examples/security-custom-identitystore/src/main/resources/META-INF/beans.xml
new file mode 100644
index 0000000..b240367
--- /dev/null
+++ b/examples/security-custom-identitystore/src/main/resources/META-INF/beans.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<beans/>
diff --git a/examples/security-custom-identitystore/src/test/java/org/superbiz/security/MovieTest.java b/examples/security-custom-identitystore/src/test/java/org/superbiz/security/MovieTest.java
new file mode 100644
index 0000000..5d79b88
--- /dev/null
+++ b/examples/security-custom-identitystore/src/test/java/org/superbiz/security/MovieTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.superbiz.security;
+
+import com.gargoylesoftware.htmlunit.DefaultCredentialsProvider;
+import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
+import com.gargoylesoftware.htmlunit.WebClient;
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import org.apache.tomee.bootstrap.Archive;
+import org.apache.tomee.bootstrap.Server;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.net.URI;
+
+public class MovieTest {
+
+    private static URI serverURI;
+
+    @BeforeClass
+    public static void setup() {
+        // Add any classes you need to an Archive
+        // or add them to a jar via any means
+        final Archive classes = Archive.archive()
+                                       .add(MovieServlet.class)
+                                       .add(TestIdentityStore.class);
+
+        // Place the classes where you would want
+        // them in a Tomcat install
+        final Server server = Server.builder()
+                                    // This effectively creates a webapp called ROOT
+                                    .add("webapps/ROOT/WEB-INF/classes", classes)
+                                    .add("webapps/ROOT/WEB-INF/beans.xml", "")
+                                    .build();
+
+        serverURI = server.getURI();
+    }
+
+    @Test
+    public void getWithoutAuthentication() throws Exception {
+        System.out.println("\n\nCalling MovieServlet without any credentials provided.");
+        try (final WebClient webClient = new WebClient()) {
+            webClient.getPage(serverURI.toString() + "/movies");
+
+        } catch (final FailingHttpStatusCodeException e) {
+            Assert.assertEquals(401, e.getStatusCode());
+        }
+    }
+
+    @Test
+    public void getWrongUser() throws Exception {
+        System.out.println("\n\nCalling MovieServlet with the wrong credentials.");
+        try (final WebClient webClient = new WebClient()) {
+            //set proxy username and password
+            final DefaultCredentialsProvider credentialsProvider = (DefaultCredentialsProvider) webClient.getCredentialsProvider();
+            credentialsProvider.addCredentials("username", "password");
+
+            webClient.getPage(serverURI.toString() + "/movies");
+
+        } catch (final FailingHttpStatusCodeException e) {
+            Assert.assertEquals(401, e.getStatusCode());
+        }
+    }
+
+    @Test
+    public void getWrongPermission() throws Exception {
+        System.out.println("\n\nCalling MovieServlet with a valid user but without required permissions.");
+        try (final WebClient webClient = new WebClient()) {
+
+            //set proxy username and password
+            final DefaultCredentialsProvider credentialsProvider = (DefaultCredentialsProvider) webClient.getCredentialsProvider();
+            credentialsProvider.addCredentials("iron", "man");
+
+            webClient.getPage(serverURI.toString() + "/movies");
+
+        } catch (final FailingHttpStatusCodeException e) {
+            Assert.assertEquals(403, e.getStatusCode());
+        }
+    }
+
+    @Test
+    public void getRightPermissions() throws Exception {
+        System.out.println("\n\nCalling MovieServlet with a valid user and valid permissions.");
+        try (final WebClient webClient = new WebClient()) {
+            //set proxy username and password
+            final DefaultCredentialsProvider credentialsProvider = (DefaultCredentialsProvider) webClient.getCredentialsProvider();
+            credentialsProvider.addCredentials("jon", "doe");
+
+            // should not throw any exception now
+            final HtmlPage htmlPage = webClient.getPage(serverURI.toString() + "/movies");
+            Assert.assertTrue(htmlPage.asNormalizedText().contains("web username: jon"));
+        }
+    }
+
+}