From: Mathieu Baudier Date: Sat, 27 Jul 2019 12:15:51 +0000 (+0200) Subject: Working ident client authentication X-Git-Tag: argeo-commons-2.1.78~14 X-Git-Url: http://git.argeo.org/?p=lgpl%2Fargeo-commons.git;a=commitdiff_plain;h=c1d7a7fa363100689019e733314723196280175b Working ident client authentication --- diff --git a/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java b/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java index 9a60e9134..ab40e720c 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java @@ -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; diff --git a/org.argeo.cms/src/org/argeo/cms/auth/HttpSessionLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/HttpSessionLoginModule.java index 91a01574e..f42e79c98 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/HttpSessionLoginModule.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/HttpSessionLoginModule.java @@ -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 index 000000000..b4c49b285 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/auth/IdentLoginModule.java @@ -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 sharedState = null; + + @Override + public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, + Map options) { + this.subject = subject; + this.callbackHandler = callbackHandler; + this.sharedState = (Map) 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; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java index cdb0f4ca2..6a3ac97df 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java @@ -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"); } diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/Activator.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/Activator.java index 07f3867ce..bba8f2bbb 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/kernel/Activator.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/kernel/Activator.java @@ -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 { diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/jaas.cfg b/org.argeo.cms/src/org/argeo/cms/internal/kernel/jaas.cfg index e32c23f11..9b3f0114b 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/kernel/jaas.cfg +++ b/org.argeo.cms/src/org/argeo/cms/internal/kernel/jaas.cfg @@ -1,5 +1,6 @@ USER { org.argeo.cms.auth.HttpSessionLoginModule sufficient; + org.argeo.cms.auth.IdentLoginModule optional; org.argeo.cms.auth.UserAdminLoginModule sufficient; }; diff --git a/org.argeo.enterprise/src/org/argeo/ident/IdentClient.java b/org.argeo.enterprise/src/org/argeo/ident/IdentClient.java index 32fb28ba1..cb0f2298c 100644 --- a/org.argeo.enterprise/src/org/argeo/ident/IdentClient.java +++ b/org.argeo.enterprise/src/org/argeo/ident/IdentClient.java @@ -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 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);