Skip to content

Commit a8f0da4

Browse files
Fix openapi/jwt-auth example and add missing example test (#2697)
* Update JWT auth example to use `apis.yaml` instead of `proxies.xml`. * add example test * fix import * sd
1 parent 35234ad commit a8f0da4

3 files changed

Lines changed: 173 additions & 36 deletions

File tree

distribution/examples/openapi/jwt-auth/README.md

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@ This example demonstrates how to secure an (Open)API using JWT authentication wi
1212

1313
## OpenAPI Specification with Scopes
1414

15-
In `proxies.xml`, reference the OpenAPI file:
15+
In `apis.yaml`, reference the OpenAPI file:
1616

17-
```xml
18-
<openapi location="secure-shop-api.yml" validateSecurity="yes"/>
17+
```yaml
18+
specs:
19+
- openapi:
20+
location: secure-shop-api.yml
21+
validateSecurity: true
1922
```
2023
2124
This makes Membrane automatically enforce the security rules defined in the spec.
@@ -52,45 +55,50 @@ paths:
5255

5356
---
5457

55-
## 2. Configure `proxies.xml`
58+
## 2. Configure `apis.yaml`
5659

5760
**Token Server**:
5861

5962
(Instead of using Membrane API Gateway as the token server, you can also integrate with Keycloak or Microsoft Entra ID. To avoid extra setup for this demo, we use tokens issued by Membrane itself.)
60-
```xml
61-
<!-- Token Server -->
62-
<api port="2000" name="Token Server">
63-
<request>
64-
<template>{
65-
"sub": "user@example.com",
66-
"aud": "shop",
67-
"scp": "inventory"
68-
}</template>
69-
<jwtSign>
70-
<jwk location="jwk.json"/>
71-
</jwtSign>
72-
</request>
73-
<return/>
74-
</api>
63+
```yaml
64+
# Token Server
65+
api:
66+
name: Token Server
67+
port: 2000
68+
flow:
69+
- request:
70+
- template:
71+
src: |
72+
{
73+
"sub": "user@example.com",
74+
"aud": "shop",
75+
"scp": "inventory"
76+
}
77+
- jwtSign:
78+
jwk:
79+
location: jwk.json
80+
- return: {}
7581
```
7682

7783
**Protected API**:
7884

79-
```xml
80-
<!-- Protected API -->
81-
<api port="2001" name="Protected API">
82-
<!-- OpenAPI with scope enforcement -->
83-
<openapi location="secure-shop-api.yml" validateSecurity="yes"/>
84-
85-
<!-- JWT verification -->
86-
<jwtAuth expectedAud="shop">
87-
<jwks>
88-
<jwk location="jwk.json"/>
89-
</jwks>
90-
</jwtAuth>
91-
92-
<openapiValidator/>
93-
</api>
85+
```yaml
86+
# Protected API
87+
api:
88+
name: Protected API
89+
port: 2001
90+
specs:
91+
- openapi:
92+
location: secure-shop-api.yml
93+
validateSecurity: true
94+
flow:
95+
- jwtAuth:
96+
expectedAud: shop
97+
jwks:
98+
jwks:
99+
- jwk:
100+
location: jwk.json
101+
- openapiValidator: {}
94102
```
95103

96104
---
@@ -115,7 +123,15 @@ membrane.cmd
115123

116124
---
117125

118-
## 4. Request a Token
126+
## 4. Try to access the Protected API
127+
128+
```cmd
129+
curl http://localhost:2001/shop/v2/products
130+
```
131+
132+
---
133+
134+
## 5. Request a Token
119135

120136
```cmd
121137
curl http://localhost:2000
@@ -131,7 +147,7 @@ The token includes `scp: "inventory"`, which satisfies the `GET /products` scope
131147

132148
---
133149

134-
## 5. Access the Protected API
150+
## 6. Access the Protected API
135151

136152
```bash
137153
curl -H "Authorization: Bearer <your-token>" http://localhost:2001/shop/v2/products

distribution/examples/openapi/jwt-auth/apis.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ api:
1414
- jwtSign:
1515
jwk:
1616
location: jwk.json
17+
- return: {}
1718

1819
---
1920

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package com.predic8.membrane.examples.withoutinternet.openapi;
2+
3+
import com.predic8.membrane.examples.util.DistributionExtractingTestcase;
4+
import com.predic8.membrane.examples.util.Process2;
5+
import org.jetbrains.annotations.NotNull;
6+
import org.junit.jupiter.api.AfterEach;
7+
import org.junit.jupiter.api.BeforeEach;
8+
import org.junit.jupiter.api.Test;
9+
10+
import java.io.File;
11+
import java.io.IOException;
12+
import java.text.MessageFormat;
13+
14+
import static com.predic8.membrane.core.util.OSUtil.isWindows;
15+
import static io.restassured.RestAssured.given;
16+
import static io.restassured.http.ContentType.JSON;
17+
import static java.nio.charset.StandardCharsets.UTF_8;
18+
import static org.hamcrest.CoreMatchers.equalTo;
19+
import static org.hamcrest.Matchers.*;
20+
21+
public class OpenApiJwtAuthExampleTest extends DistributionExtractingTestcase {
22+
23+
private Process2 process;
24+
25+
@Override
26+
protected String getExampleDirName() {
27+
return "openapi/jwt-auth";
28+
}
29+
30+
@BeforeEach
31+
void setup() throws Exception {
32+
runGenerateJwk(getExampleDir(getExampleDirName()));
33+
process = startServiceProxyScript();
34+
}
35+
36+
@AfterEach
37+
void stopMembrane() {
38+
if (process != null)
39+
process.killScript();
40+
}
41+
42+
private static String fetchJwt() {
43+
// @formatter:off
44+
return given()
45+
.when()
46+
.get("http://localhost:2000/")
47+
.then()
48+
.statusCode(200)
49+
.extract()
50+
.asString()
51+
.trim();
52+
// @formatter:on
53+
}
54+
55+
@Test
56+
void shouldListProducts_whenValidJwtProvided() {
57+
String jwt = fetchJwt();
58+
// @formatter:off
59+
given()
60+
.header("Authorization", "Bearer " + jwt)
61+
.when()
62+
.get("http://localhost:2001/shop/v2/products")
63+
.then()
64+
.statusCode(200)
65+
.contentType(JSON)
66+
.body("meta.count", greaterThan(0))
67+
.body("products", is(not(empty())))
68+
.body("products[0].id", notNullValue());
69+
// @formatter:on
70+
}
71+
72+
@Test
73+
void shouldReturnSecurityProblem_whenMissingJwt() {
74+
// @formatter:off
75+
given()
76+
.accept(JSON)
77+
.when()
78+
.get("http://localhost:2001/shop/v2/products")
79+
.then()
80+
.statusCode(400)
81+
.body("type", equalTo("https://membrane-api.io/problems/security"))
82+
.body("detail", containsString("Could not retrieve JWT"));
83+
// @formatter:on
84+
}
85+
86+
private static void runGenerateJwk(File dir) throws Exception {
87+
File jwk = new File(dir, "jwk.json");
88+
if (jwk.isFile() && jwk.length() > 0) return;
89+
90+
Process p = createJwkProcess(dir);
91+
int exit = p.waitFor();
92+
93+
if (exit != 0) {
94+
throw new IllegalStateException(MessageFormat.format("generate-jwk failed (exit {0}):\n{1}", exit, new String(p.getInputStream().readAllBytes(), UTF_8)));
95+
}
96+
if (!jwk.isFile() || jwk.length() == 0) {
97+
throw new IllegalStateException("generate-jwk did not create jwk.json in %s".formatted(dir.getAbsolutePath()));
98+
}
99+
}
100+
101+
private static @NotNull Process createJwkProcess(File dir) throws IOException {
102+
File sh = new File(dir, "membrane.sh");
103+
File bat = new File(dir, "membrane.bat");
104+
105+
ProcessBuilder pb;
106+
if (isWindows()) {
107+
String script = bat.exists() ? "membrane.bat" : "membrane";
108+
pb = new ProcessBuilder("cmd", "/c", script, "generate-jwk", "-o", "jwk.json");
109+
} else {
110+
String scriptPath = sh.exists() ? sh.getAbsolutePath() : new File(dir, "membrane").getAbsolutePath();
111+
pb = new ProcessBuilder("bash", scriptPath, "generate-jwk", "-o", "./jwk.json");
112+
}
113+
114+
pb.directory(dir);
115+
pb.redirectErrorStream(true);
116+
117+
return pb.start();
118+
}
119+
120+
}

0 commit comments

Comments
 (0)