Skip to content

Conversation

@predic8
Copy link
Member

@predic8 predic8 commented Feb 4, 2026

Summary by CodeRabbit

  • New Features

    • Added a Basic Authentication tutorial demonstrating plaintext and hashed-password logins.
    • New option to remove the Authorization header after authentication.
  • Improvements

    • Consolidated authentication model and utilities for safer init, input validation, clearer errors and improved logging.
    • Structured hash/salt handling for password processing and verification.
  • Tests

    • Large expansion of unit and integration tests covering providers, parsing, hashing and tutorials.
  • Docs

    • Roadmap updated noting provider refactors and header-removal behavior.

…ion workflow

- Introduced `AlgoSalt` record for streamlined password hashing and salt handling.
- Refactored `JdbcUserDataProvider` and `StaticUserDataProvider` to utilize enhanced hashing mechanisms.
- Added comprehensive unit tests for new password hashing, salt extraction, and validation logic in `SecurityUtilsTest`.
- Included a new tutorial class `BasicAuthenticationTutorialTest` for basic authentication scenarios with YAML configuration.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 4, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Refactors Basic auth and user-data providers: adds BasicAuthenticationUtil and AlgoSalt, introduces User and AbstractUserDataProvider, converts Static/File/Jdbc providers to extend the abstract provider, changes BasicAuthenticationInterceptor flow (optional Authorization header removal), expands hashing API, and adds extensive tests plus a Basic Auth tutorial.

Changes

Cohort / File(s) Summary
Basic auth interceptor
core/src/main/java/com/predic8/membrane/core/interceptor/authentication/BasicAuthenticationInterceptor.java
Rewrote request handling to use BasicAuthenticationUtil and credentials object, added removeAuthorizationHeader flag and accessors, conditional header-removal logic, and preserved 401/WWW-Authenticate denial flow.
Basic auth parsing util
core/src/main/java/com/predic8/membrane/core/util/security/BasicAuthenticationUtil.java
New utility exposing BasicCredentials record and getCredentials(Exchange) to decode/validate Authorization headers; throws on malformed or missing data.
Hashing & security utils
core/src/main/java/com/predic8/membrane/core/util/SecurityUtils.java
Added AlgoSalt record and factory, changed hashing API to createPasswdCompatibleHash(AlgoSalt, password), adjusted helpers and callers.
User model & abstract provider
core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/User.java, .../AbstractUserDataProvider.java
Added central User model and AbstractUserDataProvider implementing verify(...), filterPassword and getUsersByName() to consolidate provider verification flow.
Static provider
core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/StaticUserDataProvider.java
Now extends AbstractUserDataProvider, introduces UserConfig (subclass of User), removes its own verify and public usersByName APIs, and changes init/setUsers population behavior.
File provider
core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/FileUserDataProvider.java
Now extends AbstractUserDataProvider, relies on inherited user map, parses htpasswd lines using split(":", 2), and exposes htpasswdPath getter/setter.
JDBC provider
core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/JdbcUserDataProvider.java
Adds init(Router) wiring and router-aware DataSource resolution, passwordColumnName accessor, input sanitization, try-with-resources DB ops, and revised verify(...) returning attributes on success.
Basic auth tutorial & integration
distribution/tutorials/security/30-Basic-Authentication.yaml, distribution/src/test/java/.../BasicAuthenticationTutorialTest.java
Adds tutorial YAML with plaintext and hashed users and an integration test validating 401 for unauthenticated and 200 for valid credentials.
Tests: utilities & providers
core/src/test/.../SecurityUtilsTest.java, .../BasicAuthenticationUtilTest.java, .../StaticUserDataProviderTest.java, .../FileUserDataProviderTest.java, .../JdbcUserDataProviderTest.java, plus many updated tests
Adds broad unit/integration tests for hashing, Basic header parsing, and providers; updates tests to new User/UserConfig types.
Docs / Roadmap
docs/ROADMAP.md
Documents refactor of user providers and notes Authorization header removal as a breaking change.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant Interceptor as BasicAuthenticationInterceptor
  participant Util as BasicAuthenticationUtil
  participant Provider as UserDataProvider
  participant RouterDB as Router/DB

  Client->>Interceptor: HTTP request (Authorization header)
  Interceptor->>Util: getCredentials(exc)
  Util-->>Interceptor: BasicCredentials(username,password)
  Interceptor->>Provider: verify({username,password})
  Provider->>Provider: AlgoSalt.from(storedHash) / createPasswdCompatibleHash(AlgoSalt,password)
  alt provider uses DB
    Provider->>RouterDB: resolve DataSource / execute query
    RouterDB-->>Provider: query result
  end
  Provider-->>Interceptor: verified attributes (password removed) or failure
  alt verified
    Interceptor->>Interceptor: optionally remove Authorization header
    Interceptor-->>Client: forward request (next handler)
  else failure
    Interceptor-->>Client: 401 Unauthorized + WWW-Authenticate
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

7.x

Suggested reviewers

  • rrayst
  • christiangoerdes

Poem

🐇 I nibble salts and base64 delight,
I hop through providers in the quiet night,
Headers slip away if you ask me to,
Tests hum softly — hashes check true,
Hooray — a bunny-approved auth rewrite!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.03% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'BasicAuth: Tutorial Refactoring' is vague and does not clearly convey the scope of changes, which include significant refactoring of authentication infrastructure beyond just tutorial updates. Consider a more descriptive title that reflects the main changes, such as 'Refactor BasicAuthentication with UserDataProvider abstraction and header removal control' or similar, to better indicate the scope.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch basic-auth-tutorial-refactor

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/JdbcUserDataProvider.java (2)

102-121: ⚠️ Potential issue | 🔴 Critical

Resource leak: JDBC resources not properly managed with try-with-resources.

If an exception occurs after getConnection() but before the close() calls (e.g., during prepareStatement(), executeQuery(), or result processing), the Connection, PreparedStatement, and ResultSet will not be closed, leading to resource leaks and potential connection pool exhaustion.

🐛 Proposed fix using try-with-resources
-        Map<String, String> result = null;
-        try {
-            var con = datasource.getConnection();
-            var preparedStatement = con.prepareStatement(createGetUsersSql());
+        Map<String, String> result = new HashMap<>();
+        try (var con = datasource.getConnection();
+             var preparedStatement = con.prepareStatement(createGetUsersSql())) {
             preparedStatement.setString(1, username);
 
-            ResultSet rs = preparedStatement.executeQuery();
-            ResultSetMetaData rsmd = rs.getMetaData();
-
-            result = new HashMap<>();
-            while (rs.next()) for (int i = 1; i <= rsmd.getColumnCount(); i++)
-                result.put(rsmd.getColumnName(i).toLowerCase(), rs.getObject(i).toString());
-
-            rs.close();
-            preparedStatement.close();
-            con.close();
-
+            try (ResultSet rs = preparedStatement.executeQuery()) {
+                ResultSetMetaData rsmd = rs.getMetaData();
+                while (rs.next()) {
+                    for (int i = 1; i <= rsmd.getColumnCount(); i++) {
+                        Object value = rs.getObject(i);
+                        if (value != null) {
+                            result.put(rsmd.getColumnName(i).toLowerCase(), value.toString());
+                        }
+                    }
+                }
+            }
         } catch (SQLException e) {
             log.error(e.getMessage(),e);
         }

112-113: ⚠️ Potential issue | 🟠 Major

Potential NullPointerException when column value is null.

rs.getObject(i) can return null for nullable database columns. Calling .toString() on a null value will throw NullPointerException.

🤖 Fix all issues with AI agents
In
`@core/src/main/java/com/predic8/membrane/core/interceptor/authentication/BasicAuthenticationInterceptor.java`:
- Around line 129-132: setUsers currently does an unchecked cast to
StaticUserDataProvider which can throw ClassCastException; change it to mirror
getUsers by checking if userDataProvider instanceof StaticUserDataProvider first
and if so cast and call setUsers, otherwise create a new StaticUserDataProvider,
call setUsers on that new instance and replace the interceptor's provider via
setUserDataProvider(newProvider) (or alternatively throw a clear
IllegalStateException) so the behavior is safe and consistent with getUsers.

In
`@core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/FileUserDataProvider.java`:
- Around line 64-67: FileUserDataProvider currently splits userHash into
userHashSplit and indexes [1]/[2] which duplicates AlgoSalt parsing and can
throw ArrayIndexOutOfBoundsException; replace that manual parsing with
AlgoSalt.from(userHash) and pass the returned AlgoSalt into
createPasswdCompatibleHash(algosalt, postDataPassword), and add validation/error
handling for the case AlgoSalt.from(userHash) returns null or throws (log or
throw a clear authentication error) before assigning pw to avoid using malformed
hashes.

In
`@core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/StaticUserDataProvider.java`:
- Around line 77-82: AlgoSalt.from(String userHash) currently splits the input
and blindly accesses indices 1 and 2; add input validation to avoid
ArrayIndexOutOfBoundsException by first checking for null/empty userHash and
ensuring the split array length >= 3, and if the check fails throw an
IllegalArgumentException (or another appropriate checked/unchecked exception)
with a clear message including the bad input; keep the existing behavior when
valid and return new AlgoSalt(userHashSplit[1], userHashSplit[2]).

In
`@core/src/test/java/com/predic8/membrane/core/interceptor/authentication/session/StaticUserDataProviderTest.java`:
- Around line 108-121: The test uses an undersized hash so isHashedPassword()
returns false; replace the user hash in StaticUserDataProviderTest (the User
created in provider.setUsers) with a properly formatted SHA-512 string
"$6$<salt>$<hash>" where the final hash part is at least 20 characters so
isHashedPassword() detects it, then update the assertion around
provider.verify(postData) to reflect the correct hashed-password behavior
(either assert successful verification or assert the specific exception you
expect from createPasswdCompatibleHash); reference User, isHashedPassword(),
createPasswdCompatibleHash(), and provider.verify() when making the change.

In `@distribution/tutorials/security/30-Basic-Authentication.yaml`:
- Around line 5-13: Update the curl examples to avoid inline credentials so
secret scanners don't flag them: replace "curl -u alice:qwertz localhost:2000"
with "curl -u alice localhost:2000" and "curl -u carol:abc123 localhost:2000"
with "curl -u carol localhost:2000" (or equivalent using --user) so curl will
prompt for the password interactively; keep the surrounding examples and
comments intact.
🧹 Nitpick comments (6)
distribution/tutorials/security/30-Basic-Authentication.yaml (1)

19-32: Add an explicit “demo only” note for plaintext credentials.

A short warning helps prevent copy‑pasting plaintext passwords into real configs.

✏️ Suggested note
     - basicAuthentication:
         # Static in-memory user configuration.
         # Alternatives: JDBC-, LDAP-, or FileUserDataProvider.
+        # NOTE: Plaintext passwords are for tutorial/demo use only.
+        # In production, prefer hashed passwords for all users.
         users:
core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/StaticUserDataProvider.java (3)

51-52: Special "error" username handling appears to be test code in production.

The hardcoded "error" username that throws RuntimeException seems intended for testing error paths. Consider removing this from production code or documenting its purpose if it serves a legitimate use case.


70-72: Exception wrapping loses context.

Wrapping the exception with only e.getMessage() discards the original exception type and stack trace. Consider preserving the cause for better debugging.

♻️ Proposed fix
             } catch (Exception e) {
-                throw new RuntimeException(e.getMessage());
+                throw new RuntimeException("Failed to hash password", e);
             }

162-169: Duplicate population of usersByName in both setUsers() and init().

Both methods populate usersByName from the users list. If setUsers() is called before init(), the map will be populated twice with the same entries. While not incorrect, this is redundant. Consider having init() only populate if usersByName is empty, or document the expected usage pattern.

Also applies to: 179-183

core/src/main/java/com/predic8/membrane/core/util/SecurityUtils.java (1)

17-18: Consider moving AlgoSalt to SecurityUtils to avoid circular dependency.

SecurityUtils imports AlgoSalt from StaticUserDataProvider, while StaticUserDataProvider imports from SecurityUtils. This bidirectional coupling can complicate maintenance. Since AlgoSalt represents a password hashing concept (algorithm + salt), it would fit more naturally in SecurityUtils.

core/src/test/java/com/predic8/membrane/core/util/SecurityUtilsTest.java (1)

1-16: Copyright notice should precede package statement.

By Java convention, the copyright/license header should appear before the package statement.

♻️ Suggested reordering
+/* Copyright 2024 predic8 GmbH, www.predic8.com
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   ...
+   limitations under the License. */
+
 package com.predic8.membrane.core.util;
-
-/* Copyright 2024 predic8 GmbH, www.predic8.com
-...

- Introduced comprehensive unit tests for `FileUserDataProvider` covering initialization, malformed inputs, empty files, and user verification.
- Refactored user splitting logic to limit splits, fixing issues with passwords containing colons.
- Improved exception handling for invalid file paths and malformed user entries.
- Documented current behavior for handling colon usage in hashed passwords.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/JdbcUserDataProvider.java (2)

88-124: ⚠️ Potential issue | 🔴 Critical

Bind the username parameter before executing the query.

The prepared statement is created from SQL containing a ? placeholder, but the parameter is never bound. This will throw a SQLException when the query executes.

Fix: bind parameter before execute
 try (var con = datasource.getConnection();
      var preparedStatement = con.prepareStatement(createGetUsersSql())) {
+    preparedStatement.setString(1, username);
     try (var rs = preparedStatement.executeQuery()) {
         var rsmd = rs.getMetaData();

52-57: ⚠️ Potential issue | 🟠 Major

Implement sanitizeUserInputs() to prevent SQL injection via table and column identifiers.

Table and column names are concatenated directly into SQL queries at lines 67-72 (getCreateTableSql()) and 128 (createGetUsersSql()) without validation. Since these values are user-configurable via @MCAttribute setters (lines 146, 156, 166), malicious input can inject SQL. Though prepared statement parameters cannot be used for identifiers, validation is essential before interpolation.

Add allow-list validation using a pattern like ^[A-Za-z0-9_]+$ in sanitizeUserInputs():

Example implementation
 private void sanitizeUserInputs() {
     // As we can't use prepared statement parameters for table- and columnnames so we have to sanitize the user input here.
     // After this method it is assumed that user input is save to use
-
-    // TODO sanitize inputs here
+    if (tableName == null || userColumnName == null || passwordColumnName == null)
+        throw new IllegalArgumentException("Table and column names must be set.");
+
+    String identifierPattern = "^[A-Za-z0-9_]+$";
+    if (!tableName.matches(identifierPattern)
+            || !userColumnName.matches(identifierPattern)
+            || !passwordColumnName.matches(identifierPattern)) {
+        throw new IllegalArgumentException("Table/column names must be alphanumeric/underscore.");
+    }
 }
🤖 Fix all issues with AI agents
In
`@core/src/test/java/com/predic8/membrane/core/interceptor/authentication/session/FileUserDataProviderTest.java`:
- Around line 291-304: The test testPasswordWithColons in
FileUserDataProviderTest currently documents the bug but doesn't assert
behavior; after provider.init(router) and obtaining FileUserDataProvider.User
user via provider.getUsersByName().get("user"), add an assertion that
user.getPassword() (or the appropriate field on FileUserDataProvider.User)
equals the entire htpasswd hash string "$6$salt$hash:with:colons" to ensure
colons in the hash are preserved.
🧹 Nitpick comments (3)
core/src/main/java/com/predic8/membrane/core/util/SecurityUtils.java (1)

58-65: Tighten AlgoSalt.from validation for malformed hashes.
Right now it accepts strings without a leading $ or without a hash part, which can silently mis-parse into the wrong algo/salt. Consider aligning with isHashedPassword()’s expected 4‑part format.

🔧 Suggested validation tightening
 public record AlgoSalt(String algo, String salt) {
     public static AlgoSalt from(String userHash) {
         String[] userHashSplit = userHash.split(Pattern.quote("$"));
-        if (userHashSplit.length < 3) {
-            throw new IllegalArgumentException("Invalid hash format: %s at least 3 dollar separated parts required, got: %d".formatted(userHash, userHashSplit.length));
-        }
-        return new AlgoSalt(userHashSplit[1], userHashSplit[2]);
+        if (userHashSplit.length != 4 || !userHashSplit[0].isEmpty() || userHashSplit[3].isEmpty()) {
+            throw new IllegalArgumentException(
+                "Invalid hash format: %s (expected $<algo>$<salt>$<hash>)".formatted(userHash)
+            );
+        }
+        return new AlgoSalt(userHashSplit[1], userHashSplit[2]);
     }
 }
core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/StaticUserDataProvider.java (2)

68-73: Improve exception handling and logging context.

The exception handling could be more informative:

  1. The log message is empty, losing context about what failed
  2. Catching generic Exception is overly broad
  3. The re-thrown RuntimeException provides no additional context
♻️ Proposed improvement
         if (userAttributes.getPassword() != null && isHashedPassword(userAttributes.getPassword())) {
             try {
                 return createPasswdCompatibleHash(AlgoSalt.from(userAttributes.getPassword()), postDataPassword);
-            } catch (Exception e) {
-                log.debug("",e);
-                throw new RuntimeException(e);
+            } catch (IllegalArgumentException e) {
+                log.debug("Failed to hash password for user verification", e);
+                throw new RuntimeException("Password hashing failed", e);
             }
         }

173-181: Consider resolving the TODO to avoid redundant map population.

The TODO correctly identifies that usersByName may be populated twice if setUsers() is called before init(). While not incorrect, this is inefficient and could be easily fixed.

♻️ Option 1: Skip if already populated
     `@Override`
     public void init(Router router) {
+        if (!usersByName.isEmpty()) {
+            return;
+        }
         for (User user : users)
             getUsersByName().put(user.getUsername(), user);
     }
♻️ Option 2: Clear before populating (matches setUsers pattern)
     `@Override`
     public void init(Router router) {
+        getUsersByName().clear();
         for (User user : users)
             getUsersByName().put(user.getUsername(), user);
     }

…splitting, and add tests

- Improved sanitization logic in `JdbcUserDataProvider` for table/column names using regex validation.
- Fixed hash handling in `SecurityUtils` to enforce correct format and refine error messages.
- Resolved colon splitting issue in `FileUserDataProviderTest` for hashed passwords.
- Added extensive unit tests for `JdbcUserDataProvider` to verify initialization, input sanitization, and user verification scenarios.
- Refactored `StaticUserDataProvider` to prevent redundant map population in `init` method.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/JdbcUserDataProvider.java (1)

83-101: ⚠️ Potential issue | 🟠 Major

Fix exception that bypasses bean-factory fallback when registry lacks DataSource bean.

Calling get() on an empty Optional (returned by getBean()) throws NoSuchElementException, preventing the fallback lookup. Check for presence before calling get():

🛠️ Suggested fix
         if (router.getRegistry() != null) {
-            var ds = router.getRegistry().getBean(DataSource.class).get();
-            if (ds == null)
-                throw new RuntimeException("No datasource found - specifiy a DataSource bean in your Membrane configuration");
-            datasource = ds;
-            return;
+            var dsOpt = router.getRegistry().getBean(DataSource.class);
+            if (dsOpt.isPresent()) {
+                datasource = dsOpt.get();
+                return;
+            }
         }
🤖 Fix all issues with AI agents
In
`@core/src/main/java/com/predic8/membrane/core/interceptor/authentication/BasicAuthenticationInterceptor.java`:
- Around line 98-117: getAuthorizationHeaderDecoded currently assumes a non-null
Authorization header with a "Basic " prefix and will throw unchecked exceptions
for other schemes or malformed values; update it to first verify the header is
non-null and starts with the "Basic " scheme (case-insensitive) and that the
length is sufficient before trying to substring/decoding, and if the check fails
throw NoSuchElementException so callers like validUser() will treat it as
missing/invalid credentials; reference the methods hasNoAuthorizationHeader,
getAuthorizationHeaderDecoded and validUser to locate where to add the
scheme/length validation and exception throw.

In
`@core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/StaticUserDataProvider.java`:
- Around line 64-75: The getPassword method may attempt to hash a null incoming
password when stored userAttributes has a hashed password, causing
createPasswdCompatibleHash(...) to throw and return a 500; update getPassword to
first read postData.get("password") into postDataPassword and if
userAttributes.getPassword() is hashed, check postDataPassword for null/empty
and return null (or a sentinel that indicates auth failure) instead of calling
createPasswdCompatibleHash; keep the try/catch around
createPasswdCompatibleHash(AlgoSalt.from(...), postDataPassword) but only invoke
it when postDataPassword is non-null, and maintain existing logging/exception
behavior for true unexpected errors.

In
`@core/src/test/java/com/predic8/membrane/core/interceptor/authentication/session/FileUserDataProviderTest.java`:
- Around line 102-108: The test in FileUserDataProviderTest is duplicating the
valid htpasswd entry by combining Files.readString(htpasswdFile) with
StandardOpenOption.APPEND; instead of re-reading and re-appending the whole
file, append only the malformed lines to the existing file (e.g., call
Files.writeString on htpasswdFile with just the malformed_line_without_colon and
:nousername lines and StandardOpenOption.APPEND) so the valid entry is not
duplicated and the provider's duplicate-user behavior remains testable.
- Around line 118-121: The test testInitWithNonExistentFile uses a hardcoded
Unix path; make it OS-portable by creating a path inside a temporary directory
that is guaranteed not to exist and passing that to provider.setHtpasswdPath,
then assertThrows on provider.init(router); use Java's temporary folder or
Files.createTempDirectory/resolve to build the missing file path and delete it
if necessary so the test is deterministic (refer to testInitWithNonExistentFile,
provider.setHtpasswdPath(...), provider.init(router)).
- Around line 200-215: The verify() flow currently returns a User attributes map
that includes the password/hash (set via User.setPassword()), which
unnecessarily exposes sensitive data; modify the implementation so the map
returned by FileUserDataProvider.verify() (or User.getAttributes()) excludes the
"password" key—either by making User.getAttributes() return a copy with
"password" removed or by filtering/removing "password" just before verify()
returns—ensuring TokenProvider implementations (TOTPTokenProvider,
NumericTokenProvider, SMSTokenProvider) still receive needed custom attributes
like "secret", "sms", "token", and "username".

In
`@core/src/test/java/com/predic8/membrane/core/interceptor/authentication/session/JdbcUserDataProviderTest.java`:
- Around line 87-95: The test currently uses the router initialized in setUp()
which registers a DataSource, so provider.init(router) succeeds and verify(...)
fails for the wrong reason; change the test to use a router instance that does
not have any DataSource registered (e.g., create a fresh Router or clear the
datasource on the existing router) before calling provider.init(router) and then
assert that init (or init followed immediately by verify if you prefer) throws
RuntimeException; specifically update JdbcUserDataProviderTest to avoid using
the setUp() router, locate the provider.init(router) call and replace the router
with a router-without-datasource to trigger and assert the intended
RuntimeException.

…mline user attribute filtering, and improve test coverage

- Introduced `BasicAuthenticationUtil` for extracting and handling Basic authentication credentials.
- Refactored `StaticUserDataProvider` to use a common `filterPassword` method for secure user attribute handling.
- Enhanced `JdbcUserDataProvider` for safer bean initialization with null checks and better error messages.
- Expanded test coverage with detailed tests for `BasicAuthenticationUtil` and user attribute filtering.
- Improved robustness and maintainability of authentication components.
…entralize user attribute handling, and update tests
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/StaticUserDataProvider.java (1)

54-68: ⚠️ Potential issue | 🟡 Minor

Remove redundant getter method overrides.

getUsername(), getPassword(), and getAttributes() are identical to the parent User class implementations and lack annotations. These getters can be safely removed.

However, keep the setter method overrides (setUsername() and setPassword()) as they carry the @MCAttribute annotation required for configuration schema processing. The parent class methods lack these annotations, and the annotation framework only detects annotations on methods in the class being configured, not inherited methods.

core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/JdbcUserDataProvider.java (1)

114-148: ⚠️ Potential issue | 🟡 Minor

Avoid last-row override when multiple users share a username.

If the query returns multiple rows (no unique constraint on username), the current logic overwrites result and only validates the last row. Consider returning as soon as a matching row is found.

🔧 Proposed fix (return on first matching row)
-        Map<String, String> result = new HashMap<>();
+        Map<String, String> result = new HashMap<>();
         try (var con = datasource.getConnection();
              var preparedStatement = con.prepareStatement(createGetUsersSql())) {
             preparedStatement.setString(1, username);
             try (var rs = preparedStatement.executeQuery()) {
                 var rsmd = rs.getMetaData();
                 while (rs.next()) {
+                    result.clear();
                     for (int i = 1; i <= rsmd.getColumnCount(); i++) {
                         var value = rs.getObject(i);
                         if (value != null) {
                             result.put(rsmd.getColumnName(i).toLowerCase(), value.toString());
                         }
                     }
+                    if (username.equals(result.get(getUserColumnName().toLowerCase()))
+                            && verifyPassword(password, result.get(getPasswordColumnName().toLowerCase()))) {
+                        return result;
+                    }
                 }
             }
         } catch (SQLException e) {
             log.error(e.getMessage(), e);
         }
-
-
-        if (result != null && !result.isEmpty()) {
-            if (username.equals(result.get(getUserColumnName().toLowerCase())) && verifyPassword(password, result.get(getPasswordColumnName().toLowerCase()))) {
-                return result;
-            }
-        }
-
         throw new NoSuchElementException();
🤖 Fix all issues with AI agents
In
`@core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/AbstractUserDataProvider.java`:
- Around line 28-31: The filterPassword method in AbstractUserDataProvider
currently mutates the passed-in userAttributes (the User's internal attributes
map) by calling remove("password"); change it to make a defensive copy first
(e.g., new HashMap<>(userAttributes)), remove the "password" entry from the
copy, and return the copy so the original User attributes remain untouched;
update the filterPassword(Map<String,String> userAttributes) implementation to
return the modified copy rather than altering the input.

In
`@core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/JdbcUserDataProvider.java`:
- Around line 93-109: In JdbcUserDataProvider (the block that searches for a
DataSource and throws a RuntimeException), fix the typo in the exception message
by changing "specifiy" to "specify" in the RuntimeException text so it reads
e.g. "No datasource found - specify a DataSource bean in your Membrane
configuration"; update the message where the RuntimeException is thrown.

In
`@core/src/test/java/com/predic8/membrane/core/interceptor/authentication/BasicAuthenticationInterceptorTest.java`:
- Around line 35-38: The test currently registers two UserConfig entries with
the same username ("admin") so StaticUserDataProvider will overwrite the first
entry; change the second UserConfig to a distinct username (e.g., "adminHashed")
and update any assertions or calls to accept()/hashedPassword() in
BasicAuthenticationInterceptorTest to use the new username when validating the
hashed-password scenario (also apply the same username-split fix to the other
occurrence around lines 50-58); this ensures one UserConfig tests plaintext
credentials and the other tests hashed credentials without collision.
🧹 Nitpick comments (8)
core/src/main/java/com/predic8/membrane/core/util/security/BasicAuthenticationUtil.java (1)

11-12: Consider deriving BASIC_PREFIX_LENGTH from BASIC_PREFIX.

The hardcoded value 6 works correctly but could become a maintenance issue if BASIC_PREFIX is ever modified.

♻️ Suggested improvement
     private static final String BASIC_PREFIX = "Basic ";
-    private static final int BASIC_PREFIX_LENGTH = 6;
+    private static final int BASIC_PREFIX_LENGTH = BASIC_PREFIX.length();
core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/User.java (2)

33-35: Consider returning an unmodifiable view or defensive copy from getAttributes().

Returning the internal mutable map allows external code to modify the user's attributes directly, which could lead to unexpected side effects. If this is intentional for framework integration, consider documenting this behavior.

♻️ Option 1: Return unmodifiable view (if mutation is not needed)
     public Map<String, String> getAttributes() {
-        return attributes;
+        return Collections.unmodifiableMap(attributes);
     }
♻️ Option 2: Document the intentional mutability
+    /**
+     * Returns the mutable attributes map. Modifications to the returned map
+     * will affect this User instance.
+     */
     public Map<String, String> getAttributes() {
         return attributes;
     }

5-39: Consider adding equals(), hashCode(), and toString() methods.

The User class is used in maps (usersByName) and may be compared or logged. Without these methods, identity-based comparisons and default Object.toString() will be used, which may not be the desired behavior.

core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/AbstractUserDataProvider.java (2)

33-35: Exposing the internal usersByName map allows external modification.

Returning the internal mutable map permits callers to add, remove, or modify users directly. If this is intentional for configuration purposes, consider documenting it; otherwise, return an unmodifiable view.


12-21: Consider using a more specific exception type for authentication failures.

NoSuchElementException is typically used for iterator/collection operations. A custom exception or AuthenticationException would better communicate the authentication context and allow callers to handle auth failures distinctly from other errors.

core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/StaticUserDataProvider.java (1)

40-40: Unused logger declaration.

The log field is declared but never used in this class. Either remove it or add logging where appropriate.

♻️ Remove unused logger
-    private static final Logger log = LoggerFactory.getLogger(StaticUserDataProvider.class.getName());
core/src/test/java/com/predic8/membrane/core/util/security/BasicAuthenticationUtilTest.java (1)

311-319: Consider overriding toString() in BasicCredentials to mask the password.

The default record implementation exposes the password in plaintext via toString(). While the current codebase doesn't log the credentials object, this is a defensive programming practice—if credentials are ever included in logs or exception traces in the future, the password would be exposed. A masked toString() (e.g., BasicCredentials[username=alice, password=***]) prevents accidental exposure.

core/src/test/java/com/predic8/membrane/core/interceptor/authentication/session/FileUserDataProviderTest.java (1)

55-58: Use a per-user deterministic salt to strengthen test coverage.

A single fixed salt for all users can let per-user salt handling bugs slip through. Consider a deterministic per-user salt (no randomness) so multi-user tests exercise distinct salts.

♻️ Suggested change
-            String salt = "saltsalt"; // 8 characters minimum for SHA-512
+            // Deterministic per-user salt to exercise per-user hash handling
+            String salt = "salt%08x".formatted(username.hashCode());
             String hash = createPasswdCompatibleHash(new AlgoSalt("6", salt), plainPassword);

…ialization and error handling, and update tests
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/JdbcUserDataProvider.java`:
- Around line 136-138: In JdbcUserDataProvider (the catch block that currently
does "catch (SQLException e) { log.error(...); }" and then results in a
NoSuchElementException), stop masking the SQL error: instead of
swallowing/logging and letting a NoSuchElementException be thrown, rethrow an
unchecked exception that preserves the SQLException as the cause (e.g., throw
new IllegalStateException("Database error while loading user", e) or a custom
DataAccessException). Update the catch in the method (the SQLException handler)
to log and then throw the new unchecked exception so callers can distinguish DB
errors from "user not found."
🧹 Nitpick comments (1)
core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/StaticUserDataProvider.java (1)

95-101: Inconsistent field access: direct usersByName vs getUsersByName().

Line 97 accesses usersByName directly while line 99 uses getUsersByName(). For consistency with setUsers() (lines 88-90) and FileUserDataProvider, prefer using the getter throughout.

♻️ Proposed fix for consistency
     `@Override`
     public void init(Router router) {
-        if (usersByName.isEmpty()) {
+        if (getUsersByName().isEmpty()) {
             for (var user : users)
                 getUsersByName().put(user.getUsername(), user);
         }
     }

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@docs/ROADMAP.md`:
- Line 71: Update the sentence describing BasicAuthentication so it uses correct
subject-verb agreement and the proper HTTP header name: change
"BasicAuthentication interceptor remove the Authentication header from the
request." to a grammatically correct form such as "BasicAuthentication
interceptor removes the Authorization header from the request." — target the
phrase "BasicAuthentication interceptor" and the header mention "Authentication"
to replace with "Authorization".
🧹 Nitpick comments (1)
core/src/main/java/com/predic8/membrane/core/interceptor/authentication/BasicAuthenticationInterceptor.java (1)

143-158: Minor: Empty user list produces slightly awkward description.

If getUsers() returns an empty list, the method produces "Users<br/>Passwords are not shown." due to how sb.delete(sb.length() - 2, sb.length()) removes ": " instead of ", ". Consider adding an empty-list check for a cleaner output.

♻️ Optional improvement
         if (userDataProvider instanceof StaticUserDataProvider) {
-            sb.append("Users: ");
-            for (User user : ((StaticUserDataProvider) userDataProvider).getUsers()) {
-                sb.append(escapeHtml4(user.getUsername()));
-                sb.append(", ");
+            var users = ((StaticUserDataProvider) userDataProvider).getUsers();
+            if (users.isEmpty()) {
+                sb.append("No users configured.");
+            } else {
+                sb.append("Users: ");
+                for (User user : users) {
+                    sb.append(escapeHtml4(user.getUsername()));
+                    sb.append(", ");
+                }
+                sb.delete(sb.length() - 2, sb.length());
+                sb.append("<br/>Passwords are not shown.");
             }
-            sb.delete(sb.length() - 2, sb.length());
-            sb.append("<br/>Passwords are not shown.");
         }

@membrane-ci-server
Copy link

This pull request needs "/ok-to-test" from an authorized committer.

@predic8
Copy link
Member Author

predic8 commented Feb 5, 2026

/ok-to-test

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In
`@core/src/main/java/com/predic8/membrane/core/interceptor/authentication/BasicAuthenticationInterceptor.java`:
- Around line 144-158: The getLongDescription method in
BasicAuthenticationInterceptor appends "Users: " then always calls
sb.delete(...) which corrupts output when StaticUserDataProvider.getUsers() is
empty; update getLongDescription to check whether ((StaticUserDataProvider)
userDataProvider).getUsers() is non-empty (or track whether at least one
username was appended) before performing sb.delete(sb.length() - 2,
sb.length()), so the trailing comma removal only runs when a username was
actually appended; keep the rest of the logic (escapeHtml4(user.getUsername()),
appending "<br/>Passwords are not shown.") the same.
- Around line 94-101: In deny(Exchange exc) replace the call that overwrites the
response headers with one that appends the WWW-Authenticate header so the
Content-Type (and other headers) set by security(...).buildAndSetResponse(exc)
are preserved; specifically, after calling
security(...).buildAndSetResponse(exc) use the response's existing Header and
call its add(...) (or equivalent) to append "WWW-Authenticate" with the Basic
realm value instead of using setHeader(...).
- Around line 83-92: The validUser(Exchange exc) method currently only catches
NoSuchElementException from userDataProvider.verify(), but
BasicAuthenticationUtil.getCredentials(...) can throw IllegalArgumentException
for parsing errors causing 500s; update validUser to also catch
IllegalArgumentException (or catch Exception types covering parsing failures)
around the call to BasicAuthenticationUtil.getCredentials and treat those as
authentication failures (return false) so malformed/absent Authorization headers
produce 401s; keep the existing behavior for NoSuchElementException from
userDataProvider.verify() and ensure exc.setProperty(SECURITY_SCHEMES, ...) is
only executed when credentials parsing succeeds.

…toString` and add assertion to verify it's not exposed
…ility by using `var`, remove unnecessary password filtering, update test methods, and streamline imports.
@predic8 predic8 marked this pull request as ready for review February 10, 2026 19:55
@predic8 predic8 requested a review from rrayst February 10, 2026 19:55
* @description The user's phone number
*/
@MCAttribute
public void setSms(String value) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sms attribute is still being used (SMSTokenProvider).

* @description The user's shared TOTP secret (if used in combination with the {@link TOTPTokenProvider}).
*/
@MCAttribute
public void setSecret(String value) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The secret attribute is still being used (TOTPTokenProvider).

# Generate hashes with:
# openssl passwd -6 -salt k3jia "yourPassword"
# mkpasswd -m sha-512 -S k3jia "yourPassword"
password: "$6$k3jia$NlCL.JTXitlwZ4i67ELwWpqK4Co3a8xEsp5UBxlR2In7nJgKJFNywV4Bx6jB9Py5g69eNKyxveLIG3OMcyc8M1"
Copy link
Member

@rrayst rrayst Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"If you see $6$ in a modern web database, it usually means the developers haven't updated their security stack in about a decade."

@@ -1,44 +0,0 @@
# yaml-language-server: $schema=https://www.membrane-api.io/v7.0.6.json
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was this removed?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants