Repository: tomee Updated Branches: refs/heads/master 3b2d562f8 -> 5c047f803
TOMEE-2334: example for Principal usage in MicroProfile JWT with TomEE Project: http://git-wip-us.apache.org/repos/asf/tomee/repo Commit: http://git-wip-us.apache.org/repos/asf/tomee/commit/83a1a56d Tree: http://git-wip-us.apache.org/repos/asf/tomee/tree/83a1a56d Diff: http://git-wip-us.apache.org/repos/asf/tomee/diff/83a1a56d Branch: refs/heads/master Commit: 83a1a56d5c44fce919edb2c8cdc236042eefa8bf Parents: b489fca Author: cotnic <[email protected]> Authored: Mon Dec 31 19:25:50 2018 +0100 Committer: cotnic <[email protected]> Committed: Mon Dec 31 19:25:50 2018 +0100 ---------------------------------------------------------------------- examples/mp-rest-jwt-principal/README.adoc | 103 +++++++++++ examples/mp-rest-jwt-principal/pom.xml | 184 +++++++++++++++++++ .../java/org/superbiz/store/entity/Order.java | 69 +++++++ .../java/org/superbiz/store/entity/Product.java | 69 +++++++ .../java/org/superbiz/store/rest/OrderRest.java | 102 ++++++++++ .../superbiz/store/rest/RestApplication.java | 27 +++ .../superbiz/store/service/OrderService.java | 75 ++++++++ .../META-INF/microprofile-config.properties | 2 + .../src/main/resources/publicKey.pem | 8 + .../org/superbiz/store/GenerateKeyUtils.java | 37 ++++ .../org/superbiz/store/OrderRestClient.java | 60 ++++++ .../java/org/superbiz/store/OrdersTest.java | 161 ++++++++++++++++ .../java/org/superbiz/store/TokenUtils.java | 83 +++++++++ .../META-INF/microprofile-config.properties | 3 + .../src/test/resources/alice-wonder-jwt.json | 10 + .../src/test/resources/arquillian.xml | 38 ++++ .../src/test/resources/john-doe-jwt.json | 10 + .../src/test/resources/privateKey.pem | 24 +++ 18 files changed, 1065 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/tomee/blob/83a1a56d/examples/mp-rest-jwt-principal/README.adoc ---------------------------------------------------------------------- diff --git a/examples/mp-rest-jwt-principal/README.adoc b/examples/mp-rest-jwt-principal/README.adoc new file mode 100644 index 0000000..5fc3dbd --- /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: ++ +.... +<dependency> + <groupId>org.eclipse.microprofile.jwt</groupId> + <artifactId>microprofile-jwt-auth-api</artifactId> + <version>${mp-jwt.version}</version> + <scope>provided</scope> +</dependency> +.... +. 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": "[email protected]", + "name": "Alice Wonder", + "iat": 1516239022, + "groups": [ + "buyer" + ] +} +---- + +`john-doe-jwt.json` +[source,json] +---- +{ + "iss": "https://example.com", + "upn": "john", + "sub": "[email protected]", + "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 http://git-wip-us.apache.org/repos/asf/tomee/blob/83a1a56d/examples/mp-rest-jwt-principal/pom.xml ---------------------------------------------------------------------- diff --git a/examples/mp-rest-jwt-principal/pom.xml b/examples/mp-rest-jwt-principal/pom.xml new file mode 100644 index 0000000..cf9bb05 --- /dev/null +++ b/examples/mp-rest-jwt-principal/pom.xml @@ -0,0 +1,184 @@ +<?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>mp-rest-jwt-principal</artifactId> + <version>8.0.0-SNAPSHOT</version> + <packaging>war</packaging> + <name>OpenEJB :: Examples :: MP REST JWT PRINCIPAL</name> + + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <tomee.version>8.0.0-SNAPSHOT</tomee.version> + <junit.version>4.23</junit.version> + <arquillian-bom.version>1.4.1.Final</arquillian-bom.version> + <mp-jwt.version>1.1.1</mp-jwt.version> + <mp-config.version>1.0</mp-config.version> + <mp-rest-client.version>1.1</mp-rest-client.version> + </properties> + + <build> + <defaultGoal>install</defaultGoal> + <finalName>phonestore</finalName> + + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <version>2.18.1</version> + <configuration> + <reuseForks>false</reuseForks> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-war-plugin</artifactId> + <version>3.1.0</version> + </plugin> + <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> + <plugin> + <groupId>org.apache.tomee.maven</groupId> + <artifactId>tomee-maven-plugin</artifactId> + <version>${tomee.version}</version> + <configuration> + <tomeeClassifier>microprofile</tomeeClassifier> + <args>-Xmx512m -XX:PermSize=256m</args> + <config>${project.basedir}/src/main/tomee/</config> + </configuration> + </plugin> + </plugins> + </build> + + <dependencyManagement> + <dependencies> + <!-- Now pull in our server-based unit testing framework --> + <dependency> + <groupId>org.jboss.arquillian</groupId> + <artifactId>arquillian-bom</artifactId> + <version>${arquillian-bom.version}</version> + <scope>import</scope> + <type>pom</type> + </dependency> + </dependencies> + </dependencyManagement> + + + <dependencies> + <dependency> + <groupId>org.apache.tomee</groupId> + <artifactId>javaee-api</artifactId> + <version>8.0</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.eclipse.microprofile.jwt</groupId> + <artifactId>microprofile-jwt-auth-api</artifactId> + <version>${mp-jwt.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.eclipse.microprofile.rest.client</groupId> + <artifactId>microprofile-rest-client-api</artifactId> + <version>${mp-rest-client.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.eclipse.microprofile.config</groupId> + <artifactId>microprofile-config-api</artifactId> + <version>${mp-config.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.nimbusds</groupId> + <artifactId>nimbus-jose-jwt</artifactId> + <version>${junit.version}</version> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.12</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.jboss.arquillian.junit</groupId> + <artifactId>arquillian-junit-container</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <profiles> + <profile> + <id>arquillian-tomee-remote</id> + <activation> + <activeByDefault>true</activeByDefault> + </activation> + <dependencies> + <dependency> + <groupId>org.apache.tomee</groupId> + <artifactId>arquillian-tomee-remote</artifactId> + <version>${tomee.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.tomee</groupId> + <artifactId>apache-tomee</artifactId> + <version>${tomee.version}</version> + <type>zip</type> + <classifier>microprofile</classifier> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.tomee</groupId> + <artifactId>mp-jwt</artifactId> + <version>${tomee.version}</version> + <scope>provided</scope> + </dependency> + </dependencies> + </profile> + </profiles> + + <!-- + 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> http://git-wip-us.apache.org/repos/asf/tomee/blob/83a1a56d/examples/mp-rest-jwt-principal/src/main/java/org/superbiz/store/entity/Order.java ---------------------------------------------------------------------- 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 0000000..26a25bf --- /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 + * <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.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<Product> 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<Product> getProducts() { + return products; + } + + public void setProducts(List<Product> products) { + this.products = products; + } +} http://git-wip-us.apache.org/repos/asf/tomee/blob/83a1a56d/examples/mp-rest-jwt-principal/src/main/java/org/superbiz/store/entity/Product.java ---------------------------------------------------------------------- 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 0000000..4940e7e --- /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 + * <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.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 + + '}'; + } +} http://git-wip-us.apache.org/repos/asf/tomee/blob/83a1a56d/examples/mp-rest-jwt-principal/src/main/java/org/superbiz/store/rest/OrderRest.java ---------------------------------------------------------------------- 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 0000000..4ef7499 --- /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 + * <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.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<Order> 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(); + } +} http://git-wip-us.apache.org/repos/asf/tomee/blob/83a1a56d/examples/mp-rest-jwt-principal/src/main/java/org/superbiz/store/rest/RestApplication.java ---------------------------------------------------------------------- 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 0000000..d9e7610 --- /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 + * <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.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 { +} http://git-wip-us.apache.org/repos/asf/tomee/blob/83a1a56d/examples/mp-rest-jwt-principal/src/main/java/org/superbiz/store/service/OrderService.java ---------------------------------------------------------------------- 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 0000000..dcf19bc --- /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 + * <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.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<Integer, Order> ordersInStore; + + @PostConstruct + public void ProductService() { + ordersInStore = new HashMap(); + } + + public List<Order> 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); + } +} http://git-wip-us.apache.org/repos/asf/tomee/blob/83a1a56d/examples/mp-rest-jwt-principal/src/main/resources/META-INF/microprofile-config.properties ---------------------------------------------------------------------- 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 0000000..c1ca2e6 --- /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 http://git-wip-us.apache.org/repos/asf/tomee/blob/83a1a56d/examples/mp-rest-jwt-principal/src/main/resources/publicKey.pem ---------------------------------------------------------------------- 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 0000000..39afa14 --- /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----- http://git-wip-us.apache.org/repos/asf/tomee/blob/83a1a56d/examples/mp-rest-jwt-principal/src/test/java/org/superbiz/store/GenerateKeyUtils.java ---------------------------------------------------------------------- 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 0000000..e9d9f95 --- /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 + * <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.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-----"); + } +} http://git-wip-us.apache.org/repos/asf/tomee/blob/83a1a56d/examples/mp-rest-jwt-principal/src/test/java/org/superbiz/store/OrderRestClient.java ---------------------------------------------------------------------- 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 0000000..9f47955 --- /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 + * <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.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<Order> 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); +} http://git-wip-us.apache.org/repos/asf/tomee/blob/83a1a56d/examples/mp-rest-jwt-principal/src/test/java/org/superbiz/store/OrdersTest.java ---------------------------------------------------------------------- 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 0000000..2768f64 --- /dev/null +++ b/examples/mp-rest-jwt-principal/src/test/java/org/superbiz/store/OrdersTest.java @@ -0,0 +1,161 @@ +/** + * 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.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.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Ignore; +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/>"), "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<Order> 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 + @Ignore + public void shouldNotHaveAccess() throws Exception { + int statusCode = orderRestClient.deleteOrder("Bearer " + createJwtToken(false), 1).getStatus(); + + assertEquals(Response.Status.FORBIDDEN.getStatusCode(), statusCode); + } + + public String createJwtToken(boolean john) throws Exception { + return TokenUtils.generateJWTString((john ? "john-doe-jwt.json" : "alice-wonder-jwt.json")); + } + + private Order createTestOrder() { + List<Product> 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; + } +} http://git-wip-us.apache.org/repos/asf/tomee/blob/83a1a56d/examples/mp-rest-jwt-principal/src/test/java/org/superbiz/store/TokenUtils.java ---------------------------------------------------------------------- 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 0000000..503d14f --- /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 + * <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.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))); + } +} http://git-wip-us.apache.org/repos/asf/tomee/blob/83a1a56d/examples/mp-rest-jwt-principal/src/test/resources/META-INF/microprofile-config.properties ---------------------------------------------------------------------- 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 0000000..7cf428f --- /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 http://git-wip-us.apache.org/repos/asf/tomee/blob/83a1a56d/examples/mp-rest-jwt-principal/src/test/resources/alice-wonder-jwt.json ---------------------------------------------------------------------- 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 0000000..e2c6d34 --- /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": "[email protected]", + "name": "Alice Wonder", + "iat": 1516239022, + "groups": [ + "buyer" + ] +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/83a1a56d/examples/mp-rest-jwt-principal/src/test/resources/arquillian.xml ---------------------------------------------------------------------- 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 0000000..534eb1c --- /dev/null +++ b/examples/mp-rest-jwt-principal/src/test/resources/arquillian.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<!-- + + 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. +--> +<arquillian xmlns="http://jboss.org/schema/arquillian" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation=" + http://jboss.org/schema/arquillian + http://jboss.org/schema/arquillian/arquillian_1_0.xsd"> + + <container qualifier="server" default="true"> + <configuration> + <property name="httpsPort">-1</property> + <property name="httpPort">4444</property> + <property name="stopPort">-1</property> + <property name="ajpPort">-1</property> + <property name="classifier">microprofile</property> + <property name="simpleLog">true</property> + <property name="cleanOnStartUp">true</property> + <property name="dir">target/server</property> + <property name="appWorkingDir">target/arquillian</property> + </configuration> + </container> +</arquillian> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/83a1a56d/examples/mp-rest-jwt-principal/src/test/resources/john-doe-jwt.json ---------------------------------------------------------------------- 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 0000000..7c9155f --- /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": "[email protected]", + "name": "John Doe", + "iat": 1516239022, + "groups": [ + "merchant" + ] +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/83a1a56d/examples/mp-rest-jwt-principal/src/test/resources/privateKey.pem ---------------------------------------------------------------------- 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 0000000..a058305 --- /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-----
