Working ident client authentication
authorMathieu Baudier <mbaudier@argeo.org>
Sat, 27 Jul 2019 12:15:51 +0000 (14:15 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Sat, 27 Jul 2019 12:15:51 +0000 (14:15 +0200)
org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java
org.argeo.cms/src/org/argeo/cms/auth/HttpSessionLoginModule.java
org.argeo.cms/src/org/argeo/cms/auth/IdentLoginModule.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java
org.argeo.cms/src/org/argeo/cms/internal/kernel/Activator.java
org.argeo.cms/src/org/argeo/cms/internal/kernel/jaas.cfg
org.argeo.enterprise/src/org/argeo/ident/IdentClient.java

index 9a60e913465eb530c4ebba7be3ecba72791681a9..ab40e720c2eaf58b2b3d1a9457ee86c8dedbb47a 100644 (file)
@@ -44,6 +44,8 @@ class CmsAuthUtils {
        final static String SHARED_STATE_SPNEGO_TOKEN = "org.argeo.cms.auth.spnegoToken";
        final static String SHARED_STATE_SPNEGO_OUT_TOKEN = "org.argeo.cms.auth.spnegoOutToken";
        final static String SHARED_STATE_CERTIFICATE_CHAIN = "org.argeo.cms.auth.certificateChain";
+       final static String SHARED_STATE_REMOTE_ADDR = "org.argeo.cms.auth.remote.addr";
+       final static String SHARED_STATE_REMOTE_PORT = "org.argeo.cms.auth.remote.port";
 
        static void addAuthorization(Subject subject, Authorization authorization) {
                assert subject != null;
index 91a01574e18f499b6da627619576f88bf3a1d0ea..f42e79c98980d185e469329aedc8810b6775aa3d 100644 (file)
@@ -83,11 +83,6 @@ public class HttpSessionLoginModule implements LoginModule {
                } else {
                        authorization = (Authorization) request.getAttribute(HttpContext.AUTHORIZATION);
                        if (authorization == null) {// search by session ID
-                               // TODO implement ident
-//                             IdentClient identClient = new IdentClient(request.getRemoteAddr(), "changeit");
-//                             String identUsername = identClient.getUsername(request.getLocalPort(), request.getRemotePort());
-//                             log.debug("Ident username: " + identUsername);
-
                                HttpSession httpSession = request.getSession(false);
                                if (httpSession == null) {
                                        // TODO make sure this is always safe
diff --git a/org.argeo.cms/src/org/argeo/cms/auth/IdentLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/IdentLoginModule.java
new file mode 100644 (file)
index 0000000..b4c49b2
--- /dev/null
@@ -0,0 +1,84 @@
+package org.argeo.cms.auth;
+
+import java.io.IOException;
+import java.util.Map;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.internal.kernel.Activator;
+import org.argeo.ident.IdentClient;
+
+public class IdentLoginModule implements LoginModule {
+       private final static Log log = LogFactory.getLog(IdentLoginModule.class);
+
+       private Subject subject = null;
+       private CallbackHandler callbackHandler = null;
+       private Map<String, Object> sharedState = null;
+
+       @Override
+       public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
+                       Map<String, ?> options) {
+               this.subject = subject;
+               this.callbackHandler = callbackHandler;
+               this.sharedState = (Map<String, Object>) sharedState;
+       }
+
+       @Override
+       public boolean login() throws LoginException {
+               if (callbackHandler == null)
+                       return false;
+               HttpRequestCallback httpCallback = new HttpRequestCallback();
+               try {
+                       callbackHandler.handle(new Callback[] { httpCallback });
+               } catch (IOException e) {
+                       throw new LoginException("Cannot handle http callback: " + e.getMessage());
+               } catch (UnsupportedCallbackException e) {
+                       return false;
+               }
+               HttpServletRequest request = httpCallback.getRequest();
+               IdentClient identClient = Activator.getIdentClient(request.getRemoteAddr());
+               if (identClient == null)
+                       return false;
+               String identUsername;
+               try {
+                       identUsername = identClient.getUsername(request.getLocalPort(), request.getRemotePort());
+               } catch (Exception e) {
+                       e.printStackTrace();
+                       return false;
+               }
+               if (identUsername != null) {
+                       if (log.isDebugEnabled())
+                               log.debug("Ident username: " + identUsername);
+                       sharedState.put(CmsAuthUtils.SHARED_STATE_NAME, identUsername);
+                       sharedState.put(CmsAuthUtils.SHARED_STATE_REMOTE_ADDR, request.getRemoteAddr());
+                       sharedState.put(CmsAuthUtils.SHARED_STATE_REMOTE_PORT, request.getRemotePort());
+                       return true;
+               } else {
+                       return false;
+               }
+       }
+
+       @Override
+       public boolean commit() throws LoginException {
+               return true;
+       }
+
+       @Override
+       public boolean abort() throws LoginException {
+               return true;
+       }
+
+       @Override
+       public boolean logout() throws LoginException {
+               return true;
+       }
+
+}
index cdb0f4ca27edfff0ad89340753d357ab097c09d7..6a3ac97dfa76dfac6bd955edee1ef256790cdef2 100644 (file)
@@ -83,6 +83,7 @@ public class UserAdminLoginModule implements LoginModule {
                final String username;
                final char[] password;
                Object certificateChain = null;
+               boolean preauth = false;
                if (sharedState.containsKey(CmsAuthUtils.SHARED_STATE_NAME)
                                && sharedState.containsKey(CmsAuthUtils.SHARED_STATE_PWD)) {
                        // NB: required by Basic http auth
@@ -103,6 +104,12 @@ public class UserAdminLoginModule implements LoginModule {
                        username = certDn;
                        certificateChain = sharedState.get(CmsAuthUtils.SHARED_STATE_CERTIFICATE_CHAIN);
                        password = null;
+               } else if (sharedState.containsKey(CmsAuthUtils.SHARED_STATE_NAME)
+                               && sharedState.containsKey(CmsAuthUtils.SHARED_STATE_REMOTE_ADDR)
+                               && sharedState.containsKey(CmsAuthUtils.SHARED_STATE_REMOTE_PORT)) {// ident
+                       username = (String) sharedState.get(CmsAuthUtils.SHARED_STATE_NAME);
+                       password = null;
+                       preauth = true;
                } else if (singleUser) {
                        username = OsUserUtils.getOsUsername();
                        password = null;
@@ -184,6 +191,8 @@ public class UserAdminLoginModule implements LoginModule {
                        // is provided
                } else if (singleUser) {
                        // TODO verify IP address?
+               } else if (preauth) {
+                       // ident
                } else {
                        throw new CredentialNotFoundException("No credentials provided");
                }
index 07f3867ce4bbc35babac16ed587ed3fff036b22d..bba8f2bbb519e08fc3843a3620f5e0c1fe32c80e 100644 (file)
@@ -13,6 +13,7 @@ import javax.security.auth.login.Configuration;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.argeo.cms.CmsException;
+import org.argeo.ident.IdentClient;
 import org.argeo.node.ArgeoLogger;
 import org.argeo.node.NodeConstants;
 import org.argeo.node.NodeDeployment;
@@ -174,6 +175,13 @@ public class Activator implements BundleActivator {
                return KernelUtils.getFrameworkProp(NodeConstants.HTTP_PROXY_SSL_DN);
        }
 
+       public static IdentClient getIdentClient(String remoteAddr) {
+               if (!IdentClient.isDefaultAuthdPassphraseFileAvailable())
+                       return null;
+               // TODO make passphrase more configurable
+               return new IdentClient(remoteAddr);
+       }
+
        private static NodeUserAdmin getNodeUserAdmin() {
                NodeUserAdmin res;
                try {
index e32c23f11c09f167f29a93e67e6358c738c89526..9b3f0114b9a853b96276038081cf3729b5af6906 100644 (file)
@@ -1,5 +1,6 @@
 USER {
     org.argeo.cms.auth.HttpSessionLoginModule sufficient;
+    org.argeo.cms.auth.IdentLoginModule optional;
     org.argeo.cms.auth.UserAdminLoginModule sufficient;
 };
 
index 32fb28ba10363abd99b7fc8da04068f86580bc22..cb0f2298c897f2ae7072488da284e6e72761bbb1 100644 (file)
@@ -1,10 +1,16 @@
 package org.argeo.ident;
 
 import java.io.BufferedReader;
+import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
+import java.net.ConnectException;
 import java.net.Socket;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
 import java.util.StringTokenizer;
 
 /**
@@ -13,14 +19,26 @@ import java.util.StringTokenizer;
  * @see RFC 1413 https://tools.ietf.org/html/rfc1413
  */
 public class IdentClient {
-       private String host = "localhost";
-       private int port = 113;
+       public final static int DEFAULT_IDENT_PORT = 113;
+       public final static String AUTHD_PASSPHRASE_PATH = "/etc/ident.key";
+       final static String NO_USER = "NO-USER";
+
+       private final String host;
+       private final int port;
 
        private OpenSslDecryptor openSslDecryptor = new OpenSslDecryptor();
-       private String identPassphrase = "changeit";
+       private String identPassphrase = null;
+
+       public IdentClient(String host) {
+               this(host, readPassphrase(AUTHD_PASSPHRASE_PATH), DEFAULT_IDENT_PORT);
+       }
+
+       public IdentClient(String host, Path passPhrasePath) {
+               this(host, readPassphrase(passPhrasePath), DEFAULT_IDENT_PORT);
+       }
 
        public IdentClient(String host, String identPassphrase) {
-               this(host, identPassphrase, 113);
+               this(host, identPassphrase, DEFAULT_IDENT_PORT);
        }
 
        public IdentClient(String host, String identPassphrase, int port) {
@@ -29,6 +47,7 @@ public class IdentClient {
                this.port = port;
        }
 
+       /** @return the username or null if none */
        public String getUsername(int serverPort, int clientPort) {
                String answer;
                try (Socket socket = new Socket(host, port)) {
@@ -38,14 +57,22 @@ public class IdentClient {
                        out.flush();
                        BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                        answer = reader.readLine();
+               } catch (ConnectException e) {
+                       System.err
+                                       .println("Ident client is configured but no ident server available on " + host + " (port " + port + ")");
+                       return null;
                } catch (Exception e) {
-                       throw new RuntimeException("Cannot read from ident server on " + host + ":" + port, e);
+                       throw new RuntimeException("Cannot read from ident server on " + host + " (port " + port + ")", e);
                }
                StringTokenizer st = new StringTokenizer(answer, " :\n");
                String username = null;
                while (st.hasMoreTokens())
                        username = st.nextToken();
-               if (username.startsWith("[")) {
+
+               if (username.equals(NO_USER))
+                       return null;
+
+               if (identPassphrase != null && username.startsWith("[")) {
                        String encrypted = username.substring(1, username.length() - 1);
                        username = openSslDecryptor.decryptAuthd(encrypted, identPassphrase).trim();
                }
@@ -57,6 +84,38 @@ public class IdentClient {
                this.openSslDecryptor = openSslDecryptor;
        }
 
+       public static String readPassphrase(String filePath) {
+               return readPassphrase(Paths.get(filePath));
+       }
+
+       /** @return the first line of the file. */
+       public static String readPassphrase(Path path) {
+               if (!isPathAvailable(path))
+                       return null;
+               List<String> lines;
+               try {
+                       lines = Files.readAllLines(path);
+               } catch (IOException e) {
+                       throw new IllegalStateException("Cannot read " + path, e);
+               }
+               if (lines.size() == 0)
+                       return null;
+               String passphrase = lines.get(0);
+               return passphrase;
+       }
+
+       public static boolean isDefaultAuthdPassphraseFileAvailable() {
+               return isPathAvailable(Paths.get(AUTHD_PASSPHRASE_PATH));
+       }
+
+       public static boolean isPathAvailable(Path path) {
+               if (!Files.exists(path))
+                       return false;
+               if (!Files.isReadable(path))
+                       return false;
+               return true;
+       }
+
        public static void main(String[] args) {
                IdentClient identClient = new IdentClient("127.0.0.1", "changeit");
                String username = identClient.getUsername(7070, 55958);