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
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* Copyright 2026 predic8 GmbH, www.predic8.com

Licensed 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 com.predic8.membrane.tutorials.security.jwt;

import com.predic8.membrane.tutorials.AbstractMembraneTutorialTest;

public abstract class AbstractSecurityJwtTutorialTest extends AbstractMembraneTutorialTest {

@Override
protected String getTutorialDir() {
return "security";
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/* Copyright 2026 predic8 GmbH, www.predic8.com

Licensed 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 com.predic8.membrane.tutorials.security.jwt;

import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;

public class IssuingAndValidatingJwtsTutorialTest extends AbstractSecurityJwtTutorialTest {

@Override
protected String getTutorialYaml() {
return "50-Issuing-and-Validating-JWTs.yaml";
}

@Test
void issuesTokenAndProtectsResource() {
// 1) Wrong credentials are rejected by HTTP Basic authentication.
// @formatter:off
given()
.auth().preemptive().basic("alice", "wrong")
.when()
.post("http://localhost:2000/token")
.then()
.statusCode(401);
// @formatter:on

// 2) The protected resource requires a token.
// @formatter:off
given()
.when()
.get("http://localhost:2000/resource")
.then()
.statusCode(400);
// @formatter:on

// 3) The authenticated user gets a token whose "sub" is their own username.
// @formatter:off
String accessToken =
given()
.auth().preemptive().basic("alice", "alice-secret")
.when()
.post("http://localhost:2000/token")
.then()
.statusCode(200)
.body("token_type", equalTo("bearer"))
.body("expires_in", equalTo(300))
.body("access_token", notNullValue())
.extract().path("access_token");

given()
.header("Authorization", "Bearer " + accessToken)
.when()
.get("http://localhost:2000/resource")
.then()
.statusCode(200)
.body("client", equalTo("alice"))
.body("scopes", equalTo("read write"));
// @formatter:on
}
}
5 changes: 5 additions & 0 deletions distribution/tutorials/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ Run and observe Membrane in production.
Expose Membrane as an MCP server for AI clients, inspect recent API traffic, and protect the MCP endpoint with an API key.


## [Security](security)

Issue signed JSON Web Tokens and protect an API by validating them. Covers the OAuth2 client-credentials flow, Bearer tokens, and signature/expiry/audience checks.


## [SOAP Web Services (Legacy)](soap)

If you need to integrate legacy SOAP Web Services, this tutorial provides examples and practical guidance.
Expand Down
44 changes: 44 additions & 0 deletions distribution/tutorials/security/40-Requesting-a-JWT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Requesting a JWT

No setup required, just `curl`. Uses the public Membrane demo at `https://api.predic8.de`.

Based on: <https://www.membrane-api.io/jwt/jwt-api-authentication-authorization-tutorial.html>

## 1. Request a token

```sh
curl -X POST https://api.predic8.de/demo/oauth2/token \
-u "my-client:my-secret" \
-d "grant_type=client_credentials"
```

```json
{"access_token":"eyJ0eXAiOiJKV1Qi...","token_type":"bearer","expires_in":300}
```

## 2. Inspect the token

Paste the `access_token` into <https://jwt.io>. A JWT has three parts `header.payload.signature`:

- `sub` — subject (the client id)
- `aud` — audience (the API this token is for)
- `scopes` — permissions granted
- `exp` — expiry (300s)

## 3. Call the protected resource

```sh
curl https://api.predic8.de/demo/resource \
-H "Authorization: Bearer eyJ0eXAiOiJKV1Qi..."
```

```json
{ "success": true, "user": "my-client", "scopes": "read write" }
```

Try it without the header — the request is rejected.

## Next

Continue with [50-Issuing-and-Validating-JWTs.yaml](50-Issuing-and-Validating-JWTs.yaml)
where Membrane issues and validates the tokens itself.
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# yaml-language-server: $schema=https://www.membrane-api.io/v7.2.4.json
#
# Tutorial: Issuing and Validating JWTs with Membrane
#
# Membrane acts as both token server and protected resource - no external server needed.
#
# 1.) Start Membrane:
# ./membrane.sh -c 20-Issuing-and-Validating-JWTs.yaml
#
# 2.) Request a token:
# curl -X POST localhost:2000/token -u "alice:alice-secret"
#
# {"access_token":"eyJ0eXAiOiJKV1Qi...","token_type":"bearer","expires_in":300}
#
# 3.) Paste the token into https://jwt.io - notice sub, aud, scopes, exp.
#
# 4.) Call the resource without a token (rejected):
# curl -i localhost:2000/resource
#
# 5.) Call it with the token:
# curl localhost:2000/resource -H "Authorization: Bearer eyJ0eXAiOiJKV1Qi..."
#
# {"client":"alice","scopes":"read write"}
#
# NOTE: jwk.json contains a demo key (private+public) - generate your own for production.
# jwk-public.json holds only the public parameters and is used by jwtAuth for validation.

api:
port: 2000
name: Token Server
path:
uri: /token
flow:
- basicAuthentication:
users:
- username: alice
password: alice-secret
- request:
# user() returns the authenticated username; jwtSign adds iat/exp/nbf.
- template:
contentType: application/json
src: |
{
"sub": ${user()},
"aud": "demo-resource",
"scopes": "read write"
}
- jwtSign:
property: token
jwk:
location: jwk.json
- template:
contentType: application/json
src: |
{
"access_token": ${property.token},
"token_type": "bearer",
"expires_in": 300
}
- return:
status: 200
---
api:
port: 2000
name: Protected Resource
path:
uri: /resource
flow:
- jwtAuth:
expectedAud: demo-resource
jwks:
jwks:
- location: jwk-public.json
- template:
contentType: application/json
src: |
{
"client": ${property.jwt.get("sub")},
"scopes": ${property.jwt.get("scopes")}
}
- return:
status: 200
24 changes: 24 additions & 0 deletions distribution/tutorials/security/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# JWT Authentication Tutorial

Learn how to protect an API with JSON Web Tokens (JWT). A client exchanges its
credentials for a short-lived, signed token and then uses that token as a Bearer
token on each request, while the gateway validates the signature, expiry and
audience on every call.

Each step is explained directly in the configuration file, which is also the
Membrane config you run. If possible, use an editor with YAML support such as
Visual Studio Code or IntelliJ IDEA.

The tutorials build on each other, from simple to advanced:

1. [40-Requesting-a-JWT.md](40-Requesting-a-JWT.md) — a `curl`-only walkthrough of the
hosted [Membrane demo](https://www.membrane-api.io/jwt/jwt-api-authentication-authorization-tutorial.html):
request a token via the OAuth2 Client Credentials flow and use it to call a
protected API. Nothing to run locally.
2. [50-Issuing-and-Validating-JWTs.yaml](50-Issuing-and-Validating-JWTs.yaml) — let Membrane itself
issue and validate the tokens, fully offline.

## Next Steps

Start with [40-Requesting-a-JWT.md](40-Requesting-a-JWT.md), then run
[50-Issuing-and-Validating-JWTs.yaml](50-Issuing-and-Validating-JWTs.yaml).
8 changes: 8 additions & 0 deletions distribution/tutorials/security/jwk-public.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"kty": "RSA",
"e": "AQAB",
"use": "sig",
"kid": "membrane",
"alg": "RS256",
"n": "jy8oj0NscvKCaawqk_f53p-iroACUxIz1ysSfwabydA22QDIbEtJ_Z7UNiW4dbdQUpzcsdUTG--Es7ECEAvxn3Q3jxMX7hU0n75s_KHcfm1yao508F913YuMmP2THtMmBiT0cFbxVHkJD_QvcwWPqTcjqcc4n7MYVeVZaLq0KJ-pz2Avb7a7fx0Ouk2pAgO4reFiR43T6wo_dyxcN92TjhbK3z8Qmox8kME-ZukNmDIAlm_UHzKupZM8cGotP3f42xeigUZXiVwDdAxQeZgV8mWNHVIYKdDYccQxHETu94jOPUQwXL-vVp8PHTeLqhY1oGcT2EJ6SEpH6e5NIvBxhQ"
}
14 changes: 14 additions & 0 deletions distribution/tutorials/security/jwk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"p": "wP1ITsKxAWOO03eywdOj73T5Po1OFXZlFgzf1CJaf8D3piuxS0C5JSHTKTO354_r9cg2WyQ3nEqJ6YScV3-NfW8xXbiyMr5Xokzn7YpuB9dtby0veEn4w7JHChH5lV2fwrjH2iL6IIOLrND9D_Dxoc3mmLMaie0mTW9-UHGOunk",
"kty": "RSA",
"q": "ve7893s-DMjWEFZSsUaj0wQh9LicfSDBlUzISR5ojYTDphQ2RRm1b3TLHHM2okP79BXrX-Jq6NDVVVsi0DJmkvNkivNMxSuqvf8Qo1G6YI_NgEzGCM9Nq1MZkwbrBQDHfiHQIxm4Td_OE3LUi7mR7Zye4aJIu0mx9W33jIaAbG0",
"d": "Hqi5ZZvJT_-vfxMXduGlRk8mVXkhhkoigZM-faabmyYTaHnrcIzahg0JYaLIEaSz9UyTURzP36502skvKOJ11W_cKa2r9RXjU8VBrwK1pPiohDqGvaWjJlIoQ-YgJ3yM6snk8V0chbr4_sqJknaBYXlmEIeRD1kY_-OBNpSr2PqtMj3j6v3XlhvLTRbVPA6aVIiPLimX5m-Xmo5jsSOj10CIJ3RkCNaegogUfX8V0eUPYl2OsBzGWEpzpBOKt9ej4pHcidejmQpnjkBUSlDMdz0jzgQYcLlCg6v62JI66yMtFBDNTbKo0q5niNN0BWSQZH8vEL_crCSfAiGT2wqsoQ",
"e": "AQAB",
"use": "sig",
"kid": "membrane",
"qi": "tbgI_1rCyz5Xb4jO004io5k6x42O4WghFdbL3-rvYcLdVVlUf6tdncvN2pgykUYqBHneZoedzILSZsmdwZypGcZXgYTPFxx3j8bqRwOPNAYK-BBzM-WqQSU5TtgkUAa83YsJy10cLQsHHaasOFX0nPT77jZQo_HeezzIAH9tgXY",
"dp": "hTwXeHCG_Rt7lljT62aujfmmvV2Wo9CaFzAKMw0Ih5x0HJ-bhgWIDK-edZqEA3TkBUoU5LVLQzZeof3wZaPkzc0_OqHxPIEWRTFtCRyBvB4pKhD67cO733cr_jLMqSb6zdb9-oYdQucuPcAGhcPlPbzFz3QPBVvZDqrDfMv5Kpk",
"alg": "RS256",
"dq": "qqQMokwXc2T87bCgmqTcirkryLIT5leHlJtnVkn7pSminZOLLonqeDh2Qxk__IkX1DPdREgnxQPaptU6cdLWVTBXJH9yebLBs_F1AUZsLFUGTD6trTySi1odn_qXK-eHU8sNNHvnGg_5FYAVdXNDqDcOh6lFrv6G4_noblhpCQ",
"n": "jy8oj0NscvKCaawqk_f53p-iroACUxIz1ysSfwabydA22QDIbEtJ_Z7UNiW4dbdQUpzcsdUTG--Es7ECEAvxn3Q3jxMX7hU0n75s_KHcfm1yao508F913YuMmP2THtMmBiT0cFbxVHkJD_QvcwWPqTcjqcc4n7MYVeVZaLq0KJ-pz2Avb7a7fx0Ouk2pAgO4reFiR43T6wo_dyxcN92TjhbK3z8Qmox8kME-ZukNmDIAlm_UHzKupZM8cGotP3f42xeigUZXiVwDdAxQeZgV8mWNHVIYKdDYccQxHETu94jOPUQwXL-vVp8PHTeLqhY1oGcT2EJ6SEpH6e5NIvBxhQ"
}
Loading