From 7bbe54edbf06cc2dc06f0350b9080b59f06aaf3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6rdes?= Date: Tue, 27 Jan 2026 10:28:02 +0100 Subject: [PATCH 1/4] Update JWT auth example to use `apis.yaml` instead of `proxies.xml`. --- .../examples/openapi/jwt-auth/README.md | 76 ++++++++++--------- .../examples/openapi/jwt-auth/apis.yaml | 1 + 2 files changed, 43 insertions(+), 34 deletions(-) diff --git a/distribution/examples/openapi/jwt-auth/README.md b/distribution/examples/openapi/jwt-auth/README.md index 1f7481ca05..65cd57a2da 100644 --- a/distribution/examples/openapi/jwt-auth/README.md +++ b/distribution/examples/openapi/jwt-auth/README.md @@ -12,10 +12,13 @@ This example demonstrates how to secure an (Open)API using JWT authentication wi ## OpenAPI Specification with Scopes -In `proxies.xml`, reference the OpenAPI file: +In `apis.yaml`, reference the OpenAPI file: -```xml - +``` - - - - - - - - - +```yaml +# Token Server +api: + name: Token Server + port: 2000 + flow: + - request: + - template: + src: | + { + "sub": "user@example.com", + "aud": "shop", + "scp": "inventory" + } + - jwtSign: + jwk: + location: jwk.json + - return: {} ``` **Protected API**: -```xml - - - - - - - - - - - - - - +```yaml +# Protected API +api: + name: Protected API + port: 2001 + specs: + - openapi: + location: secure-shop-api.yml + validateSecurity: true + flow: + - jwtAuth: + expectedAud: shop + jwks: + jwks: + - jwk: + location: jwk.json + - openapiValidator: {} ``` --- diff --git a/distribution/examples/openapi/jwt-auth/apis.yaml b/distribution/examples/openapi/jwt-auth/apis.yaml index 8d2d3c48cf..070c08d66c 100644 --- a/distribution/examples/openapi/jwt-auth/apis.yaml +++ b/distribution/examples/openapi/jwt-auth/apis.yaml @@ -14,6 +14,7 @@ api: - jwtSign: jwk: location: jwk.json + - return: {} --- From ae95187a97e8bf9497b96ec0bd5dac66fb6daa5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6rdes?= Date: Tue, 27 Jan 2026 10:55:19 +0100 Subject: [PATCH 2/4] add example test --- .../examples/openapi/jwt-auth/README.md | 12 +- .../openapi/OpenApiJwtAuthExampleTest.java | 121 ++++++++++++++++++ 2 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/openapi/OpenApiJwtAuthExampleTest.java diff --git a/distribution/examples/openapi/jwt-auth/README.md b/distribution/examples/openapi/jwt-auth/README.md index 65cd57a2da..77320a459d 100644 --- a/distribution/examples/openapi/jwt-auth/README.md +++ b/distribution/examples/openapi/jwt-auth/README.md @@ -123,7 +123,15 @@ membrane.cmd --- -## 4. Request a Token +## 4. Try to access the Protected API + +```cmd +curl http://localhost:2001/shop/v2/products +``` + +--- + +## 5. Request a Token ```cmd curl http://localhost:2000 @@ -139,7 +147,7 @@ The token includes `scp: "inventory"`, which satisfies the `GET /products` scope --- -## 5. Access the Protected API +## 6. Access the Protected API ```bash curl -H "Authorization: Bearer " http://localhost:2001/shop/v2/products diff --git a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/openapi/OpenApiJwtAuthExampleTest.java b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/openapi/OpenApiJwtAuthExampleTest.java new file mode 100644 index 0000000000..acdf7e0c0b --- /dev/null +++ b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/openapi/OpenApiJwtAuthExampleTest.java @@ -0,0 +1,121 @@ +package com.predic8.membrane.examples.withoutinternet.openapi; + +import com.predic8.membrane.examples.util.AbstractSampleMembraneStartStopTestcase; +import com.predic8.membrane.examples.util.DistributionExtractingTestcase; +import com.predic8.membrane.examples.util.Process2; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.text.MessageFormat; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.cef.OS.isWindows; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.*; + +public class OpenApiJwtAuthExampleTest extends DistributionExtractingTestcase { + + private Process2 process; + + @Override + protected String getExampleDirName() { + return "openapi/jwt-auth"; + } + + @BeforeEach + void setup() throws Exception { + runGenerateJwk(getExampleDir(getExampleDirName())); + process = startServiceProxyScript(); + } + + @AfterEach + void stopMembrane() { + if (process != null) + process.killScript(); + } + + private static String fetchJwt() { + // @formatter:off + return given() + .when() + .get("http://localhost:2000/") + .then() + .statusCode(200) + .extract() + .asString() + .trim(); + // @formatter:on + } + + @Test + void shouldListProducts_whenValidJwtProvided() { + String jwt = fetchJwt(); + // @formatter:off + given() + .header("Authorization", "Bearer " + jwt) + .when() + .get("http://localhost:2001/shop/v2/products") + .then() + .statusCode(200) + .contentType(JSON) + .body("meta.count", greaterThan(0)) + .body("products", is(not(empty()))) + .body("products[0].id", notNullValue()); + // @formatter:on + } + + @Test + void shouldReturnSecurityProblem_whenMissingJwt() { + // @formatter:off + given() + .accept(JSON) + .when() + .get("http://localhost:2001/shop/v2/products") + .then() + .statusCode(400) + .body("type", equalTo("https://membrane-api.io/problems/security")) + .body("detail", containsString("Could not retrieve JWT")); + // @formatter:on + } + + private static void runGenerateJwk(File dir) throws Exception { + File jwk = new File(dir, "jwk.json"); + if (jwk.isFile() && jwk.length() > 0) return; + + Process p = createJwkProcess(dir); + int exit = p.waitFor(); + + if (exit != 0) { + throw new IllegalStateException(MessageFormat.format("generate-jwk failed (exit {0}):\n{1}", exit, new String(p.getInputStream().readAllBytes(), UTF_8))); + } + if (!jwk.isFile() || jwk.length() == 0) { + throw new IllegalStateException("generate-jwk did not create jwk.json in %s".formatted(dir.getAbsolutePath())); + } + } + + private static @NotNull Process createJwkProcess(File dir) throws IOException { + File sh = new File(dir, "membrane.sh"); + File bat = new File(dir, "membrane.bat"); + + ProcessBuilder pb; + if (isWindows()) { + String script = bat.exists() ? "membrane.bat" : "membrane"; + pb = new ProcessBuilder("cmd", "/c", script, "generate-jwk", "-o", "jwk.json"); + } else { + String scriptPath = sh.exists() ? sh.getAbsolutePath() : new File(dir, "membrane").getAbsolutePath(); + pb = new ProcessBuilder("bash", scriptPath, "generate-jwk", "-o", "./jwk.json"); + } + + pb.directory(dir); + pb.redirectErrorStream(true); + + return pb.start(); + } + +} From 1b4fa1c0bd3f37afcd36166b00fae3d8a4402f43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6rdes?= Date: Tue, 27 Jan 2026 11:03:16 +0100 Subject: [PATCH 3/4] fix import --- .../withoutinternet/openapi/OpenApiJwtAuthExampleTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/openapi/OpenApiJwtAuthExampleTest.java b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/openapi/OpenApiJwtAuthExampleTest.java index acdf7e0c0b..a275f83e42 100644 --- a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/openapi/OpenApiJwtAuthExampleTest.java +++ b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/openapi/OpenApiJwtAuthExampleTest.java @@ -1,6 +1,5 @@ package com.predic8.membrane.examples.withoutinternet.openapi; -import com.predic8.membrane.examples.util.AbstractSampleMembraneStartStopTestcase; import com.predic8.membrane.examples.util.DistributionExtractingTestcase; import com.predic8.membrane.examples.util.Process2; import org.jetbrains.annotations.NotNull; @@ -12,10 +11,10 @@ import java.io.IOException; import java.text.MessageFormat; +import static com.predic8.membrane.core.util.OSUtil.isWindows; import static io.restassured.RestAssured.given; import static io.restassured.http.ContentType.JSON; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.cef.OS.isWindows; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.Matchers.*; From 815ef50b49fbd8dcbb210050da3c43070c63f991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6rdes?= Date: Tue, 27 Jan 2026 11:04:01 +0100 Subject: [PATCH 4/4] sd --- distribution/examples/openapi/jwt-auth/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distribution/examples/openapi/jwt-auth/README.md b/distribution/examples/openapi/jwt-auth/README.md index 77320a459d..21083952d1 100644 --- a/distribution/examples/openapi/jwt-auth/README.md +++ b/distribution/examples/openapi/jwt-auth/README.md @@ -14,7 +14,7 @@ This example demonstrates how to secure an (Open)API using JWT authentication wi In `apis.yaml`, reference the OpenAPI file: -```