diff --git a/examples/mp-rest-jwt-principal/README.adoc b/examples/mp-rest-jwt-principal/README.adoc new file mode 100644 index 00000000000..5fc3dbdb489 --- /dev/null +++ b/examples/mp-rest-jwt-principal/README.adoc @@ -0,0 +1,103 @@ += MicroProfile JWT Principal +:index-group: MicroProfile +:jbake-type: page +:jbake-status: published + +This is an example on how to use MicroProfile JWT in TomEE by accessing +Principal from the JsonWebToken. + +== Run the application: + +[source, bash] +---- +mvn clean install tomee:run +---- + +This example is a CRUD application for orders in store. + +== Requirments and configuration + +For usage of MicroProfile JWT we have to change the following to our +project: + +[arabic] +. Add the dependency to our `pom.xml` file: ++ +.... + + org.eclipse.microprofile.jwt + microprofile-jwt-auth-api + ${mp-jwt.version} + provided + +.... +. Annotate our `Application.class` with `@LoginConfig(authMethod = "MP-JWT")` + +. Provide public and private key for authentication. And specify the location of the public key and the issuer in our +`microprofile-config.properties` file. ++ +[source,properties] +---- +mp.jwt.verify.publickey.location=/publicKey.pem +mp.jwt.verify.issuer=https://example.com +---- + +. Define `@RolesAllowed()` on the endpoints we want to protect. + +== Obtaining the JWT Principal + +We obtain the `Principal` in the MicroProfile class `org.eclipse.microprofile.jwt.JsonWebToken`. From there +we can acquire username and groups of the user that is accessing the endpoint. + +[source,java] +---- +@Inject +private JsonWebToken jwtPrincipal; +---- + +== About the application architecture + +The application enables us to manipulate orders with specific users. We have two users `Alice Wonder` +and `John Doe`. They can read, create, edit and delete specific entries. And for each creation +we save the user who created the order. In case a user edits the entry we record that by accessing +the `Principal` who has sent the request to our backend. + +`alice-wonder-jwt.json` + +[source,json] +---- +{ + "iss": "https://example.com", + "upn": "alice", + "sub": "alice.wonder@example.com", + "name": "Alice Wonder", + "iat": 1516239022, + "groups": [ + "buyer" + ] +} +---- + +`john-doe-jwt.json` +[source,json] +---- +{ + "iss": "https://example.com", + "upn": "john", + "sub": "john.doe@example.com", + "name": "John Doe", + "iat": 1516239022, + "groups": [ + "merchant" + ] +} +---- + +== Access the endpoints with JWT token + +We access endpoints from our test class by creating a `JWT` with the help of +our `TokenUtils.generateJWTString(String jsonResource)` which signs our user +data in json format with the help of our `src/test/resources/privateKey.pem` key. + +We can also generate new `privateKey.pem` and `publicKey.pem` with the +`GenerateKeyUtils.generateKeyPair(String keyAlgorithm, int keySize)` method. \ No newline at end of file diff --git a/examples/mp-rest-jwt-principal/pom.xml b/examples/mp-rest-jwt-principal/pom.xml new file mode 100644 index 00000000000..cf9bb05ed80 --- /dev/null +++ b/examples/mp-rest-jwt-principal/pom.xml @@ -0,0 +1,184 @@ + + + + 4.0.0 + + org.superbiz + mp-rest-jwt-principal + 8.0.0-SNAPSHOT + war + OpenEJB :: Examples :: MP REST JWT PRINCIPAL + + + UTF-8 + 8.0.0-SNAPSHOT + 4.23 + 1.4.1.Final + 1.1.1 + 1.0 + 1.1 + + + + install + phonestore + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.18.1 + + false + + + + org.apache.maven.plugins + maven-war-plugin + 3.1.0 + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + 1.8 + 1.8 + + + + org.apache.tomee.maven + tomee-maven-plugin + ${tomee.version} + + microprofile + -Xmx512m -XX:PermSize=256m + ${project.basedir}/src/main/tomee/ + + + + + + + + + + org.jboss.arquillian + arquillian-bom + ${arquillian-bom.version} + import + pom + + + + + + + + org.apache.tomee + javaee-api + 8.0 + provided + + + org.eclipse.microprofile.jwt + microprofile-jwt-auth-api + ${mp-jwt.version} + provided + + + org.eclipse.microprofile.rest.client + microprofile-rest-client-api + ${mp-rest-client.version} + provided + + + org.eclipse.microprofile.config + microprofile-config-api + ${mp-config.version} + provided + + + com.nimbusds + nimbus-jose-jwt + ${junit.version} + test + + + + junit + junit + 4.12 + test + + + org.jboss.arquillian.junit + arquillian-junit-container + test + + + + + + arquillian-tomee-remote + + true + + + + org.apache.tomee + arquillian-tomee-remote + ${tomee.version} + test + + + org.apache.tomee + apache-tomee + ${tomee.version} + zip + microprofile + test + + + org.apache.tomee + mp-jwt + ${tomee.version} + provided + + + + + + + + + localhost + file://${basedir}/target/repo/ + + + localhost + file://${basedir}/target/snapshot-repo/ + + + diff --git a/examples/mp-rest-jwt-principal/src/main/java/org/superbiz/store/entity/Order.java b/examples/mp-rest-jwt-principal/src/main/java/org/superbiz/store/entity/Order.java new file mode 100644 index 00000000000..26a25bf0ccb --- /dev/null +++ b/examples/mp-rest-jwt-principal/src/main/java/org/superbiz/store/entity/Order.java @@ -0,0 +1,69 @@ +/** + * 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.store.entity; + +import java.math.BigDecimal; +import java.util.List; + +public class Order { + + private Integer id; + private String createdUser; + private String updatedUser; + private BigDecimal orderPrice; + private List products; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getCreatedUser() { + return createdUser; + } + + public void setCreatedUser(String createdUser) { + this.createdUser = createdUser; + } + + public String getUpdatedUser() { + return updatedUser; + } + + public void setUpdatedUser(String updatedUser) { + this.updatedUser = updatedUser; + } + + public BigDecimal getOrderPrice() { + return orderPrice; + } + + public void setOrderPrice(BigDecimal orderPrice) { + this.orderPrice = orderPrice; + } + + public List getProducts() { + return products; + } + + public void setProducts(List products) { + this.products = products; + } +} diff --git a/examples/mp-rest-jwt-principal/src/main/java/org/superbiz/store/entity/Product.java b/examples/mp-rest-jwt-principal/src/main/java/org/superbiz/store/entity/Product.java new file mode 100644 index 00000000000..4940e7e777c --- /dev/null +++ b/examples/mp-rest-jwt-principal/src/main/java/org/superbiz/store/entity/Product.java @@ -0,0 +1,69 @@ +/** + * 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.store.entity; + +import java.math.BigDecimal; + +public class Product { + + private Integer id; + private String name; + private BigDecimal price; + private Integer stock; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public BigDecimal getPrice() { + return price; + } + + public void setPrice(BigDecimal price) { + this.price = price; + } + + public Integer getStock() { + return stock; + } + + public void setStock(Integer stock) { + this.stock = stock; + } + + @Override + public String toString() { + return "Product{" + + "id=" + id + + ", name='" + name + '\'' + + ", price=" + price + + ", stock=" + stock + + '}'; + } +} diff --git a/examples/mp-rest-jwt-principal/src/main/java/org/superbiz/store/rest/OrderRest.java b/examples/mp-rest-jwt-principal/src/main/java/org/superbiz/store/rest/OrderRest.java new file mode 100644 index 00000000000..4ef7499266a --- /dev/null +++ b/examples/mp-rest-jwt-principal/src/main/java/org/superbiz/store/rest/OrderRest.java @@ -0,0 +1,102 @@ +/** + * 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.store.rest; + +import org.eclipse.microprofile.jwt.JsonWebToken; +import org.superbiz.store.entity.Order; +import org.superbiz.store.service.OrderService; + +import javax.annotation.security.RolesAllowed; +import javax.inject.Inject; +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.List; + +@Path("store") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class OrderRest { + + @Inject + private OrderService orderService; + + @Inject + private JsonWebToken jwtPrincipal; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String status() throws Exception { + return "running"; + } + + @GET + @Path("/userinfo") + @Produces(MediaType.TEXT_PLAIN) + public String userInfo() { + return "User: " + jwtPrincipal.getName() + " is in groups " + jwtPrincipal.getGroups(); + } + + @GET + @Path("/orders") + @RolesAllowed({"merchant", "buyer"}) + public List getListOfOrders() { + return orderService.getOrders(); + } + + @GET + @Path("/orders/{id}") + @RolesAllowed({"merchant", "buyer"}) + public Order getOrder(@PathParam("id") int id) { + return orderService.getOrder(id); + } + + @POST + @Path("/orders") + @RolesAllowed({"merchant", "buyer"}) + public Response addOrder(Order order) { + Order createdOrder = orderService.addOrder(order, jwtPrincipal.getName()); + + return Response + .status(Response.Status.CREATED) + .entity(createdOrder) + .build(); + } + + @DELETE + @Path("/orders/{id}") + @RolesAllowed({"merchant"}) + public Response deleteOrder(@PathParam("id") int id) { + orderService.deleteOrder(id); + + return Response + .status(Response.Status.NO_CONTENT) + .build(); + } + + @PUT + @Path("/orders") + @RolesAllowed({"merchant", "buyer"}) + public Response updateOrder(Order order) { + Order updatedOrder = orderService.updateOrder(order, jwtPrincipal.getName()); + + return Response + .status(Response.Status.OK) + .entity(updatedOrder) + .build(); + } +} diff --git a/examples/mp-rest-jwt-principal/src/main/java/org/superbiz/store/rest/RestApplication.java b/examples/mp-rest-jwt-principal/src/main/java/org/superbiz/store/rest/RestApplication.java new file mode 100644 index 00000000000..d9e761067c9 --- /dev/null +++ b/examples/mp-rest-jwt-principal/src/main/java/org/superbiz/store/rest/RestApplication.java @@ -0,0 +1,27 @@ +/** + * 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.store.rest; + +import org.eclipse.microprofile.auth.LoginConfig; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +@ApplicationPath("/rest") +@LoginConfig(authMethod = "MP-JWT") +public class RestApplication extends Application { +} diff --git a/examples/mp-rest-jwt-principal/src/main/java/org/superbiz/store/service/OrderService.java b/examples/mp-rest-jwt-principal/src/main/java/org/superbiz/store/service/OrderService.java new file mode 100644 index 00000000000..dcf19bcf085 --- /dev/null +++ b/examples/mp-rest-jwt-principal/src/main/java/org/superbiz/store/service/OrderService.java @@ -0,0 +1,75 @@ +/** + * 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.store.service; + +import org.superbiz.store.entity.Order; +import org.superbiz.store.entity.Product; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@ApplicationScoped +public class OrderService { + + private Map ordersInStore; + + @PostConstruct + public void ProductService() { + ordersInStore = new HashMap(); + } + + public List getOrders() { + return new ArrayList<>(ordersInStore.values()); + } + + public Order getOrder(int id) { + return ordersInStore.get(id); + } + + public Order addOrder(Order order, String user) { + order.setOrderPrice(calculateOrderPrice(order)); + order.setCreatedUser(user); + + ordersInStore.put(order.getId(), order); + + return order; + } + + public void deleteOrder(int id) { + ordersInStore.remove(id); + } + + public Order updateOrder(Order order, String user) { + order.setOrderPrice(calculateOrderPrice(order)); + order.setUpdatedUser(user); + + ordersInStore.put(order.getId(), order); + + return order; + } + + private BigDecimal calculateOrderPrice(Order order) { + return order.getProducts().stream() + .map(Product::getPrice) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } +} diff --git a/examples/mp-rest-jwt-principal/src/main/resources/META-INF/microprofile-config.properties b/examples/mp-rest-jwt-principal/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 00000000000..c1ca2e65d22 --- /dev/null +++ b/examples/mp-rest-jwt-principal/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,2 @@ +mp.jwt.verify.publickey.location=/publicKey.pem +mp.jwt.verify.issuer=https://example.com \ No newline at end of file diff --git a/examples/mp-rest-jwt-principal/src/main/resources/publicKey.pem b/examples/mp-rest-jwt-principal/src/main/resources/publicKey.pem new file mode 100644 index 00000000000..39afa141390 --- /dev/null +++ b/examples/mp-rest-jwt-principal/src/main/resources/publicKey.pem @@ -0,0 +1,8 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApaNAV+HrffoQiXv1F7uqxup406191W0t +CBcJYzXaSCqA9Y64sRIeMLNO6L8iz1yz8VmWIwMRGjcGRQKH4ddInrHNtKdsdRUC/tbvR4wD/04V +gFR5Lm00jz3rHb2w1znn6GmdEzE1QoFUdRRzA+M0WJ+A0E6f9g7zXfJuHIsRkZVBfhRBxmKgvryH +t1sdlItOoZFwwEz+3PDNcMEfFRJ8EfOixhtSIyX1VSSal4ychycBdZNQLAjqrCLf0MMXqPQfuqYy +z4/4CE09yLoKqsoMfIwe2RgrGTYdHfa7z9sMSI9x1CHSYY7tx/h63weYBHSAhWBqaU0WyvYrUtLl ++xmorQIDAQAB +-----END PUBLIC KEY----- diff --git a/examples/mp-rest-jwt-principal/src/test/java/org/superbiz/store/GenerateKeyUtils.java b/examples/mp-rest-jwt-principal/src/test/java/org/superbiz/store/GenerateKeyUtils.java new file mode 100644 index 00000000000..e9d9f956a51 --- /dev/null +++ b/examples/mp-rest-jwt-principal/src/test/java/org/superbiz/store/GenerateKeyUtils.java @@ -0,0 +1,37 @@ +/** + * 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.store; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +public class GenerateKeyUtils { + public static void generateKeyPair(String keyAlgorithm, int keySize) throws NoSuchAlgorithmException { + KeyPairGenerator kpg = KeyPairGenerator.getInstance(keyAlgorithm); // RSA + kpg.initialize(keySize); // 2048 + KeyPair kp = kpg.generateKeyPair(); + + System.out.println("-----BEGIN PRIVATE KEY-----"); + System.out.println(Base64.getMimeEncoder().encodeToString(kp.getPrivate().getEncoded())); + System.out.println("-----END PRIVATE KEY-----"); + System.out.println("-----BEGIN PUBLIC KEY-----"); + System.out.println(Base64.getMimeEncoder().encodeToString(kp.getPublic().getEncoded())); + System.out.println("-----END PUBLIC KEY-----"); + } +} diff --git a/examples/mp-rest-jwt-principal/src/test/java/org/superbiz/store/OrderRestClient.java b/examples/mp-rest-jwt-principal/src/test/java/org/superbiz/store/OrderRestClient.java new file mode 100644 index 00000000000..9f4795517ce --- /dev/null +++ b/examples/mp-rest-jwt-principal/src/test/java/org/superbiz/store/OrderRestClient.java @@ -0,0 +1,60 @@ +/** + * 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.store; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.superbiz.store.entity.Order; + +import javax.enterprise.context.Dependent; +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.List; + +@Dependent +@RegisterRestClient +@Path("/test/rest/store/") +@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) +@Consumes({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) +public interface OrderRestClient { + @GET + String status(); + + @GET + @Path("/userinfo") + String getUserInfo(@HeaderParam("Authorization") String authHeaderValue); + + @GET + @Path("/orders/{id}") + Response getOrder(@HeaderParam("Authorization") String authHeaderValue, @PathParam("id") int id); + + @GET + @Path("/orders") + List getOrders(@HeaderParam("Authorization") String authHeaderValue); + + @POST + @Path("/orders") + Response addOrder(@HeaderParam("Authorization") String authHeaderValue, Order newOrder); + + @PUT + @Path("/orders") + Response updateOrder(@HeaderParam("Authorization") String authHeaderValue, Order updatedOrder); + + @DELETE + @Path("/orders/{id}") + Response deleteOrder(@HeaderParam("Authorization") String authHeaderValue, @PathParam("id") int id); +} diff --git a/examples/mp-rest-jwt-principal/src/test/java/org/superbiz/store/OrdersTest.java b/examples/mp-rest-jwt-principal/src/test/java/org/superbiz/store/OrdersTest.java new file mode 100644 index 00000000000..5f47323573e --- /dev/null +++ b/examples/mp-rest-jwt-principal/src/test/java/org/superbiz/store/OrdersTest.java @@ -0,0 +1,158 @@ +/** + * 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.store; + +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.test.spi.ArquillianProxyException; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.superbiz.store.entity.Order; +import org.superbiz.store.entity.Product; +import org.superbiz.store.rest.OrderRest; +import org.superbiz.store.rest.RestApplication; +import org.superbiz.store.service.OrderService; + +import javax.inject.Inject; +import javax.ws.rs.core.Response; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +import static org.junit.Assert.assertEquals; + +@RunWith(Arquillian.class) +public class OrdersTest { + + private final static Logger LOGGER = Logger.getLogger(OrdersTest.class.getName()); + + @Inject + @RestClient + private OrderRestClient orderRestClient; + + @Deployment() + public static WebArchive createDeployment() { + final WebArchive webArchive = ShrinkWrap.create(WebArchive.class, "test.war") + .addClasses(OrderRest.class, RestApplication.class) + .addClasses(Order.class, Product.class) + .addClass(OrderService.class) + .addClasses(OrderRestClient.class, TokenUtils.class) + .addPackages(true, "com.nimbusds", "net.minidev.json") + .addAsWebInfResource(new StringAsset(""), "beans.xml") + .addAsResource("META-INF/microprofile-config.properties") + .addAsResource("john-doe-jwt.json") + .addAsResource("alice-wonder-jwt.json") + .addAsResource("privateKey.pem") + .addAsResource("publicKey.pem"); + return webArchive; + } + + @Test + public void shouldBeRunning() { + assertEquals("running", orderRestClient.status()); + } + + @Test + public void shouldReturnUserInfo() throws Exception { + assertEquals("User: john is in groups [merchant]", orderRestClient.getUserInfo("Bearer " + createJwtToken(true))); + } + + @Test + public void shouldReturnOrder() throws Exception { + int statusCode = orderRestClient.addOrder("Bearer " + createJwtToken(true), createTestOrder()).getStatus(); + + assertEquals(Response.Status.CREATED.getStatusCode(), statusCode); + + Order order = orderRestClient.getOrder("Bearer " + createJwtToken(false), 1).readEntity(Order.class); + + assertEquals("john", order.getCreatedUser()); + } + + @Test + public void shouldReturnListOfOrders() throws Exception { + List ordersList = orderRestClient.getOrders("Bearer " + createJwtToken(true)); + assertEquals(0, ordersList.size()); + } + + @Test + public void shouldSaveOrder() throws Exception { + Order newOrder = orderRestClient.addOrder("Bearer " + createJwtToken(true), createTestOrder()).readEntity(Order.class); + + assertEquals("john", newOrder.getCreatedUser()); + } + + @Test + public void shouldUpdateOrder() throws Exception { + Order newOrder = orderRestClient.addOrder("Bearer " + createJwtToken(true), createTestOrder()).readEntity(Order.class); + + assertEquals("john", newOrder.getCreatedUser()); + assertEquals(null, newOrder.getUpdatedUser()); + + newOrder.setOrderPrice(new BigDecimal(1000)); + Order updatedOrder = orderRestClient.updateOrder("Bearer " + createJwtToken(false), newOrder).readEntity(Order.class); + + assertEquals("alice", updatedOrder.getUpdatedUser()); + } + + @Test + public void shouldDeleteOrder() throws Exception { + int statusCode = orderRestClient.addOrder("Bearer " + createJwtToken(true), createTestOrder()).getStatus(); + + assertEquals(Response.Status.CREATED.getStatusCode(), statusCode); + + statusCode = orderRestClient.deleteOrder("Bearer " + createJwtToken(true), 1).getStatus(); + + assertEquals(Response.Status.NO_CONTENT.getStatusCode(), statusCode); + } + + @Test(expected = ArquillianProxyException.class) + public void shouldNotHaveAccess() throws Exception { + orderRestClient.deleteOrder("Bearer " + createJwtToken(false), 1).getStatus(); + } + + public String createJwtToken(boolean john) throws Exception { + return TokenUtils.generateJWTString((john ? "john-doe-jwt.json" : "alice-wonder-jwt.json")); + } + + private Order createTestOrder() { + List products = new ArrayList<>(); + Product huaweiProduct = new Product(); + huaweiProduct.setId(1); + huaweiProduct.setName("Huawei P20 Lite"); + huaweiProduct.setPrice(new BigDecimal(203.31)); + huaweiProduct.setStock(2); + products.add(huaweiProduct); + + Product samsungProduct = new Product(); + samsungProduct.setId(2); + samsungProduct.setName("Samsung S9"); + samsungProduct.setPrice(new BigDecimal(821.42)); + samsungProduct.setStock(1); + products.add(samsungProduct); + + Order order = new Order(); + order.setId(1); + order.setProducts(products); + + return order; + } +} diff --git a/examples/mp-rest-jwt-principal/src/test/java/org/superbiz/store/TokenUtils.java b/examples/mp-rest-jwt-principal/src/test/java/org/superbiz/store/TokenUtils.java new file mode 100644 index 00000000000..503d14f5544 --- /dev/null +++ b/examples/mp-rest-jwt-principal/src/test/java/org/superbiz/store/TokenUtils.java @@ -0,0 +1,83 @@ +/** + * 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.store; + +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jwt.SignedJWT; +import net.minidev.json.JSONObject; +import net.minidev.json.parser.JSONParser; +import org.eclipse.microprofile.jwt.Claims; + +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; + +import static com.nimbusds.jose.JOSEObjectType.JWT; +import static com.nimbusds.jose.JWSAlgorithm.RS256; +import static com.nimbusds.jwt.JWTClaimsSet.parse; +import static java.lang.Thread.currentThread; +import static net.minidev.json.parser.JSONParser.DEFAULT_PERMISSIVE_MODE; + +public class TokenUtils { + + public static String generateJWTString(String jsonResource) throws Exception { + byte[] byteBuffer = new byte[16384]; + currentThread().getContextClassLoader() + .getResource(jsonResource) + .openStream() + .read(byteBuffer); + + JSONParser parser = new JSONParser(DEFAULT_PERMISSIVE_MODE); + JSONObject jwtJson = (JSONObject) parser.parse(byteBuffer); + + long currentTimeInSecs = (System.currentTimeMillis() / 1000); + long expirationTime = currentTimeInSecs + 1000; + + jwtJson.put(Claims.iat.name(), currentTimeInSecs); + jwtJson.put(Claims.auth_time.name(), currentTimeInSecs); + jwtJson.put(Claims.exp.name(), expirationTime); + + SignedJWT signedJWT = new SignedJWT(new JWSHeader + .Builder(RS256) + .keyID("/privateKey.pem") + .type(JWT) + .build(), parse(jwtJson)); + + signedJWT.sign(new RSASSASigner(readPrivateKey("privateKey.pem"))); + + return signedJWT.serialize(); + } + + public static PrivateKey readPrivateKey(String resourceName) throws Exception { + byte[] byteBuffer = new byte[16384]; + int length = currentThread().getContextClassLoader() + .getResource(resourceName) + .openStream() + .read(byteBuffer); + + String key = new String(byteBuffer, 0, length).replaceAll("-----BEGIN (.*)-----", "") + .replaceAll("-----END (.*)----", "") + .replaceAll("\r\n", "") + .replaceAll("\n", "") + .trim(); + + return KeyFactory.getInstance("RSA") + .generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(key))); + } +} diff --git a/examples/mp-rest-jwt-principal/src/test/resources/META-INF/microprofile-config.properties b/examples/mp-rest-jwt-principal/src/test/resources/META-INF/microprofile-config.properties new file mode 100644 index 00000000000..7cf428f3c94 --- /dev/null +++ b/examples/mp-rest-jwt-principal/src/test/resources/META-INF/microprofile-config.properties @@ -0,0 +1,3 @@ +org.superbiz.store.OrderRestClient/mp-rest/url=http://localhost:4444 +mp.jwt.verify.publickey.location=/publicKey.pem +mp.jwt.verify.issuer=https://example.com \ No newline at end of file diff --git a/examples/mp-rest-jwt-principal/src/test/resources/alice-wonder-jwt.json b/examples/mp-rest-jwt-principal/src/test/resources/alice-wonder-jwt.json new file mode 100644 index 00000000000..e2c6d345585 --- /dev/null +++ b/examples/mp-rest-jwt-principal/src/test/resources/alice-wonder-jwt.json @@ -0,0 +1,10 @@ +{ + "iss": "https://example.com", + "upn": "alice", + "sub": "alice.wonder@example.com", + "name": "Alice Wonder", + "iat": 1516239022, + "groups": [ + "buyer" + ] +} \ No newline at end of file diff --git a/examples/mp-rest-jwt-principal/src/test/resources/arquillian.xml b/examples/mp-rest-jwt-principal/src/test/resources/arquillian.xml new file mode 100644 index 00000000000..534eb1cb6a5 --- /dev/null +++ b/examples/mp-rest-jwt-principal/src/test/resources/arquillian.xml @@ -0,0 +1,38 @@ + + + + + + + -1 + 4444 + -1 + -1 + microprofile + true + true + target/server + target/arquillian + + + \ No newline at end of file diff --git a/examples/mp-rest-jwt-principal/src/test/resources/john-doe-jwt.json b/examples/mp-rest-jwt-principal/src/test/resources/john-doe-jwt.json new file mode 100644 index 00000000000..7c9155faad1 --- /dev/null +++ b/examples/mp-rest-jwt-principal/src/test/resources/john-doe-jwt.json @@ -0,0 +1,10 @@ +{ + "iss": "https://example.com", + "upn": "john", + "sub": "john.doe@example.com", + "name": "John Doe", + "iat": 1516239022, + "groups": [ + "merchant" + ] +} \ No newline at end of file diff --git a/examples/mp-rest-jwt-principal/src/test/resources/privateKey.pem b/examples/mp-rest-jwt-principal/src/test/resources/privateKey.pem new file mode 100644 index 00000000000..a05830543e3 --- /dev/null +++ b/examples/mp-rest-jwt-principal/src/test/resources/privateKey.pem @@ -0,0 +1,24 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQClo0BX4et9+hCJe/UXu6rG6njT +rX3VbS0IFwljNdpIKoD1jrixEh4ws07ovyLPXLPxWZYjAxEaNwZFAofh10iesc20p2x1FQL+1u9H +jAP/ThWAVHkubTSPPesdvbDXOefoaZ0TMTVCgVR1FHMD4zRYn4DQTp/2DvNd8m4cixGRlUF+FEHG +YqC+vIe3Wx2Ui06hkXDATP7c8M1wwR8VEnwR86LGG1IjJfVVJJqXjJyHJwF1k1AsCOqsIt/Qwxeo +9B+6pjLPj/gITT3Iugqqygx8jB7ZGCsZNh0d9rvP2wxIj3HUIdJhju3H+HrfB5gEdICFYGppTRbK +9itS0uX7GaitAgMBAAECggEBAIp+VpVku++If/1EnOisLJ3HfSGYpWcnswhZoqrGY24Fw/AN1pWP +jZiwxGDNOADkBTze6C78KHef3GklNZJ63Ch8iH/toTzARdHoywcqpkj48/dqKizMeK7wWb9zVQKQ +Uzy72e8rLNofowuF/qkYdzNGooEJzKwDUi+SPck16omrRgJoPd2A8WQC4Fjib7dd8uyP8iDwUYn0 +c4IGWULuKI+mEpE2QXR+wp1aXoJxT0TayyKBHweDzmcLpCpJ77XojHN8ZaEvgRDz+a2i/UVRQe9B +NBIInIrgCa+D6YTfjVl2ApLstmcmp/mKId0r3ca5aII46GehTjRsXyb+38cY/60CgYEA4I83rtDt +yMGfkVzHn9og8faYzMafCQW3WGVJ/4aIb+yM08+q+IqmtAcAv9qjnNvsUJDFqZVeSlxMlrq49qA/ +HV9WTc3zepmRdTH7wrFxs8ragW9krIj4Y9rEb47SyL2FzmXzXnMj9SXVWpnY7qjG1Ykobo+dRwGY +TVbB/U/7Cn8CgYEAvNQg42rkXn/DXEPsdAQ2bdRh8a+2tCPcSkzk9wvhocBuk52XdFQuNouq5wjy +83JayBQV1jNP9hFiZppPuraVzMtiY76ZOSST+houUga4/sh8oD4uJRPmFAv5H9XuYsFZpbSzZXtr +562zCxUPN0ssDrE4c+TkHPTaiSlzzWY6/tMCgYBlezON1Ctxa1ciSQyJx/jVgDyjZite1295CiU7 +zd+AvSUTX6kDMx2NBBEporQH4jdUXWiGb9Mxxa5y+6U1B0weiQQmmykqQZZDoTgGT0x0FPtUPTQA +6NFfxvC4/ZFyWHvMv7QQ/fXFBrj5fcdUa3+X5qkX9dz8xtK+OLPoNynbHwKBgQCHhcfE8KbOm+ve +gHFoIs3drZxOFcqPJ12nheCe3kwmBzJVh7l3qCMyyrLx9h5IUz6Mcr3pahJtjLSO5xlp7Dk/LomD +BPx7YqFB0yCDhoendMTdTUNZIRr0MFOwYZ4iCpsIrtRCdX5QXP4vagHtsWoBcXgO1axSi/l8j9+o +/0JAOQKBgBu9iMX0tZNBoGW4Z0NABtr5xF9nsutbfXPFpcoAUZI/g1eL3GXCpVi/Q5Ztzzh7m3xb +8LmeAiCNbQseUOi4r8aO54jmSaCam0EIZuTKpGMzxL5aHZK2wsouLVF1hA7+vI7Fa6QoHiwUuoXV +5i2PPk0BHFjPDbIhhpGgUIkiQRom +-----END PRIVATE KEY-----