From d2057396fab26e7b94e9d479d8429e0ed2487067 Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Sat, 26 Aug 2017 15:46:45 +0200 Subject: [PATCH] Introduce weak authentication --- .../cms/auth/HttpSessionLoginModule.java | 10 +++ .../src/org/argeo/naming/NamingUtils.java | 62 +++++++++++++++++++ .../osgi/useradmin/AbstractUserDirectory.java | 3 +- .../org/argeo/osgi/useradmin/LdifUser.java | 61 ++++++++++++++---- .../argeo/osgi/useradmin/UserAdminConf.java | 44 ++++++------- .../src/org/argeo/node/NodeConstants.java | 2 + 6 files changed, 147 insertions(+), 35 deletions(-) create mode 100644 org.argeo.enterprise/src/org/argeo/naming/NamingUtils.java 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 9d41cea69..d3103627c 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/HttpSessionLoginModule.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/HttpSessionLoginModule.java @@ -20,6 +20,7 @@ import org.apache.commons.codec.binary.Base64; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.argeo.cms.CmsException; +import org.argeo.naming.LdapAttrs; import org.osgi.framework.BundleContext; import org.osgi.framework.FrameworkUtil; import org.osgi.framework.InvalidSyntaxException; @@ -175,6 +176,15 @@ public class HttpSessionLoginModule implements LoginModule { } } } + + // auth token +// String mail = request.getParameter(LdapAttrs.mail.name()); +// String authPassword = request.getParameter(LdapAttrs.authPassword.name()); +// if (authPassword != null) { +// sharedState.put(CmsAuthUtils.SHARED_STATE_PWD, authPassword); +// if (mail != null) +// sharedState.put(CmsAuthUtils.SHARED_STATE_NAME, mail); +// } } private X509Certificate[] extractClientCertificate(HttpServletRequest req) { diff --git a/org.argeo.enterprise/src/org/argeo/naming/NamingUtils.java b/org.argeo.enterprise/src/org/argeo/naming/NamingUtils.java new file mode 100644 index 000000000..fc505022f --- /dev/null +++ b/org.argeo.enterprise/src/org/argeo/naming/NamingUtils.java @@ -0,0 +1,62 @@ +package org.argeo.naming; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +public class NamingUtils { + private final static DateTimeFormatter ldapDateTimeFormatter = DateTimeFormatter + .ofPattern("uuuuMMddHHmmss[,S][.S]X"); + + public static OffsetDateTime ldapDateToInstant(String ldapDate) { + return OffsetDateTime.parse(ldapDate, ldapDateTimeFormatter); + } + + public static String getQueryValue(Map> query, String key) { + if (!query.containsKey(key)) + return null; + List val = query.get(key); + if (val.size() == 1) + return val.get(0); + else + throw new IllegalArgumentException("There are " + val.size() + " value(s) for " + key); + } + + public static Map> queryToMap(URI uri) { + return queryToMap(uri.getQuery()); + } + + private static Map> queryToMap(String queryPart) { + try { + final Map> query_pairs = new LinkedHashMap>(); + if (queryPart == null) + return query_pairs; + final String[] pairs = queryPart.split("&"); + for (String pair : pairs) { + final int idx = pair.indexOf("="); + final String key = idx > 0 ? URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8.name()) + : pair; + if (!query_pairs.containsKey(key)) { + query_pairs.put(key, new LinkedList()); + } + final String value = idx > 0 && pair.length() > idx + 1 + ? URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8.name()) : null; + query_pairs.get(key).add(value); + } + return query_pairs; + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException("Cannot convert " + queryPart + " to map", e); + } + } + + private NamingUtils() { + + } +} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java index c20260056..081d9e1fa 100644 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java +++ b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java @@ -60,7 +60,8 @@ public abstract class AbstractUserDirectory implements UserAdmin, UserDirectory // LdapAttrs.cn.name() }); private String memberAttributeId = "member"; - private List credentialAttributeIds = Arrays.asList(new String[] { LdapAttrs.userPassword.name() }); + private List credentialAttributeIds = Arrays + .asList(new String[] { LdapAttrs.userPassword.name(), LdapAttrs.authPassword.name() }); // JTA private TransactionManager transactionManager; diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUser.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUser.java index d26ed148f..7cf416526 100644 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUser.java +++ b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUser.java @@ -1,9 +1,12 @@ package org.argeo.osgi.useradmin; -import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; @@ -13,7 +16,9 @@ import java.util.Enumeration; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.StringTokenizer; import javax.naming.NamingEnumeration; import javax.naming.NamingException; @@ -23,6 +28,7 @@ import javax.naming.directory.BasicAttribute; import javax.naming.ldap.LdapName; import org.argeo.naming.LdapAttrs; +import org.argeo.naming.NamingUtils; /** Directory user implementation */ class LdifUser implements DirectoryUser { @@ -73,12 +79,47 @@ class LdifUser implements DirectoryUser { public boolean hasCredential(String key, Object value) { if (key == null) { // TODO check other sources (like PKCS12) + String pwd = new String((char[]) value); char[] password = toChars(value); byte[] hashedPassword = hash(password); - return hasCredential(LdapAttrs.userPassword.name(), hashedPassword); + if (hasCredential(LdapAttrs.userPassword.name(), hashedPassword)) + return true; + if (hasCredential(LdapAttrs.authPassword.name(), pwd)) + return true; + return false; } Object storedValue = getCredentials().get(key); + + // authPassword (RFC 312 https://tools.ietf.org/html/rfc3112) + if (LdapAttrs.authPassword.name().equals(key)) { + StringTokenizer st = new StringTokenizer((String) storedValue, "$ "); + // TODO make it more robust, deal with bad formatting + String authScheme = st.nextToken(); + String authInfo = st.nextToken(); + String authValue = st.nextToken(); + if (authScheme.equals("X-Node-Token")) { + try { + URI uri = new URI(authInfo); + Map> query = NamingUtils.queryToMap(uri); + String expiryTimestamp = NamingUtils.getQueryValue(query, LdapAttrs.modifyTimestamp.name()); + if (expiryTimestamp != null) { + OffsetDateTime expiryOdt = NamingUtils.ldapDateToInstant(expiryTimestamp); + if (expiryOdt.isBefore(OffsetDateTime.now())) + return false; + } else { + throw new UnsupportedOperationException("An expiry timestamp " + + LdapAttrs.modifyTimestamp.name() + " must be set in the URI query"); + } + byte[] hash = Base64.getDecoder().decode(authValue); + byte[] hashedInput = DigestUtils.sha1((authInfo + value).getBytes(StandardCharsets.US_ASCII)); + return Arrays.equals(hash, hashedInput); + } catch (URISyntaxException e) { + throw new UserDirectoryException("Badly formatted " + authInfo, e); + } + } + } + if (storedValue == null || value == null) return false; if (!(value instanceof String || value instanceof byte[])) @@ -93,14 +134,14 @@ class LdifUser implements DirectoryUser { /** Hash and clear the password */ private byte[] hash(char[] password) { byte[] hashedPassword = ("{SHA}" + Base64.getEncoder().encodeToString(DigestUtils.sha1(toBytes(password)))) - .getBytes(); - Arrays.fill(password, '\u0000'); + .getBytes(StandardCharsets.UTF_8); + // Arrays.fill(password, '\u0000'); return hashedPassword; } private byte[] toBytes(char[] chars) { CharBuffer charBuffer = CharBuffer.wrap(chars); - ByteBuffer byteBuffer = Charset.forName("UTF-8").encode(charBuffer); + ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer); byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()); Arrays.fill(charBuffer.array(), '\u0000'); // clear sensitive data Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data @@ -113,7 +154,7 @@ class LdifUser implements DirectoryUser { if (!(obj instanceof byte[])) throw new IllegalArgumentException(obj.getClass() + " is not a byte array"); ByteBuffer fromBuffer = ByteBuffer.wrap((byte[]) obj); - CharBuffer toBuffer = Charset.forName("UTF-8").decode(fromBuffer); + CharBuffer toBuffer = StandardCharsets.UTF_8.decode(fromBuffer); char[] res = Arrays.copyOfRange(toBuffer.array(), toBuffer.position(), toBuffer.limit()); Arrays.fill(fromBuffer.array(), (byte) 0); // clear sensitive data Arrays.fill((byte[]) obj, (byte) 0); // clear sensitive data @@ -255,7 +296,7 @@ class LdifUser implements DirectoryUser { if (key.equals(LdapAttrs.userPassword.name())) // TODO other cases (certificates, images) return value; - value = new String((byte[]) value, Charset.forName("UTF-8")); + value = new String((byte[]) value, StandardCharsets.UTF_8); } if (attr.size() == 1) return value; @@ -305,11 +346,7 @@ class LdifUser implements DirectoryUser { Attribute attribute = getModifiedAttributes().get(key.toString()); attribute = new BasicAttribute(key.toString()); if (value instanceof String && !isAsciiPrintable(((String) value))) - try { - attribute.add(((String) value).getBytes("UTF-8")); - } catch (UnsupportedEncodingException e) { - throw new UserDirectoryException("Cannot encode " + value, e); - } + attribute.add(((String) value).getBytes(StandardCharsets.UTF_8)); else attribute.add(value); Attribute previousAttribute = getModifiedAttributes().put(attribute); diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserAdminConf.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserAdminConf.java index 093b44370..83cbf795c 100644 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserAdminConf.java +++ b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserAdminConf.java @@ -1,16 +1,12 @@ package org.argeo.osgi.useradmin; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; -import java.net.URLDecoder; import java.util.Dictionary; import java.util.Enumeration; import java.util.Hashtable; -import java.util.LinkedHashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -20,6 +16,7 @@ import javax.naming.NamingException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.argeo.naming.DnsBrowser; +import org.argeo.naming.NamingUtils; import org.osgi.framework.Constants; /** Properties used to configure user admins. */ @@ -150,7 +147,7 @@ public enum UserAdminConf { } else if (scheme.equals("ipa")) { } else throw new UserDirectoryException("Unsupported scheme " + scheme); - Map> query = splitQuery(u.getQuery()); + Map> query = NamingUtils.queryToMap(u); for (String key : query.keySet()) { UserAdminConf ldapProp = UserAdminConf.valueOf(key); List values = query.get(key); @@ -221,23 +218,26 @@ public enum UserAdminConf { } - private static Map> splitQuery(String query) throws UnsupportedEncodingException { - final Map> query_pairs = new LinkedHashMap>(); - if (query == null) - return query_pairs; - final String[] pairs = query.split("&"); - for (String pair : pairs) { - final int idx = pair.indexOf("="); - final String key = idx > 0 ? URLDecoder.decode(pair.substring(0, idx), "UTF-8") : pair; - if (!query_pairs.containsKey(key)) { - query_pairs.put(key, new LinkedList()); - } - final String value = idx > 0 && pair.length() > idx + 1 - ? URLDecoder.decode(pair.substring(idx + 1), "UTF-8") : null; - query_pairs.get(key).add(value); - } - return query_pairs; - } + // private static Map> splitQuery(String query) throws + // UnsupportedEncodingException { + // final Map> query_pairs = new LinkedHashMap>(); + // if (query == null) + // return query_pairs; + // final String[] pairs = query.split("&"); + // for (String pair : pairs) { + // final int idx = pair.indexOf("="); + // final String key = idx > 0 ? URLDecoder.decode(pair.substring(0, idx), + // "UTF-8") : pair; + // if (!query_pairs.containsKey(key)) { + // query_pairs.put(key, new LinkedList()); + // } + // final String value = idx > 0 && pair.length() > idx + 1 + // ? URLDecoder.decode(pair.substring(idx + 1), "UTF-8") : null; + // query_pairs.get(key).add(value); + // } + // return query_pairs; + // } public static void main(String[] args) { Dictionary props = uriAsProperties("ldap://" + "uid=admin,ou=system:secret@localhost:10389" diff --git a/org.argeo.node.api/src/org/argeo/node/NodeConstants.java b/org.argeo.node.api/src/org/argeo/node/NodeConstants.java index 01fb5185a..2b4c284f6 100644 --- a/org.argeo.node.api/src/org/argeo/node/NodeConstants.java +++ b/org.argeo.node.api/src/org/argeo/node/NodeConstants.java @@ -62,6 +62,8 @@ public interface NodeConstants { // user U anonymous = everyone String ROLE_USER = "cn=user," + ROLES_BASEDN; String ROLE_ANONYMOUS = "cn=anonymous," + ROLES_BASEDN; + // Account lifecycle + String ROLE_REGISTERING = "cn=registering," + ROLES_BASEDN; /* * LOGIN CONTEXTS -- 2.30.2