From 4a8ef20f83953eeff5d587d8a7f21b16479bb80a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6rdes?= Date: Tue, 6 Jan 2026 11:52:03 +0100 Subject: [PATCH 1/6] update example (WIP) --- .../registration/SecurityUtils.java | 6 +- .../security/basic-auth/database/README.md | 119 +++++------------- .../security/basic-auth/database/apis.yaml | 31 +++++ .../basic-auth/database/insert_users.sql | 8 ++ 4 files changed, 72 insertions(+), 92 deletions(-) create mode 100644 distribution/examples/security/basic-auth/database/apis.yaml create mode 100644 distribution/examples/security/basic-auth/database/insert_users.sql diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/registration/SecurityUtils.java b/core/src/main/java/com/predic8/membrane/core/interceptor/registration/SecurityUtils.java index 3fd5103ea2..7cf78a9072 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/registration/SecurityUtils.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/registration/SecurityUtils.java @@ -53,6 +53,10 @@ public static String createPasswdCompatibleHash(String password, String saltStri } public static String extractMagicString(String password) { - return password.split(Pattern.quote("$"))[1]; + try{ + return password.split(Pattern.quote("$"))[1]; + } catch (Exception e) { + throw new RuntimeException("Password must be in hash notation", e); + } } } diff --git a/distribution/examples/security/basic-auth/database/README.md b/distribution/examples/security/basic-auth/database/README.md index 76b69ffd27..4369aa8a57 100644 --- a/distribution/examples/security/basic-auth/database/README.md +++ b/distribution/examples/security/basic-auth/database/README.md @@ -2,109 +2,46 @@ This example walks you through setting up **HTTP Basic Authentication** for an API or Web application using a JDBC data source. +### Prerequisite -## Running the Example - -1. Navigate to the `examples/security/basic-auth/database` directory. +- **Docker installed:** -2. Download the latest `Platform-Independent Zip` from the [H2 Downloads](https://www.h2database.com/html/download-archive.html) page. + - If Docker is already installed, skip to the next step. + - Otherwise, install Docker from [https://docs.docker.com/get-started/get-docker/](https://docs.docker.com/get-started/get-docker/). -3. Unzip the downloaded file inside the current directory (resulting in an h2 folder). Install the `h2-*.jar` (database driver) from `./h2/bin` into the `/lib` directory. +--- -4. Execute `run_h2.sh` or `run_h2.bat`. This should open the web console in your primary browser (if not, press the H2 tray icon). Log in using `org.h2.Driver` as `Driver Class`, `jdbc:h2:mem:userdata` as `JDBC URL` and `sa` as username with an empty password. +## Running the Example -5. Create demo users: - - Once logged in, enter the following SQL into the text box, then press the run button above. - ```SQL - CREATE TABLE "user" ( - "nickname" VARCHAR(255) NOT NULL, - "password" VARCHAR(255) NOT NULL, - PRIMARY KEY ("nickname") - ); - - INSERT INTO "user" ("nickname", "password") - VALUES ('johnsmith123', 'pass123'), ('membrane', 'proxy'); +1. **Start a database container (e.g. postgres):** + ```shell + docker run --name postgres -e POSTGRES_USER=user -e POSTGRES_PASSWORD=password -p 5432:5432 -d postgres ``` -5. Execute `membrane.cmd` or `membrane.sh`. - -6. Open the URL http://localhost:2000 in your browser. - -7. Login with the username `membrane` and the password `gateway`. - - -## How it is done - -Let's examine the `proxies.xml` file. - -```xml - - - - - - - - - - - - - - - -``` - -This configuration sets up an `` component that directs calls from port `2000` to `https://api.predic8.de`, invoking the basicAuthentication-plugin for each request. +2. **Download JDBC Driver:** + - Download the PostgreSQL JDBC driver from the official + site: [https://jdbc.postgresql.org/download/](https://jdbc.postgresql.org/download/). + - Place it in the `lib` directory of your Membrane installation. -Let's take a closer look at the `` element: - -```xml - - - -``` - -We define a new `jdbcUserDataProvider` that fetches authentication details from a JDBC datasource. -The attributes of the provider element specify the table name and the columns for username and password. - -The datasource attribute requires a bean that implements the java DataSource interface, -in this example we use the following spring `bean` element definition to create one: - -```xml - - - - - - -``` - -Membrane includes the class `org.apache.commons.dbcp2.BasicDataSource`, we can use it as a DataSource implementor. -Now we simply define our connection data as we did for the web console, except for the url. -Because we are targeting an external database, we will have to specify the address within in the JDBC url `...tcp://localhost/mem...`. - ---- -When opening the URL `http://localhost:2000/`, membrane will respond with `401 Unauthorized`. - -```html -HTTP/1.1 401 Unauthorized -Content-Type: text/html;charset=utf-8 -WWW-Authenticate: Basic realm="Membrane Authentication" +3. Create demo users: + ```shell + docker exec -i postgres psql -U user -d postgres < ./insert_users.sql + ``` + test: + ```shell + docker exec -i postgres psql -U user -d postgres -c 'SELECT * FROM users;' + ``` -Error

401 Unauthorized.

-``` +4. Start Membrane: + ```shell + ./membrane.sh + ``` -The response will have the `WWW-Authenticate` header set. First the browser will ask you for your username and password. Then it will send the following request: +5. Open the URL http://localhost:2000 in your browser. -``` -GET / HTTP/1.1 -Host: localhost:2000 -Authorization: Basic bWVtYnJhbmU6bWVtYnJhbmU= -``` +6. Login with the username `membrane` and the password `gateway`. -Notice how the `Authorization` header is set with the hash of username and password. If the user is valid, membrane will let the request pass and the target host will respond. +7. Take a look at the [`apis.yaml`](apis.yaml) to see how it is configured. --- See: diff --git a/distribution/examples/security/basic-auth/database/apis.yaml b/distribution/examples/security/basic-auth/database/apis.yaml new file mode 100644 index 0000000000..00aef93da8 --- /dev/null +++ b/distribution/examples/security/basic-auth/database/apis.yaml @@ -0,0 +1,31 @@ +components: + dataSource: + bean: + class: org.apache.commons.dbcp2.BasicDataSource + properties: + - property: + name: driverClassName + value: org.postgresql.Driver + - property: + name: url + value: jdbc:postgresql://localhost:5432/postgres + - property: + name: username + value: user + - property: + name: password + value: password + +--- + +api: + port: 2000 + flow: + - basicAuthentication: + jdbcUserDataProvider: + datasource: '#/components/dataSource' + tableName: users + userColumnName: nickname + passwordColumnName: password + target: + url: https://apis.predic8.de \ No newline at end of file diff --git a/distribution/examples/security/basic-auth/database/insert_users.sql b/distribution/examples/security/basic-auth/database/insert_users.sql new file mode 100644 index 0000000000..ca3b97f1d0 --- /dev/null +++ b/distribution/examples/security/basic-auth/database/insert_users.sql @@ -0,0 +1,8 @@ +CREATE TABLE users ( + nickname VARCHAR(255) PRIMARY KEY, + password VARCHAR(255) NOT NULL +); + +INSERT INTO users (nickname, password) +VALUES ('johnsmith123', 'pass123'), + ('membrane', 'proxy'); From 4fa116d22f8c9d79f418724b6b0dcd9eb98fc57d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6rdes?= Date: Tue, 6 Jan 2026 14:13:26 +0100 Subject: [PATCH 2/6] code improvements & fixes --- .../session/JdbcUserDataProvider.java | 10 +++++---- .../session/StaticUserDataProvider.java | 21 ++++--------------- .../registration/SecurityUtils.java | 10 +++++++-- 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/JdbcUserDataProvider.java b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/JdbcUserDataProvider.java index 17349b8983..778597fce7 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/JdbcUserDataProvider.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/JdbcUserDataProvider.java @@ -27,6 +27,9 @@ import java.util.Map; import java.util.NoSuchElementException; +import static com.predic8.membrane.core.interceptor.registration.SecurityUtils.*; +import static com.predic8.membrane.core.interceptor.registration.SecurityUtils.isHashedPassword; + @MCElement(name = "jdbcUserDataProvider") public class JdbcUserDataProvider implements UserDataProvider { private static final Logger log = LoggerFactory.getLogger(JdbcUserDataProvider.class.getName()); @@ -122,11 +125,10 @@ public Map verify(Map postData) { log.error(e.getMessage()); } - if (result != null && result.size() > 0) { + if (result != null && !result.isEmpty()) { String passwordFromDB = result.get(getPasswordColumnName().toLowerCase()); - if (!SecurityUtils.isHashedPassword(password)) - password = SecurityUtils.createPasswdCompatibleHash(SecurityUtils.extractMagicString(passwordFromDB), password, SecurityUtils.extractSalt(passwordFromDB)); - + if (!isHashedPassword(password) && isHashedPassword(passwordFromDB)) + password = createPasswdCompatibleHash(extractMagicString(passwordFromDB), password, extractSalt(passwordFromDB)); if (username.equals(result.get(getUserColumnName().toLowerCase())) && password.equals(passwordFromDB)) return result; } diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/StaticUserDataProvider.java b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/StaticUserDataProvider.java index 6e26bf482f..e45883edeb 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/StaticUserDataProvider.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/StaticUserDataProvider.java @@ -20,6 +20,9 @@ import java.util.*; import java.util.regex.*; +import static com.predic8.membrane.core.interceptor.registration.SecurityUtils.createPasswdCompatibleHash; +import static com.predic8.membrane.core.interceptor.registration.SecurityUtils.isHashedPassword; + /** * @description A user data provider listing all user data in-place in the config file. * @explanation

@@ -60,7 +63,7 @@ public Map verify(Map postData) { String algo = userHashSplit[1]; String salt = userHashSplit[2]; try { - pw = createPasswdCompatibleHash(algo,postDataPassword,salt); + pw = createPasswdCompatibleHash(algo, postDataPassword, salt); } catch (Exception e) { throw new RuntimeException(e.getMessage()); } @@ -74,22 +77,6 @@ public Map verify(Map postData) { return userAttributes.getAttributes(); } - public boolean isHashedPassword(String postDataPassword) { - String[] split = postDataPassword.split(Pattern.quote("$")); - if (split.length != 4) - return false; - if (!split[0].isEmpty()) - return false; - if (split[3].length() < 20) - return false; - // Check if second part is a valid hex - return Pattern.matches("\\$([^$]+)\\$([^$]+)\\$.+", postDataPassword); - } - - private String createPasswdCompatibleHash(String algo, String password, String salt) { - return Crypt.crypt(password, "$" + algo + "$" + salt); - } - @MCElement(name="user", component =false, id="staticUserDataProvider-user") public static class User { final Map attributes = new HashMap<>(); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/registration/SecurityUtils.java b/core/src/main/java/com/predic8/membrane/core/interceptor/registration/SecurityUtils.java index 7cf78a9072..93042f6ef0 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/registration/SecurityUtils.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/registration/SecurityUtils.java @@ -25,9 +25,15 @@ public class SecurityUtils { private static final SecureRandom secureRandom = new SecureRandom(); public static boolean isHashedPassword(String postDataPassword) { - // TODO do a better check here String[] split = postDataPassword.split(Pattern.quote("$")); - return split.length == 4 && split[0].isEmpty() && split[3].length() >= 20; + if (split.length != 4) + return false; + if (!split[0].isEmpty()) + return false; + if (split[3].length() < 20) + return false; + // Check if the second part is a valid hex + return Pattern.matches("\\$([^$]+)\\$([^$]+)\\$.+", postDataPassword); } static String createPasswdCompatibleHash(String password) { From 98d0342704378a6d769fbc35cb5987c2f9c03c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6rdes?= Date: Tue, 6 Jan 2026 14:16:28 +0100 Subject: [PATCH 3/6] fix --- .../authentication/session/StaticUserDataProviderTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/authentication/session/StaticUserDataProviderTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/authentication/session/StaticUserDataProviderTest.java index fe46cac9cf..fac8e335df 100644 --- a/core/src/test/java/com/predic8/membrane/core/interceptor/authentication/session/StaticUserDataProviderTest.java +++ b/core/src/test/java/com/predic8/membrane/core/interceptor/authentication/session/StaticUserDataProviderTest.java @@ -16,14 +16,15 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import static com.predic8.membrane.core.interceptor.registration.SecurityUtils.isHashedPassword; + public class StaticUserDataProviderTest { @Test public void test() { - StaticUserDataProvider sudp = new StaticUserDataProvider(); Assertions.assertTrue( - sudp.isHashedPassword("$5$9d3c06e19528aebb$cZBA3E3SdoUvk865.WyPA5iNUEA7uwDlDX7D5Npkh8/")); + isHashedPassword("$5$9d3c06e19528aebb$cZBA3E3SdoUvk865.WyPA5iNUEA7uwDlDX7D5Npkh8/")); Assertions.assertTrue( - sudp.isHashedPassword("$5$99a6391616158b48$PqFPn9f/ojYdRcu.TVsdKeeRHKwbWApdEypn6wlUQn5")); + isHashedPassword("$5$99a6391616158b48$PqFPn9f/ojYdRcu.TVsdKeeRHKwbWApdEypn6wlUQn5")); } } From 5f243dcb0da416331c00fa4da08cc5bcdf229be5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6rdes?= Date: Tue, 6 Jan 2026 16:26:34 +0100 Subject: [PATCH 4/6] fix --- .../examples/security/basic-auth/database/insert_users.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distribution/examples/security/basic-auth/database/insert_users.sql b/distribution/examples/security/basic-auth/database/insert_users.sql index ca3b97f1d0..ee2eb29282 100644 --- a/distribution/examples/security/basic-auth/database/insert_users.sql +++ b/distribution/examples/security/basic-auth/database/insert_users.sql @@ -5,4 +5,4 @@ CREATE TABLE users ( INSERT INTO users (nickname, password) VALUES ('johnsmith123', 'pass123'), - ('membrane', 'proxy'); + ('membrane', 'gateway'); From 1f7fa79b70da4d5be9005bf21db3258bbc834202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6rdes?= Date: Tue, 6 Jan 2026 16:56:18 +0100 Subject: [PATCH 5/6] fix --- .../authentication/session/JdbcUserDataProvider.java | 4 ++-- docs/ROADMAP.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/JdbcUserDataProvider.java b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/JdbcUserDataProvider.java index 778597fce7..069331b612 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/JdbcUserDataProvider.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/JdbcUserDataProvider.java @@ -47,9 +47,9 @@ public void init(Router router) { getDatasourceIfNull(); try { - createTableIfNeeded(); + createTableIfNeeded(); // TODO does not work with postgres } catch (SQLException e) { - e.printStackTrace(); + e.printStackTrace(); // TODO refactor/improve log.error("Something went wrong at jdbcUserDataProvider table creation"); log.error(e.getMessage()); } diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 790018470d..2ffffda8f8 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -21,6 +21,7 @@ - Tutorial - Documentation - See JmxExporter +- refactor JdbcUserDataProvider # 7.0.4 From d3fee1fd7b86514f2e24812e6a91b28256d1d1fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6rdes?= Date: Wed, 7 Jan 2026 09:09:59 +0100 Subject: [PATCH 6/6] fixes --- .../session/JdbcUserDataProvider.java | 6 ++-- .../registration/SecurityUtils.java | 6 ++-- .../basic-auth/database/apis-preview.yaml | 33 ------------------- .../security/basic-auth/database/apis.yaml | 1 + 4 files changed, 7 insertions(+), 39 deletions(-) delete mode 100644 distribution/examples/security/basic-auth/database/apis-preview.yaml diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/JdbcUserDataProvider.java b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/JdbcUserDataProvider.java index 069331b612..0a1b1cbd8b 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/JdbcUserDataProvider.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/JdbcUserDataProvider.java @@ -47,11 +47,9 @@ public void init(Router router) { getDatasourceIfNull(); try { - createTableIfNeeded(); // TODO does not work with postgres + createTableIfNeeded(); // @todo: works with postgres but prints stacktrace and warning } catch (SQLException e) { - e.printStackTrace(); // TODO refactor/improve - log.error("Something went wrong at jdbcUserDataProvider table creation"); - log.error(e.getMessage()); + log.warn("Error creating table.",e); } } diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/registration/SecurityUtils.java b/core/src/main/java/com/predic8/membrane/core/interceptor/registration/SecurityUtils.java index 93042f6ef0..0a9b6856ea 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/registration/SecurityUtils.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/registration/SecurityUtils.java @@ -23,6 +23,8 @@ public class SecurityUtils { private static final SecureRandom secureRandom = new SecureRandom(); + public static final Pattern HEX_PASSWORD_PATTERN = Pattern.compile("\\$([^$]+)\\$([^$]+)\\$.+"); + public static final String $ = Pattern.quote("$"); public static boolean isHashedPassword(String postDataPassword) { String[] split = postDataPassword.split(Pattern.quote("$")); @@ -33,7 +35,7 @@ public static boolean isHashedPassword(String postDataPassword) { if (split[3].length() < 20) return false; // Check if the second part is a valid hex - return Pattern.matches("\\$([^$]+)\\$([^$]+)\\$.+", postDataPassword); + return HEX_PASSWORD_PATTERN.matcher(postDataPassword).matches(); } static String createPasswdCompatibleHash(String password) { @@ -47,7 +49,7 @@ static String createPasswdCompatibleHash(String password) { } public static String extractSalt(String password) { - return password.split(Pattern.quote("$"))[2]; + return password.split($)[2]; } public static String createPasswdCompatibleHash(String algo, String password, String salt) { diff --git a/distribution/examples/security/basic-auth/database/apis-preview.yaml b/distribution/examples/security/basic-auth/database/apis-preview.yaml deleted file mode 100644 index 12bb8e898a..0000000000 --- a/distribution/examples/security/basic-auth/database/apis-preview.yaml +++ /dev/null @@ -1,33 +0,0 @@ -# yaml-language-server: $schema=https://www.membrane-api.io/v7.0.5.json - -components: - dataSource: - bean: - class: org.apache.commons.dbcp2.BasicDataSource - properties: - - property: - name: driverClassName - value: org.h2.Driver - - property: - name: url - value: jdbc:h2:tcp://localhost/mem:userdata - - property: - name: username - value: sa - - property: - name: password - value: - ---- - -api: - port: 2000 - flow: - - basicAuthentication: - jdbcUserDataProvider: - datasource: '#/components/dataSource' - tableName: accounts - userColumnName: nickname - passwordColumnName: password - target: - url: https://api.predic8.de \ No newline at end of file diff --git a/distribution/examples/security/basic-auth/database/apis.yaml b/distribution/examples/security/basic-auth/database/apis.yaml index 00aef93da8..7f6fac0679 100644 --- a/distribution/examples/security/basic-auth/database/apis.yaml +++ b/distribution/examples/security/basic-auth/database/apis.yaml @@ -1,3 +1,4 @@ +# Works with most JDBC databases. With PostgreSQL, an exception may be logged, but the functionality is not affected. components: dataSource: bean: