http://localhost:8080/myapp/
. If unspecified, the app context
- * root will be automatically detected by {@link ApplicationEnvironment#getApplicationURL()}
+ * For example, http://localhost:8080/myapp/
. If unspecified, the app context
+ * root will be automatically detected by {@link ApplicationEnvironment#getApplicationURL()}
* @return The same builder instance
*/
public RestClientBuilder withAppContextRoot(String appContextRoot) {
@@ -67,9 +67,9 @@ public RestClientBuilder withAppContextRoot(String appContextRoot) {
/**
* @param jaxrsPath The portion of the path after the app context root. For example, if a JAX-RS
- * endpoint is deployed at http://localhost:8080/myapp/hello
and the app context root
- * is http://localhost:8080/myapp/
, then the jaxrsPath is hello
. If
- * unspecified, the JAX-RS path will be automatically detected by annotation scanning.
+ * endpoint is deployed at http://localhost:8080/myapp/hello
and the app context root
+ * is http://localhost:8080/myapp/
, then the jaxrsPath is hello
. If
+ * unspecified, the JAX-RS path will be automatically detected by annotation scanning.
* @return The same builder instance
*/
public RestClientBuilder withJaxrsPath(String jaxrsPath) {
@@ -93,7 +93,7 @@ public RestClientBuilder withJwt(String jwt) {
}
/**
- * @param user The username portion of the Basic auth header
+ * @param user The username portion of the Basic auth header
* @param password The password portion of the Basic auth header
* @return The same builder instance
*/
@@ -110,7 +110,7 @@ public RestClientBuilder withBasicAuth(String user, String password) {
}
/**
- * @param key The header key
+ * @param key The header key
* @param value The header value
* @return The same builder instance
*/
@@ -126,8 +126,8 @@ public RestClientBuilder withHeader(String key, String value) {
/**
* @param providers One or more providers to apply. Providers typically implement
- * {@link MessageBodyReader} and/or {@link MessageBodyWriter}. If unspecified,
- * the {@link JsonBProvider} will be applied.
+ * {@link MessageBodyReader} and/or {@link MessageBodyWriter}. If unspecified,
+ * the {@link JsonBProvider} will be applied.
* @return The same builder instance
*/
public RestClientBuilder withProviders(Class>... providers) {
@@ -145,7 +145,7 @@ public @MicroProfileTest
is used on a test class.
* Currently this is tied to Testcontainers managing runtime build/deployment, but in a future version
@@ -90,8 +85,8 @@ private static void injectRestClients(Class> clazz) {
for (Field restClientField : restClientFields) {
if (!Modifier.isPublic(restClientField.getModifiers()) ||
- !Modifier.isStatic(restClientField.getModifiers()) ||
- Modifier.isFinal(restClientField.getModifiers())) {
+ !Modifier.isStatic(restClientField.getModifiers()) ||
+ Modifier.isFinal(restClientField.getModifiers())) {
throw new ExtensionConfigurationException("REST client field must be public, static, and non-final: " + restClientField);
}
RestClientBuilder rcBuilder = new RestClientBuilder();
@@ -137,10 +132,10 @@ private static void injectKafkaClients(Class> clazz) {
throw new ExtensionConfigurationException("Fields annotated with @KafkaProducerClient must be of the type " + KafkaProducer.getName());
}
if (!Modifier.isPublic(producerField.getModifiers()) ||
- !Modifier.isStatic(producerField.getModifiers()) ||
- Modifier.isFinal(producerField.getModifiers())) {
+ !Modifier.isStatic(producerField.getModifiers()) ||
+ Modifier.isFinal(producerField.getModifiers())) {
throw new ExtensionConfigurationException("The KafkaProducer field annotated with @KafkaProducerClient " +
- "must be public, static, and non-final: " + producerField);
+ "must be public, static, and non-final: " + producerField);
}
Properties properties = kafkaProcessor.getProducerProperties(producerField);
@@ -159,10 +154,10 @@ private static void injectKafkaClients(Class> clazz) {
throw new ExtensionConfigurationException("Fields annotated with @KafkaConsumerClient must be of the type " + KafkaConsumer.getName());
}
if (!Modifier.isPublic(consumerField.getModifiers()) ||
- !Modifier.isStatic(consumerField.getModifiers()) ||
- Modifier.isFinal(consumerField.getModifiers())) {
+ !Modifier.isStatic(consumerField.getModifiers()) ||
+ Modifier.isFinal(consumerField.getModifiers())) {
throw new ExtensionConfigurationException("The KafkaProducer field annotated with @KafkaConsumerClient " +
- "must be public, static, and non-final: " + consumerField);
+ "must be public, static, and non-final: " + consumerField);
}
Properties properties = kafkaProcessor.getConsumerProperties(consumerField);
@@ -182,7 +177,7 @@ private static void injectKafkaClients(Class> clazz) {
}
}
- @SuppressWarnings({ "unchecked", "rawtypes" })
+ @SuppressWarnings({"unchecked", "rawtypes"})
private static void configureRestAssured(ApplicationEnvironment config) {
if (!config.configureRestAssured())
return;
diff --git a/core/src/main/java/org/microshed/testing/jwt/JwtConfig.java b/core/src/main/java/org/microshed/testing/jwt/JwtConfig.java
index 0b522962..cdd839c4 100644
--- a/core/src/main/java/org/microshed/testing/jwt/JwtConfig.java
+++ b/core/src/main/java/org/microshed/testing/jwt/JwtConfig.java
@@ -18,21 +18,23 @@
*/
package org.microshed.testing.jwt;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.microshed.testing.jaxrs.RESTClient;
+
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
-import org.microshed.testing.jaxrs.RESTClient;
-
/**
* Used to annotate a REST Client to configure MicroProfile JWT settings
* that will be applied to all of its HTTP invocations.
* In order for this annotation to have any effect, the field must also
* be annotated with {@link RESTClient}.
*/
-@Target({ ElementType.FIELD })
+@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
+@ExtendWith(JwtConfigExtension.class)
public @interface JwtConfig {
public static final String DEFAULT_ISSUER = "http://testissuer.com";
@@ -46,7 +48,7 @@
* array of claims in the following format:
* key=value
* example: {"sub=fred", "upn=fred", "kid=123"}
- *
+ *
* For arrays, separate values with a comma.
* example: {"groups=red,green,admin", "sub=fred"}
*
diff --git a/core/src/main/java/org/microshed/testing/jwt/JwtConfigExtension.java b/core/src/main/java/org/microshed/testing/jwt/JwtConfigExtension.java
new file mode 100644
index 00000000..8f700fbd
--- /dev/null
+++ b/core/src/main/java/org/microshed/testing/jwt/JwtConfigExtension.java
@@ -0,0 +1,97 @@
+package org.microshed.testing.jwt;
+
+import org.junit.jupiter.api.extension.*;
+import org.microshed.testing.internal.InternalLogger;
+import org.microshed.testing.jupiter.MicroShedTestExtension;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+public class JwtConfigExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
+
+ private static final InternalLogger LOG = InternalLogger.get(JwtConfigExtension.class);
+
+ @Override
+ public void beforeTestExecution(ExtensionContext context) throws Exception {
+ configureJwt(context);
+ }
+
+ @Override
+ public void afterTestExecution(ExtensionContext context) throws Exception {
+ removeJwt(context);
+ }
+
+ private void configureJwt(ExtensionContext context) throws Exception {
+
+ // Check if the test method has the @JwtConfig annotation
+ Method testMethod = context.getTestMethod().orElse(null);
+ if (testMethod != null) {
+
+ // Check if RestAssured is being used
+ Class> restAssuredClass = tryLoad("io.restassured.RestAssured");
+ if (restAssuredClass == null) {
+ LOG.debug("RESTAssured not found!");
+ return;
+ }
+
+ LOG.debug("RESTAssured found!");
+
+ JwtConfig jwtConfig = testMethod.getAnnotation(JwtConfig.class);
+ if (jwtConfig != null) {
+ // Configure RestAssured with the values from @JwtConfig for each test method
+ LOG.info("JWTConfig on method: " + testMethod.getName());
+ // Get the RequestSpecBuilder class
+ Class> requestSpecBuilderClass = Class.forName("io.restassured.builder.RequestSpecBuilder");
+ // Create an instance of RequestSpecBuilder
+ Object requestSpecBuilder = requestSpecBuilderClass.newInstance();
+ // Get the requestSpecification field
+ Field requestSpecificationField = restAssuredClass.getDeclaredField("requestSpecification");
+ requestSpecificationField.setAccessible(true);
+
+ // Get the header method of RequestSpecBuilder
+ Method headerMethod = requestSpecBuilderClass.getDeclaredMethod("addHeader", String.class, String.class);
+
+ try {
+ String jwt = JwtBuilder.buildJwt(jwtConfig.subject(), jwtConfig.issuer(), jwtConfig.claims());
+ headerMethod.invoke(requestSpecBuilder, "Authorization", "Bearer " + jwt);
+ LOG.debug("Using provided JWT auth header: " + jwt);
+ } catch (Exception e) {
+ throw new ExtensionConfigurationException("Error while building JWT for method " + testMethod.getName() + " with JwtConfig: " + jwtConfig, e);
+ }
+
+ // Set the updated requestSpecification
+ requestSpecificationField.set(null, requestSpecBuilderClass.getMethod("build").invoke(requestSpecBuilder));
+ }
+ }
+ }
+
+ private void removeJwt(ExtensionContext context) throws Exception {
+ // Check if RestAssured is being used
+ Class> restAssuredClass = tryLoad("io.restassured.RestAssured");
+ if (restAssuredClass == null) {
+ LOG.debug("RESTAssured not found!");
+ return;
+ }
+
+ // Check if the test method has the @JwtConfig annotation
+ Method testMethod = context.getTestMethod().orElse(null);
+ if (testMethod != null) {
+
+ LOG.debug("Method was annotated with: " + testMethod.getName());
+ // Get the requestSpecification field
+ Field requestSpecificationField = restAssuredClass.getDeclaredField("requestSpecification");
+ requestSpecificationField.setAccessible(true);
+
+ // Removes all requestSpec
+ requestSpecificationField.set(null, null);
+ }
+ }
+
+ private static Class> tryLoad(String clazz) {
+ try {
+ return Class.forName(clazz, false, MicroShedTestExtension.class.getClassLoader());
+ } catch (ClassNotFoundException | LinkageError e) {
+ return null;
+ }
+ }
+}
diff --git a/docs/features/MP_JWT.md b/docs/features/MP_JWT.md
index 368c2a5c..b15b53e5 100644
--- a/docs/features/MP_JWT.md
+++ b/docs/features/MP_JWT.md
@@ -9,7 +9,7 @@ is a specification that standardizes OpenID Connect (OIDC) based JSON Web Tokens
## Sample MP JWT secured endpoint
-Typically MP JWT is used to secure REST endpoints using the `@javax.annotation.security.RolesAllowed` annotation at either the class or method level. Suppose we have a REST endpoint secured with MP JWT as follows:
+Typically MP JWT is used to secure REST endpoints using the `@jakarta.annotation.security.RolesAllowed` annotation at either the class or method level. Suppose we have a REST endpoint secured with MP JWT as follows:
```java
@Path("/data")
@@ -38,11 +38,12 @@ As the `@RolesAllowed` annotations imply, anyone can access the `GET /data/ping`
## Testing a MP JWT secured endpoint
-When MicroShed Testing will automatically generate and configure a pair of JWT secrets for the `ApplicationContainer` container. Then a test client may access these endpoints using the `@JwtConfig` annotation on injected REST clients as follows:
+### MicroShed RestClient
+MicroShed Testing will automatically generate and configure a pair of JWT secrets for the `ApplicationContainer` container when a test client is annotated with: `@JwtConfig` on the injected REST clients as follows:
```java
-import javax.ws.rs.ForbiddenException;
-import javax.ws.rs.NotAuthorizedException;
+import jakarta.ws.rs.ForbiddenException;
+import jakarta.ws.rs.NotAuthorizedException;
import org.junit.jupiter.api.Test;
import org.microshed.testing.jaxrs.RESTClient;
@@ -94,6 +95,44 @@ In the above code example, the `securedSvc` REST client will be generated with t
The `noJwtSecuredSvc` REST client will be generated with no JWT header, and the `misSecuredSvc` client will be generated with an invalid group claim. As a result, neither of these REST clients will be able to sucessfully access the `GET /data/users` secured endpoint, as expected.
+### RestAssured
+When using RestAssured, the `@JwtConfig` can be used on the test which will use RestAssured. MicroShed Testing will automatically generate and configure a pair of JWT secrets for the `ApplicationContainer` container. And injected a header in the RestAssured configuration, with: "Authorization: Bearer ":
+
+```java
+import jakarta.ws.rs.ForbiddenException;
+import jakarta.ws.rs.NotAuthorizedException;
+
+import org.junit.jupiter.api.Test;
+import org.microshed.testing.jaxrs.RESTClient;
+import org.microshed.testing.jupiter.MicroShedTest;
+import org.microshed.testing.jwt.JwtConfig;
+import org.microshed.testing.testcontainers.ApplicationContainer;
+import org.testcontainers.junit.jupiter.Container;
+
+@MicroShedTest
+public class SecuredSvcIT {
+
+ @Container
+ public static ApplicationContainer app = new ApplicationContainer()
+ .withAppContextRoot("/")
+ .withReadinessPath("/data/ping");
+
+ @Test
+ @JwtConfig(claims = {"groups=users"})
+ public void givenAPersonResourceWhenUsingRASecuredEndPointWithCorrectGroupThen200() {
+ given().when().get("app/data").then().statusCode(200);
+ }
+
+ @Test
+ @JwtConfig(claims = {"groups=wrong"})
+ public void givenAPersonResourceWhenUsingRASecuredEndPointWithWrongGroupThen403() {
+ given().when().get("app/data").then().statusCode(403);
+ }
+}
+```
+
+In the above code example, the `givenAPersonResourceWhenUsingRASecuredEndPointWithCorrectGroupThen200` test will be given an Authorization header, with the generated JWT key that has been configured on the `app` container, along with the group claim `users`. The result is that the `secureSvc` REST client can successfully access the `GET app/data` endpoint, which is restricted to clients in the `users` role.
+
## Learning resources
- [Tomitribe blog explaining MicroProfile JWT](https://www.tomitribe.com/blog/microprofile-json-web-token-jwt/)
diff --git a/docs/features/RestAssured.md b/docs/features/RestAssured.md
index f15a2a50..37a7193f 100644
--- a/docs/features/RestAssured.md
+++ b/docs/features/RestAssured.md
@@ -14,7 +14,7 @@ To enable REST Assured, add the following dependency to your pom.xml: