You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tomee.apache.org by db...@apache.org on 2022/09/13 01:18:09 UTC

[tomee] branch main updated: iTest coverage for TOMEE-2517

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

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


The following commit(s) were added to refs/heads/main by this push:
     new c419971d4c iTest coverage for TOMEE-2517
c419971d4c is described below

commit c419971d4c70d4bac9fa94fe5c7abeb07e231581
Author: David Blevins <db...@tomitribe.com>
AuthorDate: Mon Sep 12 18:16:58 2022 -0700

    iTest coverage for TOMEE-2517
---
 .../tomee/microprofile/jwt/itest/Output.java       |  49 +++
 .../jwt/itest/bval/ValidationConstraintsTest.java  | 469 +++++++++++++++++++++
 2 files changed, 518 insertions(+)

diff --git a/itests/microprofile-jwt-itests/src/test/java/org/apache/tomee/microprofile/jwt/itest/Output.java b/itests/microprofile-jwt-itests/src/test/java/org/apache/tomee/microprofile/jwt/itest/Output.java
new file mode 100644
index 0000000000..67d83af77f
--- /dev/null
+++ b/itests/microprofile-jwt-itests/src/test/java/org/apache/tomee/microprofile/jwt/itest/Output.java
@@ -0,0 +1,49 @@
+/*
+ * 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.apache.tomee.microprofile.jwt.itest;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.Assert.assertTrue;
+
+public class Output {
+    private final List<String> output = new ArrayList<>();
+
+    public void add(final String line) {
+        this.output.add(line);
+    }
+
+    public Output assertPresent(final String s) {
+        final Optional<String> actual = output.stream()
+                .filter(line -> line.contains(s))
+                .findFirst();
+
+        assertTrue(actual.isPresent());
+        return this;
+    }
+
+    public Output assertNotPresent(final String s) {
+        final Optional<String> actual = output.stream()
+                .filter(line -> line.contains(s))
+                .findFirst();
+
+        assertTrue(!actual.isPresent());
+        return this;
+    }
+}
diff --git a/itests/microprofile-jwt-itests/src/test/java/org/apache/tomee/microprofile/jwt/itest/bval/ValidationConstraintsTest.java b/itests/microprofile-jwt-itests/src/test/java/org/apache/tomee/microprofile/jwt/itest/bval/ValidationConstraintsTest.java
new file mode 100644
index 0000000000..7f047f4576
--- /dev/null
+++ b/itests/microprofile-jwt-itests/src/test/java/org/apache/tomee/microprofile/jwt/itest/bval/ValidationConstraintsTest.java
@@ -0,0 +1,469 @@
+/*
+ * 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.apache.tomee.microprofile.jwt.itest.bval;
+
+import jakarta.enterprise.context.RequestScoped;
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+import jakarta.validation.Payload;
+import jakarta.ws.rs.ApplicationPath;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import org.apache.cxf.feature.LoggingFeature;
+import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.johnzon.jaxrs.JohnzonProvider;
+import org.apache.tomee.microprofile.jwt.itest.Output;
+import org.apache.tomee.microprofile.jwt.itest.Tokens;
+import org.apache.tomee.server.composer.Archive;
+import org.apache.tomee.server.composer.TomEE;
+import org.eclipse.microprofile.auth.LoginConfig;
+import org.eclipse.microprofile.jwt.JsonWebToken;
+import org.junit.Test;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.Base64;
+import java.util.Set;
+
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static java.util.Collections.singletonList;
+import static org.junit.Assert.assertEquals;
+
+public class ValidationConstraintsTest {
+
+    @Test
+    public void testMissingSub() throws Exception {
+        final Scenario scenario = Scenario.setup();
+
+        final String claims = "{" +
+//                "  \"sub\":\"Jane Awesome\"," +
+                "  \"exp\":2552047942" +
+                "}";
+
+        {// invalid token
+            final String token = scenario.tokens.asToken(claims);
+            final Response response = scenario.webClient.reset()
+                    .path("/colors/red")
+                    .header("Content-Type", "application/json")
+                    .header("Authorization", "Bearer " + token)
+                    .get();
+            assertEquals(401, response.getStatus());
+        }
+
+        scenario.output()
+                .assertPresent("rejected due to invalid claims")
+                .assertPresent("No Subject (sub) claim is present.")
+                .assertNotPresent("\tat org."); // no stack traces
+    }
+
+    @Test
+    public void testMissingExpiration() throws Exception {
+
+        final String claims = "{" +
+                "  \"sub\":\"Jane Awesome\"" +
+                "}";
+
+        final Scenario scenario = Scenario.setup();
+
+        {// invalid token
+            final String token = scenario.tokens.asToken(claims);
+            final Response response = scenario.webClient.reset()
+                    .path("/colors/red")
+                    .header("Content-Type", "application/json")
+                    .header("Authorization", "Bearer " + token)
+                    .get();
+            assertEquals(401, response.getStatus());
+        }
+
+        scenario.output()
+                .assertPresent("rejected due to invalid claims")
+                .assertPresent("No Expiration Time (exp) claim present.")
+                .assertNotPresent("\tat org."); // no stack traces
+    }
+
+    @Test
+    public void valid() throws Exception {
+        final String claims = "{" +
+                "  \"sub\":\"Jane Awesome\"," +
+                "  \"iss\":\"http://foo.bar.com\"," +
+                "  \"aud\":[\"bar\",\"user\"]," +
+                "  \"groups\":[\"manager\",\"user\"]," +
+                "  \"exp\":2552047942" +
+                "}";
+
+        final Scenario scenario = Scenario.setup();
+
+        {// invalid token
+            final String token = scenario.tokens.asToken(claims);
+            final Response response = scenario.webClient.reset()
+                    .path("/colors/red")
+                    .header("Content-Type", "application/json")
+                    .header("Authorization", "Bearer " + token)
+                    .get();
+            assertEquals(200, response.getStatus());
+        }
+
+        scenario.output()
+                .assertNotPresent("rejected due to invalid claims") // no stack traces
+                .assertNotPresent("\tat org."); // no stack traces
+    }
+
+
+    @Test
+    public void invalidAudAndIss() throws Exception {
+        final String claims = "{" +
+                "  \"sub\":\"Jane Awesome\"," +
+                "  \"iss\":\"http://something.com\"," +
+                "  \"groups\":[\"manager\",\"user\"]," +
+                "  \"exp\":2552047942" +
+                "}";
+
+        final Scenario scenario = Scenario.setup();
+
+        {// invalid token
+            final String token = scenario.tokens.asToken(claims);
+            final Response response = scenario.webClient.reset()
+                    .path("/colors/red")
+                    .header("Content-Type", "application/json")
+                    .header("Authorization", "Bearer " + token)
+                    .get();
+            assertEquals(403, response.getStatus());
+        }
+
+        scenario.output()
+                .assertPresent("The 'aud' claim is required")
+                .assertPresent("The 'aud' claim must contain 'bar'")
+                .assertPresent("The 'iss' claim must be 'http://foo.bar.com'")
+                .assertNotPresent("\tat org."); // no stack traces
+    }
+
+    @Test
+    public void invalidIss() throws Exception {
+        final String claims = "{" +
+                "  \"sub\":\"Jane Awesome\"," +
+                "  \"aud\":[\"bar\",\"user\"]," +
+                "  \"iss\":\"http://something.com\"," +
+                "  \"groups\":[\"manager\",\"user\"]," +
+                "  \"exp\":2552047942" +
+                "}";
+
+        final Scenario scenario = Scenario.setup();
+
+        {// invalid token
+            final String token = scenario.tokens.asToken(claims);
+            final Response response = scenario.webClient.reset()
+                    .path("/colors/red")
+                    .header("Content-Type", "application/json")
+                    .header("Authorization", "Bearer " + token)
+                    .get();
+            assertEquals(403, response.getStatus());
+        }
+
+        scenario.output()
+                .assertPresent("The 'iss' claim must be 'http://foo.bar.com'")
+                .assertNotPresent("\tat org."); // no stack traces
+    }
+
+    @Test
+    public void invalidAud() throws Exception {
+        final String claims = "{" +
+                "  \"sub\":\"Jane Awesome\"," +
+                "  \"aud\":[\"foo\",\"user\"]," +
+                "  \"iss\":\"http://foo.bar.com\"," +
+                "  \"groups\":[\"manager\",\"user\"]," +
+                "  \"exp\":2552047942" +
+                "}";
+
+        final Scenario scenario = Scenario.setup();
+
+        {// invalid token
+            final String token = scenario.tokens.asToken(claims);
+            final Response response = scenario.webClient.reset()
+                    .path("/colors/red")
+                    .header("Content-Type", "application/json")
+                    .header("Authorization", "Bearer " + token)
+                    .get();
+            assertEquals(403, response.getStatus());
+        }
+
+        scenario.output()
+                .assertPresent("The 'aud' claim must contain 'bar'")
+                .assertNotPresent("\tat org."); // no stack traces
+    }
+
+    @Test
+    public void missingAud() throws Exception {
+
+        final String claims = "{" +
+                "  \"sub\":\"Jane Awesome\"," +
+                "  \"iss\":\"http://foo.bar.com\"," +
+                "  \"groups\":[\"manager\",\"user\"]," +
+                "  \"exp\":2552047942" +
+                "}";
+
+        final Scenario scenario = Scenario.setup();
+
+        {// invalid token
+            final String token = scenario.tokens.asToken(claims);
+            final Response response = scenario.webClient.reset()
+                    .path("/colors/red")
+                    .header("Content-Type", "application/json")
+                    .header("Authorization", "Bearer " + token)
+                    .get();
+            assertEquals(403, response.getStatus());
+        }
+
+        scenario.output()
+                .assertPresent("The 'aud' claim is required")
+                .assertPresent("The 'aud' claim must contain 'bar'")
+                .assertNotPresent("\tat org."); // no stack traces
+    }
+
+    private static class Scenario {
+        private final Tokens tokens;
+        private final WebClient webClient;
+        private final Output output;
+
+        public Scenario(final Tokens tokens, final WebClient webClient, final Output output) {
+            this.tokens = tokens;
+            this.webClient = webClient;
+            this.output = output;
+        }
+
+        public static Scenario setup() throws Exception {
+            final Tokens tokens = Tokens.rsa(2048, 256);
+
+            final Output output = new Output();
+            final TomEE tomee = TomEE.microprofile()
+                    .add("webapps/test/WEB-INF/beans.xml", "")
+                    .add("webapps/test/WEB-INF/lib/app.jar", Archive.archive()
+                            .add(ValidationConstraintsTest.class)
+                            .add(ColorService.class)
+                            .add(Color.class)
+                            .add(RequireClaim.class)
+                            .add(Audience.class)
+                            .add(Issuer.class)
+                            .add(False.class)
+                            .add(RequireClaim.Constraint.class)
+                            .add(Audience.Constraint.class)
+                            .add(Issuer.Constraint.class)
+                            .add(False.Constraint.class)
+                            .add(Api.class)
+                            .add("META-INF/microprofile-config.properties", "#\n" +
+                                    "mp.jwt.verify.publickey=" + Base64.getEncoder().encodeToString(tokens.getPublicKey().getEncoded()))
+                            .asJar())
+                    .watch("org.apache.tomee.microprofile.jwt.", "\n", output::add)
+                    //                .update()
+                    .build();
+
+            final WebClient webClient = WebClient.create(tomee.toURI().resolve("/test").toURL().toExternalForm(),
+                    singletonList(new JohnzonProvider<>()),
+                    singletonList(new LoggingFeature()), null);
+            return new Scenario(tokens, webClient, output);
+        }
+
+        public Output output() {
+            return output;
+        }
+    }
+
+
+    @ApplicationPath("/api")
+    @LoginConfig(authMethod = "MP-JWT")
+    public class Api extends Application {
+    }
+
+    @Path("/colors")
+    @Produces(MediaType.APPLICATION_JSON)
+    @Consumes(MediaType.APPLICATION_JSON)
+    @RequestScoped
+    public static class ColorService {
+
+        @GET
+        @Path("red")
+        @Audience("bar")
+        @Issuer("http://foo.bar.com")
+        @False
+        public Color red() {
+            return new Color("red");
+        }
+
+        @GET
+        @Path("green")
+        public Color green() {
+            return new Color("green");
+        }
+
+        /**
+         * To ensure non-public methods do not cause errors
+         */
+        @GET
+        @Path("blue")
+        private Color blue() {
+            return new Color("blue");
+        }
+    }
+
+    public static class Color {
+        private String color;
+
+        public Color() {
+        }
+
+        public Color(final String color) {
+            this.color = color;
+        }
+
+        public String getColor() {
+            return color;
+        }
+
+        public void setColor(final String color) {
+            this.color = color;
+        }
+    }
+
+    @Documented
+    @jakarta.validation.Constraint(validatedBy = {RequireClaim.Constraint.class})
+    @Target({METHOD, FIELD, ANNOTATION_TYPE, PARAMETER})
+    @Retention(RUNTIME)
+    public @interface RequireClaim {
+
+        String value();
+
+        Class<?>[] groups() default {};
+
+        String message() default "The '{value}' claim is required";
+
+        Class<? extends Payload>[] payload() default {};
+
+        class Constraint implements ConstraintValidator<RequireClaim, JsonWebToken> {
+
+            private RequireClaim claim;
+
+            @Override
+            public void initialize(final RequireClaim claim) {
+                this.claim = claim;
+            }
+
+            @Override
+            public boolean isValid(final JsonWebToken jsonWebToken, final ConstraintValidatorContext context) {
+                return jsonWebToken.claim(claim.value()).isPresent();
+            }
+        }
+    }
+
+    @RequireClaim("aud")
+    @Documented
+    @jakarta.validation.Constraint(validatedBy = {Audience.Constraint.class})
+    @Target({METHOD, FIELD, ANNOTATION_TYPE, PARAMETER})
+    @Retention(RUNTIME)
+    public @interface Audience {
+
+        String value();
+
+        Class<?>[] groups() default {};
+
+        String message() default "The 'aud' claim must contain '{value}'";
+
+        Class<? extends Payload>[] payload() default {};
+
+
+        class Constraint implements ConstraintValidator<Audience, JsonWebToken> {
+            private Audience audience;
+
+            @Override
+            public void initialize(final Audience constraint) {
+                this.audience = constraint;
+            }
+
+            @Override
+            public boolean isValid(final JsonWebToken value, final ConstraintValidatorContext context) {
+                final Set<String> audience = value.getAudience();
+                return audience != null && audience.contains(this.audience.value());
+            }
+        }
+    }
+
+    @Documented
+    @jakarta.validation.Constraint(validatedBy = {Issuer.Constraint.class})
+    @Target({METHOD, FIELD, ANNOTATION_TYPE, PARAMETER})
+    @Retention(RUNTIME)
+    public @interface Issuer {
+
+        String value();
+
+        Class<?>[] groups() default {};
+
+        String message() default "The 'iss' claim must be '{value}'";
+
+        Class<? extends Payload>[] payload() default {};
+
+
+        class Constraint implements ConstraintValidator<Issuer, JsonWebToken> {
+            private Issuer issuer;
+
+            @Override
+            public void initialize(final Issuer constraint) {
+                this.issuer = constraint;
+            }
+
+            @Override
+            public boolean isValid(final JsonWebToken value, final ConstraintValidatorContext context) {
+                final String issuer = value.getIssuer();
+                return this.issuer.value().equals(issuer);
+            }
+        }
+    }
+
+    /**
+     * Regular bean validation annotations should not affect the JWT Validation
+     */
+    @Documented
+    @jakarta.validation.Constraint(validatedBy = {False.Constraint.class})
+    @Target({METHOD, FIELD, ANNOTATION_TYPE, PARAMETER})
+    @Retention(RUNTIME)
+    public @interface False {
+
+        Class<?>[] groups() default {};
+
+        String message() default "This will never pass";
+
+        Class<? extends Payload>[] payload() default {};
+
+
+        class Constraint implements ConstraintValidator<False, Color> {
+            @Override
+            public boolean isValid(final Color value, final ConstraintValidatorContext context) {
+                return false;
+            }
+        }
+    }
+
+}
\ No newline at end of file