Repository: diennea/herddb
Pull request #801 (2023-02-03 16:53:42)

[security] Implement SASL username/password auth (prerequisite for JWT authentication)
### Motivation

* In order to implement JWT token authentication we need a auth mechanism that allows you to pass a plain token
* The idea will be eventually to carry the JWT token in the "password" field (like we do it in https://github.com/datastax/startlight-for-kafka)

### Modification
* implemented the PLAIN auth mechanism on both the client and the server
* add test cases
* implement a DigestModule in order to not require people use ZooKeeper implementation

### Note

Please note that MD5-DIGEST is recently considered not secure, adding a username/password mechanism doesn't add a security issue. 

To make clear that you license your contribution under 
the [Apache License Version 2.0, January 2004](http://www.apache.org/licenses/LICENSE-2.0)
you have to acknowledge this by using the following check-box.

 - [x] I hereby declare this contribution to be licenced under the [Apache License Version 2.0, January 2004](http://www.apache.org/licenses/LICENSE-2.0)



Patch:
diff --git a/herddb-core/src/main/java/herddb/client/ClientConfiguration.java b/herddb-core/src/main/java/herddb/client/ClientConfiguration.java
index f688d90b..95d482dc 100644
--- a/herddb-core/src/main/java/herddb/client/ClientConfiguration.java
+++ b/herddb-core/src/main/java/herddb/client/ClientConfiguration.java
@@ -67,6 +67,8 @@ public class ClientConfiguration {
 
     public static final String PROPERTY_MODE = "client.mode";
 
+    public static final String PROPERTY_AUTH_MECH = "client.auth.mech";
+
     public static final String PROPERTY_MODE_LOCAL = "local";
     public static final String PROPERTY_MODE_STANDALONE = "standalone";
     public static final String PROPERTY_MODE_CLUSTER = "cluster";
diff --git a/herddb-core/src/main/java/herddb/client/RoutedClientSideConnection.java b/herddb-core/src/main/java/herddb/client/RoutedClientSideConnection.java
index 208114c8..4b5ad7a4 100644
--- a/herddb-core/src/main/java/herddb/client/RoutedClientSideConnection.java
+++ b/herddb-core/src/main/java/herddb/client/RoutedClientSideConnection.java
@@ -110,11 +110,12 @@ public class RoutedClientSideConnection implements ChannelEventListener, ClientS
         if (ClientConfiguration.PROPERTY_MODE_LOCAL.equals(mode) && channel.isLocalChannel()) {
             return;
         }
+        String mech = connection.getClient().getConfiguration().getString(ClientConfiguration.PROPERTY_AUTH_MECH, SaslUtils.AUTH_DIGEST_MD5);
 
         SaslNettyClient saslNettyClient = new SaslNettyClient(
                 connection.getClient().getConfiguration().getString(ClientConfiguration.PROPERTY_CLIENT_USERNAME, ClientConfiguration.PROPERTY_CLIENT_USERNAME_DEFAULT),
                 connection.getClient().getConfiguration().getString(ClientConfiguration.PROPERTY_CLIENT_PASSWORD, ClientConfiguration.PROPERTY_CLIENT_PASSWORD_DEFAULT),
-                serverHostname
+                serverHostname, mech
         );
 
         byte[] firstToken = new byte[0];
@@ -124,13 +125,17 @@ public class RoutedClientSideConnection implements ChannelEventListener, ClientS
 
         long requestId = channel.generateRequestId();
         Pdu saslResponse = channel.sendMessageWithPduReply(requestId,
-                PduCodec.SaslTokenMessageRequest.write(requestId, SaslUtils.AUTH_DIGEST_MD5, firstToken), timeout);
+                PduCodec.SaslTokenMessageRequest.write(requestId, mech, firstToken), timeout);
         try {
             for (int i = 0; i < 100; i++) {
                 byte[] responseToSendToServer;
                 switch (saslResponse.type) {
                     case Pdu.TYPE_SASL_TOKEN_SERVER_RESPONSE:
                         byte[] token = PduCodec.SaslTokenServerResponse.readToken(saslResponse);
+                        if (saslNettyClient.isComplete()) {
+                            LOGGER.finest("SASL auth completed with success");
+                            return;
+                        }
                         responseToSendToServer = saslNettyClient.evaluateChallenge(token);
                         requestId = channel.generateRequestId();
                         saslResponse.close();
diff --git a/herddb-core/src/main/java/herddb/security/UserManager.java b/herddb-core/src/main/java/herddb/security/UserManager.java
index 4919e2d2..b1a27cf9 100644
--- a/herddb-core/src/main/java/herddb/security/UserManager.java
+++ b/herddb-core/src/main/java/herddb/security/UserManager.java
@@ -20,12 +20,37 @@
 
 package herddb.security;
 
+import java.io.IOException;
+import java.util.Objects;
+
 /**
- * Abstract over user management
+ * Abstract over user management.
  *
  * @author enrico.olivelli
  */
 public abstract class UserManager {
 
-    public abstract String getExpectedPassword(String username);
+    /**
+     * Return the expected password for a given user or null if the user doesn't exist.
+     * This method is needed only by the DIGEST-MD5 mechanism.
+     * @param username the username
+     * @return the password, or null if the user doesn't exist
+     */
+    public abstract String getExpectedPassword(String username) throws IOException;
+
+    /**
+     * Validate a username/password pair.
+     *
+     * @param username the username
+     * @param pwd the password
+     * @throws IOException in case of failure
+     */
+    public void authenticate(String username, char[] pwd) throws IOException {
+        String password = pwd != null ? new String(pwd) : null;
+        String expectedPassword = getExpectedPassword(username);
+        if (expectedPassword == null // user does not exist
+                || !Objects.equals(password, expectedPassword)) {
+            throw new IOException("Password doesn't match for user " + username + " (or user doesn't exist)");
+        }
+    }
 }
diff --git a/herddb-core/src/main/java/herddb/security/sasl/DigestLoginModule.java b/herddb-core/src/main/java/herddb/security/sasl/DigestLoginModule.java
new file mode 100644
index 00000000..77d35637
--- /dev/null
+++ b/herddb-core/src/main/java/herddb/security/sasl/DigestLoginModule.java
@@ -0,0 +1,60 @@
+/*
+ Licensed to Diennea S.r.l. under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. Diennea S.r.l. licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+
+ */
+package herddb.security.sasl;
+
+import java.util.Map;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.spi.LoginModule;
+
+public class DigestLoginModule  implements LoginModule {
+    private Subject subject;
+
+    public DigestLoginModule() {
+    }
+
+    public boolean abort() {
+        return false;
+    }
+
+    public boolean commit() {
+        return true;
+    }
+
+    public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
+        if (options.containsKey("username")) {
+            this.subject = subject;
+            String username = (String) options.get("username");
+            this.subject.getPublicCredentials().add(username);
+            String password = (String) options.get("password");
+            this.subject.getPrivateCredentials().add(password);
+        }
+
+    }
+
+    public boolean logout() {
+        return true;
+    }
+
+    public boolean login() {
+        return true;
+    }
+}
+
diff --git a/herddb-core/src/main/java/herddb/security/sasl/PlainSaslServerProvider.java b/herddb-core/src/main/java/herddb/security/sasl/PlainSaslServerProvider.java
new file mode 100644
index 00000000..64270df7
--- /dev/null
+++ b/herddb-core/src/main/java/herddb/security/sasl/PlainSaslServerProvider.java
@@ -0,0 +1,174 @@
+/*
+ Licensed to Diennea S.r.l. under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. Diennea S.r.l. licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+
+ */
+package herddb.security.sasl;
+
+import herddb.client.ClientConfiguration;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.Provider;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.sasl.AuthenticationException;
+import javax.security.sasl.SaslException;
+import javax.security.sasl.SaslServer;
+import javax.security.sasl.SaslServerFactory;
+
+public class PlainSaslServerProvider extends Provider {
+    private static final long serialVersionUID = 1L;
+
+    private static final Logger LOG = Logger
+            .getLogger(PlainSaslServerProvider.class.getName());
+
+    protected PlainSaslServerProvider() {
+        super("SASL/PLAIN Server Provider", 1.0, "SASL/PLAIN Server Provider for HerdDB");
+        put("SaslServerFactory." + SaslUtils.AUTH_PLAIN,
+                PlainSaslServerFactory.class.getName());
+    }
+
+
+    public static class PlainSaslServerFactory implements SaslServerFactory {
+        @Override
+        public SaslServer createSaslServer(String mechanism, String protocol, String serverName, Map<String, ?> props,
+                                           CallbackHandler callbackHandler) {
+            String[] mechanismNamesCompatibleWithPolicy = getMechanismNames(props);
+            for (int i = 0; i < mechanismNamesCompatibleWithPolicy.length; i++) {
+                if (mechanismNamesCompatibleWithPolicy[i].equals(mechanism)) {
+                    return new PlainSaslServer(callbackHandler);
+                }
+            }
+            return null;
+        }
+
+        @Override
+        public String[] getMechanismNames(Map<String, ?> props) {
+            return new String[]{SaslUtils.AUTH_PLAIN};
+        }
+    }
+
+    public static class PlainSaslServer implements SaslServer {
+
+
+        private boolean complete;
+        private final CallbackHandler callbackHandler;
+
+        private String username;
+
+        public PlainSaslServer(CallbackHandler callbackHandler) {
+            this.callbackHandler = callbackHandler;
+        }
+
+        @Override
+        public byte[] evaluateResponse(byte[] response) throws SaslException {
+            // see https://www.rfc-editor.org/rfc/rfc4616.html
+            if (response.length == 0 || response[0] != 0) {
+                throw new AuthenticationException("Invalid auth");
+            }
+            int endusername = -1;
+            for (int i = 1; i < response.length; i++) {
+                if (response[i] == 0) {
+                    endusername = i;
+                    break;
+                }
+            }
+            if (endusername < 0) {
+                throw new AuthenticationException("Invalid auth");
+            }
+            this.username = new String(response, 1, endusername - 1,
+                    StandardCharsets.UTF_8);
+            String password = new String(response, endusername + 1, response.length - endusername - 1,
+                    StandardCharsets.UTF_8);
+
+            NameCallback nameCallback = new NameCallback("username", ClientConfiguration.PROPERTY_CLIENT_USERNAME_DEFAULT);
+            nameCallback.setName(username);
+            PasswordCallback passwordCallback = new PasswordCallback("password", false);
+            passwordCallback.setPassword(password.toCharArray());
+            Callback[] callbacks = new Callback[] { nameCallback, passwordCallback };
+
+
+            try {
+                callbackHandler.handle(callbacks);
+            } catch (IOException | UnsupportedCallbackException error) {
+                LOG.log(Level.SEVERE, "Error", error);
+                throw new AuthenticationException("Error while performing SASL PLAIN authentication " + error, error);
+            }
+
+            complete = true;
+
+            return new byte[0];
+        }
+
+        @Override
+        public String getAuthorizationID() {
+            if (!complete) {
+                throw new IllegalStateException("Authentication exchange has not completed");
+            }
+            return username;
+        }
+
+        @Override
+        public String getMechanismName() {
+            return SaslUtils.AUTH_PLAIN;
+        }
+
+        @Override
+        public Object getNegotiatedProperty(String propName) {
+            if (!complete) {
+                throw new IllegalStateException("Authentication exchange has not completed");
+            }
+            return null;
+        }
+
+        @Override
+        public boolean isComplete() {
+            return complete;
+        }
+
+        @Override
+        public byte[] unwrap(byte[] incoming, int offset, int len) {
+            if (!complete) {
+                throw new IllegalStateException("Authentication exchange has not completed");
+            }
+            return Arrays.copyOfRange(incoming, offset, offset + len);
+        }
+
+        @Override
+        public byte[] wrap(byte[] outgoing, int offset, int len) {
+            if (!complete) {
+                throw new IllegalStateException("Authentication exchange has not completed");
+            }
+            return Arrays.copyOfRange(outgoing, offset, offset + len);
+        }
+
+        @Override
+        public void dispose() {
+            complete = false;
+            username = null;
+        }
+
+    }
+
+}
diff --git a/herddb-core/src/main/java/herddb/security/sasl/SaslNettyClient.java b/herddb-core/src/main/java/herddb/security/sasl/SaslNettyClient.java
index d1b4fc9c..3f93184b 100644
--- a/herddb-core/src/main/java/herddb/security/sasl/SaslNettyClient.java
+++ b/herddb-core/src/main/java/herddb/security/sasl/SaslNettyClient.java
@@ -63,16 +63,26 @@ public class SaslNettyClient {
     /**
      * Create a SaslNettyClient for authentication with servers.
      */
-    public SaslNettyClient(String username, String password, String serverHostname) throws Exception {
+    public SaslNettyClient(String username, String password, String serverHostname, String mech) throws Exception {
         String serverPrincipal = "herddb/" + serverHostname;
         clientSubject = loginClient();
 
         if (clientSubject == null) {
-            LOG.log(Level.FINEST, "Using plain SASL/DIGEST-MD5 auth to connect to " + serverHostname);
-            saslClient = Sasl.createSaslClient(
-                    new String[]{SaslUtils.AUTH_DIGEST_MD5}, null, null,
-                    SaslUtils.DEFAULT_REALM, SaslUtils.getSaslProps(),
-                    new SaslClientCallbackHandler(username, password.toCharArray()));
+            if (mech == null || mech.equals(SaslUtils.AUTH_DIGEST_MD5)) {
+                LOG.log(Level.FINEST, "Using plain SASL/DIGEST-MD5 auth to connect to " + serverHostname);
+                saslClient = Sasl.createSaslClient(
+                        new String[]{SaslUtils.AUTH_DIGEST_MD5}, null, null,
+                        SaslUtils.DEFAULT_REALM, SaslUtils.getSaslProps(false),
+                        new SaslClientCallbackHandler(username, password.toCharArray()));
+            } else if (mech.equals(SaslUtils.AUTH_PLAIN)) {
+                    LOG.log(Level.FINEST, "Using plain SASL/PLAIN auth to connect to " + serverHostname);
+                    saslClient = Sasl.createSaslClient(
+                            new String[]{SaslUtils.AUTH_PLAIN}, null, null,
+                            SaslUtils.DEFAULT_REALM, SaslUtils.getSaslProps(true),
+                            new SaslClientCallbackHandler(username, password.toCharArray()));
+            } else {
+                throw new IOException("Unsupported mechanism " + mech);
+            }
         } else if (clientSubject.getPrincipals().isEmpty()) {
             LOG.log(Level.FINEST, "Using JAAS/SASL/DIGEST-MD5 auth to connect to " + serverPrincipal);
             String[] mechs = {"DIGEST-MD5"};
diff --git a/herddb-core/src/main/java/herddb/security/sasl/SaslNettyServer.java b/herddb-core/src/main/java/herddb/security/sasl/SaslNettyServer.java
index 0e1f9bb3..312dcb92 100644
--- a/herddb-core/src/main/java/herddb/security/sasl/SaslNettyServer.java
+++ b/herddb-core/src/main/java/herddb/security/sasl/SaslNettyServer.java
@@ -25,6 +25,7 @@ import java.io.IOException;
 import java.security.Principal;
 import java.security.PrivilegedActionException;
 import java.security.PrivilegedExceptionAction;
+import java.security.Security;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.logging.Level;
@@ -77,11 +78,24 @@ public class SaslNettyServer {
 
     }
 
+    private CallbackHandler buildCallbackHandler(String mech) throws IOException {
+        switch (mech + "") {
+            case SaslUtils.AUTH_DIGEST_MD5:
+                return new SaslDigestCallbackHandler();
+            case SaslUtils.AUTH_PLAIN:
+                return new SaslPlainCallbackHandler();
+            default:
+                throw new IOException("Unsupported mechanism " + mech);
+        }
+
+    }
+
     private SaslServer createSaslServer(final String mech, final Subject subject) throws IOException {
         if (subject == null) {
-            SaslDigestCallbackHandler ch = new SaslNettyServer.SaslDigestCallbackHandler();
+            CallbackHandler ch = buildCallbackHandler(mech);
+            boolean allowPlain = SaslUtils.AUTH_PLAIN.equals(mech);
             return Sasl.createSaslServer(mech, null,
-                    SaslUtils.DEFAULT_REALM, SaslUtils.getSaslProps(), ch);
+                    SaslUtils.DEFAULT_REALM, SaslUtils.getSaslProps(allowPlain), ch);
         } else {
             SaslServerCallbackHandler callbackHandler = new SaslServerCallbackHandler(Configuration.getConfiguration());
             // server is using a JAAS-authenticated subject: determine service principal name and hostname from zk server's subject.
@@ -212,14 +226,44 @@ public class SaslNettyServer {
         return saslServer.getAuthorizationID();
     }
 
+    private class SaslPlainCallbackHandler implements CallbackHandler {
+        @Override
+        public void handle(Callback[] callbacks) throws IOException,
+                UnsupportedCallbackException {
+            NameCallback nc = null;
+            PasswordCallback pc = null;
+
+            for (Callback callback : callbacks) {
+                if (callback instanceof NameCallback) {
+                    nc = (NameCallback) callback;
+                } else if (callback instanceof PasswordCallback) {
+                    pc = (PasswordCallback) callback;
+                } else if (callback instanceof RealmCallback) {
+                    continue; // realm is ignored
+                } else {
+                    throw new UnsupportedCallbackException(callback,
+                            "handle: Unrecognized SASL PLAIN Callback");
+                }
+            }
+
+            if (nc == null || pc == null) {
+                throw new IOException("Missing either username or password");
+            }
+
+            try {
+                server.getUserManager().authenticate(nc.getName(), pc.getPassword());
+                LOG.log(Level.INFO, "Authenticated user {0} using PLAIN", nc.getName());
+            } catch (Exception err) {
+                throw new IOException("Failed authentication for username " + nc.getName());
+            }
+        }
+    }
+
     /**
      * CallbackHandler for SASL DIGEST-MD5 mechanism
      */
     private class SaslDigestCallbackHandler implements CallbackHandler {
 
-        public SaslDigestCallbackHandler() {
-        }
-
         @Override
         public void handle(Callback[] callbacks) throws IOException,
                 UnsupportedCallbackException {
@@ -376,4 +420,14 @@ public class SaslNettyServer {
         }
 
     }
+
+
+    public static void initialize() {
+        Security.addProvider(new PlainSaslServerProvider());
+    }
+
+
+    static {
+        initialize();
+    }
 }
diff --git a/herddb-core/src/main/java/herddb/security/sasl/SaslUtils.java b/herddb-core/src/main/java/herddb/security/sasl/SaslUtils.java
index 82dac379..e1169bf5 100644
--- a/herddb-core/src/main/java/herddb/security/sasl/SaslUtils.java
+++ b/herddb-core/src/main/java/herddb/security/sasl/SaslUtils.java
@@ -33,11 +33,16 @@ import javax.security.sasl.Sasl;
 public class SaslUtils {
 
     public static final String AUTH_DIGEST_MD5 = "DIGEST-MD5";
+
+    public static final String AUTH_PLAIN = "PLAIN";
+
+
     public static final String DEFAULT_REALM = "default";
 
-    static Map<String, String> getSaslProps() {
-        Map<String, String> props = new HashMap<>();
-        props.put(Sasl.POLICY_NOPLAINTEXT, "true");
+
+    static Map<String, String> getSaslProps(boolean allowPlain) {
+        Map<String, String> props = new HashMap<String, String>();
+        props.put(Sasl.POLICY_NOPLAINTEXT, !allowPlain + "");
         return props;
     }
 
diff --git a/herddb-core/src/main/java/herddb/server/ServerSideConnectionPeer.java b/herddb-core/src/main/java/herddb/server/ServerSideConnectionPeer.java
index d95a8130..aeaa3b38 100644
--- a/herddb-core/src/main/java/herddb/server/ServerSideConnectionPeer.java
+++ b/herddb-core/src/main/java/herddb/server/ServerSideConnectionPeer.java
@@ -1108,6 +1108,12 @@ public class ServerSideConnectionPeer implements ServerSideConnection, ChannelEv
                 saslNettyServer = new SaslNettyServer(server, mech);
             }
             byte[] responseToken = saslNettyServer.response(token);
+            if (saslNettyServer.isComplete()) {
+                username = saslNettyServer.getUserName();
+                authenticated = true;
+                LOGGER.log(Level.INFO, "client {0} connected as {1}", new Object[]{this.channel.getRemoteAddress(), username});
+                saslNettyServer = null;
+            }
             ByteBuf tokenChallenge = PduCodec.SaslTokenServerResponse.write(message.messageId, responseToken);
             channel.sendReplyMessage(message.messageId, tokenChallenge);
         } catch (Exception err) {


Test patch:
diff --git a/herddb-core/src/test/java/herddb/server/security/JAASKerberosTest.java b/herddb-core/src/test/java/herddb/server/security/JAASKerberosTest.java
index 7eb28264..bfac1567 100644
--- a/herddb-core/src/test/java/herddb/server/security/JAASKerberosTest.java
+++ b/herddb-core/src/test/java/herddb/server/security/JAASKerberosTest.java
@@ -36,6 +36,7 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.Map;
 import java.util.Properties;
+import javax.security.auth.login.Configuration;
 import org.apache.hadoop.minikdc.MiniKdc;
 import org.junit.After;
 import org.junit.Assert;
@@ -162,6 +163,7 @@ public class JAASKerberosTest {
             }
         } finally {
             System.clearProperty("java.security.auth.login.config");
+            Configuration.getConfiguration().refresh();
         }
     }
 }
diff --git a/herddb-core/src/test/java/herddb/server/security/JAASMD5Test.java b/herddb-core/src/test/java/herddb/server/security/JAASMD5Test.java
index d3ad478a..faf91608 100644
--- a/herddb-core/src/test/java/herddb/server/security/JAASMD5Test.java
+++ b/herddb-core/src/test/java/herddb/server/security/JAASMD5Test.java
@@ -33,6 +33,7 @@ import java.io.File;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Map;
+import javax.security.auth.login.Configuration;
 import org.junit.Assert;
 import org.junit.Rule;
 import org.junit.Test;
@@ -69,6 +70,7 @@ public class JAASMD5Test {
             }
         } finally {
             System.clearProperty("java.security.auth.login.config");
+            Configuration.getConfiguration().refresh();
         }
     }
 }
diff --git a/herddb-core/src/test/java/herddb/server/security/JASSPLAINTest.java b/herddb-core/src/test/java/herddb/server/security/JASSPLAINTest.java
new file mode 100644
index 00000000..03e5baec
--- /dev/null
+++ b/herddb-core/src/test/java/herddb/server/security/JASSPLAINTest.java
@@ -0,0 +1,92 @@
+/*
+ Licensed to Diennea S.r.l. under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. Diennea S.r.l. licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+
+ */
+
+package herddb.server.security;
+
+import static herddb.core.TestUtils.newServerConfigurationWithAutoPort;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import herddb.client.ClientConfiguration;
+import herddb.client.HDBClient;
+import herddb.client.HDBConnection;
+import herddb.client.HDBException;
+import herddb.model.TableSpace;
+import herddb.security.sasl.SaslUtils;
+import herddb.server.Server;
+import herddb.server.StaticClientSideMetadataProvider;
+import herddb.utils.RawString;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Simple username/password authentication
+ *
+ * @author enrico.olivelli
+ */
+public class JASSPLAINTest {
+
+    @Rule
+    public TemporaryFolder folder = new TemporaryFolder();
+
+    @Test
+    public void test() throws Exception {
+        try (Server server = new Server(newServerConfigurationWithAutoPort(folder.newFolder().toPath()))) {
+            server.start();
+
+
+            try (HDBClient client = new HDBClient(new ClientConfiguration(folder.newFolder().toPath())
+                    .set(ClientConfiguration.PROPERTY_AUTH_MECH, SaslUtils.AUTH_PLAIN)
+                    .set(ClientConfiguration.PROPERTY_CLIENT_USERNAME, "sa")
+                    .set(ClientConfiguration.PROPERTY_CLIENT_PASSWORD, "bad-password"));
+                 HDBConnection connection = client.openConnection()) {
+                client.setClientSideMetadataProvider(new StaticClientSideMetadataProvider(server));
+
+                connection.executeUpdate(TableSpace.DEFAULT,
+                        "CREATE TABLE mytable (id string primary key, n1 long, n2 integer)", 0, false, true, Collections.emptyList());
+                fail();
+            } catch (HDBException error) {
+                assertTrue(error.getMessage().contains("Failed authentication for username"));
+            }
+
+            try (HDBClient client = new HDBClient(new ClientConfiguration(folder.newFolder().toPath())
+                    .set(ClientConfiguration.PROPERTY_AUTH_MECH, SaslUtils.AUTH_PLAIN)
+                    .set(ClientConfiguration.PROPERTY_CLIENT_USERNAME, "sa")
+                    .set(ClientConfiguration.PROPERTY_CLIENT_PASSWORD, "hdb"));
+                 HDBConnection connection = client.openConnection()) {
+                client.setClientSideMetadataProvider(new StaticClientSideMetadataProvider(server));
+
+                long resultCreateTable = connection.executeUpdate(TableSpace.DEFAULT,
+                        "CREATE TABLE mytable (id string primary key, n1 long, n2 integer)", 0, false, true, Collections.emptyList()).updateCount;
+                Assert.assertEquals(1, resultCreateTable);
+
+                Assert.assertEquals(1, connection.executeUpdate(TableSpace.DEFAULT, "INSERT INTO mytable (id,n1,n2) values(?,?,?)", 0, false, true, Arrays.asList("test_0", 1, 2)).updateCount);
+                Map<RawString, Object> newValue = connection.executeUpdate(TableSpace.DEFAULT, "UPDATE mytable set n1= n1+1 where id=?", 0, true, true, Arrays.asList("test_0")).newvalue;
+                assertEquals(Long.valueOf(2), newValue.get(RawString.of("n1")));
+
+            }
+        }
+    }
+}
diff --git a/herddb-core/src/test/resources/test_jaas_md5.conf b/herddb-core/src/test/resources/test_jaas_md5.conf
index aa570404..00b21a57 100644
--- a/herddb-core/src/test/resources/test_jaas_md5.conf
+++ b/herddb-core/src/test/resources/test_jaas_md5.conf
@@ -19,12 +19,12 @@
 */
 
 HerdDBServer {
-       org.apache.zookeeper.server.auth.DigestLoginModule required
+       herddb.security.sasl.DigestLoginModule required
        user_hd="testpwd";
 };
 
 HerdDBClient {
-       org.apache.zookeeper.server.auth.DigestLoginModule required
+       herddb.security.sasl.DigestLoginModule required
        username="hd"
        password="testpwd";
 };