Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 52 additions & 36 deletions distribution/examples/openapi/jwt-auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<openapi location="secure-shop-api.yml" validateSecurity="yes"/>
```yaml
specs:
- openapi:
location: secure-shop-api.yml
validateSecurity: true
```

This makes Membrane automatically enforce the security rules defined in the spec.
Expand Down Expand Up @@ -52,45 +55,50 @@ paths:

---

## 2. Configure `proxies.xml`
## 2. Configure `apis.yaml`

**Token Server**:

(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.)
```xml
<!-- Token Server -->
<api port="2000" name="Token Server">
<request>
<template>{
"sub": "user@example.com",
"aud": "shop",
"scp": "inventory"
}</template>
<jwtSign>
<jwk location="jwk.json"/>
</jwtSign>
</request>
<return/>
</api>
```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
<!-- Protected API -->
<api port="2001" name="Protected API">
<!-- OpenAPI with scope enforcement -->
<openapi location="secure-shop-api.yml" validateSecurity="yes"/>

<!-- JWT verification -->
<jwtAuth expectedAud="shop">
<jwks>
<jwk location="jwk.json"/>
</jwks>
</jwtAuth>

<openapiValidator/>
</api>
```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: {}
```

---
Expand All @@ -115,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
Expand All @@ -131,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 <your-token>" http://localhost:2001/shop/v2/products
Expand Down
1 change: 1 addition & 0 deletions distribution/examples/openapi/jwt-auth/apis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ api:
- jwtSign:
jwk:
location: jwk.json
- return: {}

---

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package com.predic8.membrane.examples.withoutinternet.openapi;

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 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.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();
}

}
Loading