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..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 @@ -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()); @@ -44,11 +47,9 @@ public void init(Router router) { getDatasourceIfNull(); try { - createTableIfNeeded(); + createTableIfNeeded(); // @todo: works with postgres but prints stacktrace and warning } catch (SQLException e) { - e.printStackTrace(); - log.error("Something went wrong at jdbcUserDataProvider table creation"); - log.error(e.getMessage()); + log.warn("Error creating table.",e); } } @@ -122,11 +123,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 3fd5103ea2..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,11 +23,19 @@ 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) { - // 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 HEX_PASSWORD_PATTERN.matcher(postDataPassword).matches(); } static String createPasswdCompatibleHash(String password) { @@ -41,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) { @@ -53,6 +61,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/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")); } } 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-preview.yaml b/distribution/examples/security/basic-auth/database/apis.yaml similarity index 61% rename from distribution/examples/security/basic-auth/database/apis-preview.yaml rename to distribution/examples/security/basic-auth/database/apis.yaml index 12bb8e898a..7f6fac0679 100644 --- a/distribution/examples/security/basic-auth/database/apis-preview.yaml +++ b/distribution/examples/security/basic-auth/database/apis.yaml @@ -1,5 +1,4 @@ -# yaml-language-server: $schema=https://www.membrane-api.io/v7.0.5.json - +# Works with most JDBC databases. With PostgreSQL, an exception may be logged, but the functionality is not affected. components: dataSource: bean: @@ -7,16 +6,16 @@ components: properties: - property: name: driverClassName - value: org.h2.Driver + value: org.postgresql.Driver - property: name: url - value: jdbc:h2:tcp://localhost/mem:userdata + value: jdbc:postgresql://localhost:5432/postgres - property: name: username - value: sa + value: user - property: name: password - value: + value: password --- @@ -26,8 +25,8 @@ api: - basicAuthentication: jdbcUserDataProvider: datasource: '#/components/dataSource' - tableName: accounts + tableName: users userColumnName: nickname passwordColumnName: password target: - url: https://api.predic8.de \ No newline at end of file + 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..ee2eb29282 --- /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', 'gateway'); diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 1988b9c571..8e3032188e 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -22,6 +22,7 @@ - Tutorial - Documentation - See JmxExporter +- refactor JdbcUserDataProvider # 7.0.4