Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
Expand Up @@ -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());
Expand All @@ -44,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());
}
Expand Down Expand Up @@ -122,11 +125,10 @@ public Map<String, String> verify(Map<String, String> 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <i>user data provider</i> listing all user data in-place in the config file.
* @explanation <p>
Expand Down Expand Up @@ -60,7 +63,7 @@ public Map<String, String> verify(Map<String, String> 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());
}
Expand All @@ -74,22 +77,6 @@ public Map<String, String> verify(Map<String, String> 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<String, String> attributes = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -53,6 +59,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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"));

}
}
119 changes: 28 additions & 91 deletions distribution/examples/security/basic-auth/database/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<membrane-root>/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
<spring:bean name="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
<spring:property name="driverClassName" value="org.h2.Driver" />
<spring:property name="url" value="jdbc:h2:tcp://localhost/mem:userdata" />
<spring:property name="username" value="sa" />
<spring:property name="password" value="" />
</spring:bean>

<router>
<api port="2000">
<basicAuthentication>
<jdbcUserDataProvider datasource="dataSource" tableName="user" userColumnName="nickname" passwordColumnName="password" />
</basicAuthentication>
<target url="https://api.predic8.de"/>
</api>
</router>
```

This configuration sets up an `<api>` 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 `<basicAuthentication>` element:

```xml
<basicAuthentication>
<jdbcUserDataProvider datasource="jdbc:h2:mem:userdata" tableName="user" userColumnName="nickname" passwordColumnName="password" />
</basicAuthentication>
```

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
<spring:bean name="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
<spring:property name="driverClassName" value="org.h2.Driver" />
<spring:property name="url" value="jdbc:h2:tcp://localhost/mem:userdata" />
<spring:property name="username" value="sa" />
<spring:property name="password" value="" />
</spring:bean>
```

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;'
```

<HTML><HEAD><TITLE>Error</TITLE><META HTTP-EQUIV='Content-Type' CONTENT='text/html; charset=utf-8'></HEAD><BODY><H1>401 Unauthorized.</H1></BODY></HTML>
```
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:
Expand Down
31 changes: 31 additions & 0 deletions distribution/examples/security/basic-auth/database/apis.yaml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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');
1 change: 1 addition & 0 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
- Tutorial
- Documentation
- See JmxExporter
- refactor JdbcUserDataProvider

# 7.0.4

Expand Down
Loading