Improve token authentication
authorMathieu Baudier <mbaudier@argeo.org>
Wed, 8 Nov 2017 16:10:49 +0000 (17:10 +0100)
committerMathieu Baudier <mbaudier@argeo.org>
Wed, 8 Nov 2017 16:10:49 +0000 (17:10 +0100)
org.argeo.cms.ui/src/org/argeo/cms/ui/AbstractCmsEntryPoint.java
org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java
org.argeo.enterprise/src/org/argeo/naming/AuthPassword.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/naming/LdapAttrs.java
org.argeo.enterprise/src/org/argeo/naming/NamingUtils.java
org.argeo.enterprise/src/org/argeo/naming/SharedSecret.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java
org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUser.java

index 43906fae73e7914aaa0797d06aa7746e76da93ad..9d56e5eae4654072e0a1671ee4238896dd4ccaaa 100644 (file)
@@ -1,6 +1,7 @@
 package org.argeo.cms.ui;
 
-import java.io.IOException;
+import static org.argeo.naming.SharedSecret.X_SHARED_SECRET;
+
 import java.security.PrivilegedAction;
 import java.util.HashMap;
 import java.util.Map;
@@ -13,11 +14,6 @@ import javax.jcr.RepositoryException;
 import javax.jcr.Session;
 import javax.jcr.nodetype.NodeType;
 import javax.security.auth.Subject;
-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.auth.login.LoginContext;
 import javax.security.auth.login.LoginException;
 import javax.servlet.http.HttpServletRequest;
@@ -29,6 +25,8 @@ import org.argeo.cms.auth.CurrentUser;
 import org.argeo.cms.auth.HttpRequestCallbackHandler;
 import org.argeo.eclipse.ui.specific.UiContext;
 import org.argeo.jcr.JcrUtils;
+import org.argeo.naming.AuthPassword;
+import org.argeo.naming.SharedSecret;
 import org.argeo.node.NodeConstants;
 import org.eclipse.rap.rwt.RWT;
 import org.eclipse.rap.rwt.application.AbstractEntryPoint;
@@ -274,24 +272,28 @@ public abstract class AbstractCmsEntryPoint extends AbstractEntryPoint implement
                        }
 
                        // auth
-                       int colonIndex = prefix.indexOf(':');
+                       int colonIndex = prefix.indexOf('$');
                        if (colonIndex > 0) {
-                               String user = prefix.substring(0, colonIndex);
-                               // if (isAnonymous()) {
-                               String token = prefix.substring(colonIndex + 1);
-                               LoginContext lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, new CallbackHandler() {
-
-                                       @Override
-                                       public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
-                                               for (Callback callback : callbacks) {
-                                                       if (callback instanceof NameCallback)
-                                                               ((NameCallback) callback).setName(user);
-                                                       else if (callback instanceof PasswordCallback)
-                                                               ((PasswordCallback) callback).setPassword(token.toCharArray());
-                                               }
-
-                                       }
-                               });
+                               // String user = prefix.substring(0, colonIndex);
+                               // // if (isAnonymous()) {
+                               // String token = prefix.substring(colonIndex + 1);
+                               // LoginContext lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, new
+                               // CallbackHandler() {
+                               //
+                               // @Override
+                               // public void handle(Callback[] callbacks) throws IOException,
+                               // UnsupportedCallbackException {
+                               // for (Callback callback : callbacks) {
+                               // if (callback instanceof NameCallback)
+                               // ((NameCallback) callback).setName(user);
+                               // else if (callback instanceof PasswordCallback)
+                               // ((PasswordCallback) callback).setPassword(token.toCharArray());
+                               // }
+                               //
+                               // }
+                               // });
+                               SharedSecret token = new SharedSecret(new AuthPassword(X_SHARED_SECRET + '$' + prefix));
+                               LoginContext lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, token);
                                lc.login();
                                authChange(lc);// sets the node as well
                                // } else {
index 8124c7d0ffbab745f9ac113f2184d641de5a5f2d..331f1af0522375adfec819ec722ef0573ea1427e 100644 (file)
@@ -42,7 +42,7 @@ public class UserAdminLoginModule implements LoginModule {
        private Map<String, Object> sharedState = null;
 
        private List<String> indexedUserProperties = Arrays
-                       .asList(new String[] { LdapAttrs.uid.name(), LdapAttrs.mail.name(), LdapAttrs.cn.name() });
+                       .asList(new String[] { LdapAttrs.mail.name(), LdapAttrs.uid.name(), LdapAttrs.authPassword.name() });
 
        // private state
        private BundleContext bc;
diff --git a/org.argeo.enterprise/src/org/argeo/naming/AuthPassword.java b/org.argeo.enterprise/src/org/argeo/naming/AuthPassword.java
new file mode 100644 (file)
index 0000000..f9cfc69
--- /dev/null
@@ -0,0 +1,142 @@
+package org.argeo.naming;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.StringTokenizer;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+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 org.argeo.osgi.useradmin.UserDirectoryException;
+
+/**
+ * LDAP authPassword field according to RFC 3112.
+ * 
+ * @see https://www.ietf.org/rfc/rfc3112.txt
+ */
+public class AuthPassword implements CallbackHandler {
+       private final String authScheme;
+       private final String authInfo;
+       private final String authValue;
+
+       public AuthPassword(String value) {
+               StringTokenizer st = new StringTokenizer(value, "$");
+               // TODO make it more robust, deal with bad formatting
+               this.authScheme = st.nextToken().trim();
+               this.authInfo = st.nextToken().trim();
+               this.authValue = st.nextToken().trim();
+
+               String expectedAuthScheme = getExpectedAuthScheme();
+               if (expectedAuthScheme != null && !authScheme.equals(expectedAuthScheme))
+                       throw new IllegalArgumentException(
+                                       "Auth scheme " + authScheme + " is not compatible with " + expectedAuthScheme);
+       }
+
+       protected AuthPassword(String authInfo, String authValue) {
+               this.authScheme = getExpectedAuthScheme();
+               if (authScheme == null)
+                       throw new IllegalArgumentException("Expected auth scheme cannot be null");
+               this.authInfo = authInfo;
+               this.authValue = authValue;
+       }
+
+       protected AuthPassword(AuthPassword authPassword) {
+               this.authScheme = authPassword.getAuthScheme();
+               this.authInfo = authPassword.getAuthInfo();
+               this.authValue = authPassword.getAuthValue();
+       }
+
+       protected String getExpectedAuthScheme() {
+               return null;
+       }
+
+       protected boolean matchAuthValue(Object object) {
+               return authValue.equals(object.toString());
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (!(obj instanceof AuthPassword))
+                       return false;
+               AuthPassword authPassword = (AuthPassword) obj;
+               return authScheme.equals(authPassword.authScheme) && authInfo.equals(authPassword.authInfo)
+                               && authValue.equals(authValue);
+       }
+
+       public boolean keyEquals(AuthPassword authPassword) {
+               return authScheme.equals(authPassword.authScheme) && authInfo.equals(authPassword.authInfo);
+       }
+
+       @Override
+       public int hashCode() {
+               return authValue.hashCode();
+       }
+
+       @Override
+       public String toString() {
+               return toAuthPassword();
+       }
+
+       public final String toAuthPassword() {
+               return getAuthScheme() + '$' + authInfo + '$' + authValue;
+       }
+
+       public String getAuthScheme() {
+               return authScheme;
+       }
+
+       public String getAuthInfo() {
+               return authInfo;
+       }
+
+       public String getAuthValue() {
+               return authValue;
+       }
+
+       public static AuthPassword matchAuthValue(Attributes attributes, char[] value) {
+               try {
+                       Attribute authPassword = attributes.get(LdapAttrs.authPassword.name());
+                       NamingEnumeration<?> values = authPassword.getAll();
+                       while (values.hasMore()) {
+                               Object val = values.next();
+                               AuthPassword token = new AuthPassword(val.toString());
+                               String auth;
+                               if (Arrays.binarySearch(value, '$') >= 0) {
+                                       auth = token.authInfo + '$' + token.authValue;
+                               } else {
+                                       auth = token.authValue;
+                               }
+                               if (Arrays.equals(auth.toCharArray(), value))
+                                       return token;
+                               // if (token.matchAuthValue(value))
+                               // return token;
+                       }
+                       return null;
+               } catch (NamingException e) {
+                       throw new UserDirectoryException("Cannot check attribute", e);
+               }
+       }
+
+       public static boolean remove(Attributes attributes, AuthPassword value) {
+               Attribute authPassword = attributes.get(LdapAttrs.authPassword.name());
+               return authPassword.remove(value.toAuthPassword());
+       }
+
+       @Override
+       public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+               for (Callback callback : callbacks) {
+                       if (callback instanceof NameCallback)
+                               ((NameCallback) callback).setName(toAuthPassword());
+                       else if (callback instanceof PasswordCallback)
+                               ((PasswordCallback) callback).setPassword(getAuthValue().toCharArray());
+               }
+       }
+
+}
index 006f0e767e3ed045beb058ec23efada81f3f96f8..fe03e82f32daa0eaeff29a4e06b9825cb432b6a7 100644 (file)
@@ -266,6 +266,9 @@ public enum LdapAttrs implements SpecifiedName {
        userPKCS12("2.16.840.1.113730.3.1.216", "RFC 2798"),
        /** */
        displayName("2.16.840.1.113730.3.1.241", "RFC 2798"),
+       
+       // Sun memberOf
+       memberOf("1.2.840.113556.1.2.102","389 DS memberOf"),
 
        // KERBEROS (partial)
        krbPrincipalName("2.16.840.1.113719.1.301.6.8.1", "Novell Kerberos Schema Definitions"),
index 64fd65faa470aea8721334f0defef883fe9bd7e3..7d5c7b7d3d82d8dc4a9a3926ed316f7761d39c5b 100644 (file)
@@ -4,11 +4,13 @@ import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.net.URLDecoder;
 import java.nio.charset.StandardCharsets;
+import java.time.DateTimeException;
 import java.time.Instant;
 import java.time.OffsetDateTime;
 import java.time.ZoneOffset;
 import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
 import java.time.temporal.ChronoField;
 import java.util.Calendar;
 import java.util.GregorianCalendar;
@@ -21,8 +23,13 @@ public class NamingUtils {
        private final static DateTimeFormatter utcLdapDate = DateTimeFormatter.ofPattern("uuuuMMddHHmmssX")
                        .withZone(ZoneOffset.UTC);
 
+       /** @return null if not parseable */
        public static Instant ldapDateToInstant(String ldapDate) {
-               return OffsetDateTime.parse(ldapDate, utcLdapDate).toInstant();
+               try {
+                       return OffsetDateTime.parse(ldapDate, utcLdapDate).toInstant();
+               } catch (DateTimeParseException e) {
+                       return null;
+               }
        }
 
        public static Calendar ldapDateToCalendar(String ldapDate) {
@@ -66,7 +73,8 @@ public class NamingUtils {
                                        query_pairs.put(key, new LinkedList<String>());
                                }
                                final String value = idx > 0 && pair.length() > idx + 1
-                                               ? URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8.name()) : null;
+                                               ? URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8.name())
+                                               : null;
                                query_pairs.get(key).add(value);
                        }
                        return query_pairs;
diff --git a/org.argeo.enterprise/src/org/argeo/naming/SharedSecret.java b/org.argeo.enterprise/src/org/argeo/naming/SharedSecret.java
new file mode 100644 (file)
index 0000000..d1f0b91
--- /dev/null
@@ -0,0 +1,41 @@
+package org.argeo.naming;
+
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+
+public class SharedSecret extends AuthPassword {
+       public final static String X_SHARED_SECRET = "X-SharedSecret";
+       private final Instant expiry;
+
+       public SharedSecret(AuthPassword authPassword) {
+               super(authPassword);
+               String authInfo = getAuthInfo();
+               if (authInfo.length() == 16) {
+                       expiry = NamingUtils.ldapDateToInstant(authInfo);
+               } else {
+                       expiry = null;
+               }
+       }
+
+       public SharedSecret(ZonedDateTime expiryTimestamp, String value) {
+               super(NamingUtils.instantToLdapDate(expiryTimestamp), value);
+               expiry = expiryTimestamp.withZoneSameInstant(ZoneOffset.UTC).toInstant();
+       }
+
+       public SharedSecret(int hours, String value) {
+               this(ZonedDateTime.now().plusHours(hours), value);
+       }
+
+       @Override
+       protected String getExpectedAuthScheme() {
+               return X_SHARED_SECRET;
+       }
+
+       public boolean isExpired() {
+               if (expiry == null)
+                       return false;
+               return expiry.isBefore(Instant.now());
+       }
+
+}
index 66b6e91e27eac392fc4af28fb7791a3efb454a40..957a1c953990767effa6ee122db4a01a8fcb59e2 100644 (file)
@@ -173,7 +173,7 @@ public abstract class AbstractUserDirectory implements UserAdmin, UserDirectory
        private void collectRoles(DirectoryUser user, List<Role> allRoles) {
                Attributes attrs = user.getAttributes();
                // TODO centralize attribute name
-               Attribute memberOf = attrs.get("memberOf");
+               Attribute memberOf = attrs.get(LdapAttrs.memberOf.name());
                if (memberOf != null) {
                        try {
                                NamingEnumeration<?> values = memberOf.getAll();
index 4eab8cd87023a0cd5b3f791979af0b1e70ef52ce..90b8daa0eff0603c3f50e1398586a505e9dee192 100644 (file)
@@ -1,11 +1,8 @@
 package org.argeo.osgi.useradmin;
 
-import java.net.URI;
-import java.net.URISyntaxException;
 import java.nio.ByteBuffer;
 import java.nio.CharBuffer;
 import java.nio.charset.StandardCharsets;
-import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Base64;
@@ -15,9 +12,7 @@ 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;
@@ -26,8 +21,9 @@ import javax.naming.directory.Attributes;
 import javax.naming.directory.BasicAttribute;
 import javax.naming.ldap.LdapName;
 
+import org.argeo.naming.AuthPassword;
 import org.argeo.naming.LdapAttrs;
-import org.argeo.naming.NamingUtils;
+import org.argeo.naming.SharedSecret;
 
 /** Directory user implementation */
 class LdifUser implements DirectoryUser {
@@ -78,47 +74,73 @@ 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);
+                       // String pwd = new String((char[]) value);
+                       // authPassword (RFC 312 https://tools.ietf.org/html/rfc3112)
                        char[] password = toChars(value);
+                       AuthPassword authPassword = AuthPassword.matchAuthValue(getAttributes(), password);
+                       if (authPassword != null) {
+                               if (authPassword.getAuthScheme().equals(SharedSecret.X_SHARED_SECRET)) {
+                                       SharedSecret onceToken = new SharedSecret(authPassword);
+                                       if (onceToken.isExpired()) {
+                                               // AuthPassword.remove(getAttributes(), onceToken);
+                                               return false;
+                                       } else {
+                                               // boolean wasRemoved = AuthPassword.remove(getAttributes(), onceToken);
+                                               return true;
+                                       }
+                                       // TODO delete expired tokens?
+                               } else {
+                                       // TODO implement SHA
+                                       throw new UnsupportedOperationException(
+                                                       "Unsupported authPassword scheme " + authPassword.getAuthScheme());
+                               }
+                       }
+
+                       // Regular password
                        byte[] hashedPassword = hash(password);
                        if (hasCredential(LdapAttrs.userPassword.name(), hashedPassword))
                                return true;
-                       if (hasCredential(LdapAttrs.authPassword.name(), pwd))
-                               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<String, List<String>> query = NamingUtils.queryToMap(uri);
-                                       String expiryTimestamp = NamingUtils.getQueryValue(query, LdapAttrs.modifyTimestamp.name());
-                                       if (expiryTimestamp != null) {
-                                               Instant expiryOdt = NamingUtils.ldapDateToInstant(expiryTimestamp);
-                                               if (expiryOdt.isBefore(Instant.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);
-                               }
-                       }
-               }
+               // authPassword (RFC 3112 https://tools.ietf.org/html/rfc3112)
+               // if (key.startsWith(ClientToken.X_CLIENT_TOKEN)) {
+               // return ClientToken.checkAttribute(getAttributes(), key, value);
+               // } else if (key.startsWith(OnceToken.X_ONCE_TOKEN)) {
+               // return OnceToken.checkAttribute(getAttributes(), key, value);
+               // }
+               // 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(UriToken.X_URI_TOKEN)) {
+               // UriToken token = new UriToken((String)storedValue);
+               // try {
+               // URI uri = new URI(authInfo);
+               // Map<String, List<String>> query = NamingUtils.queryToMap(uri);
+               // String expiryTimestamp = NamingUtils.getQueryValue(query,
+               // LdapAttrs.modifyTimestamp.name());
+               // if (expiryTimestamp != null) {
+               // Instant expiryOdt = NamingUtils.ldapDateToInstant(expiryTimestamp);
+               // if (expiryOdt.isBefore(Instant.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);
+               // }
+               // }
 
+               Object storedValue = getCredentials().get(key);
                if (storedValue == null || value == null)
                        return false;
                if (!(value instanceof String || value instanceof byte[]))
@@ -328,6 +350,9 @@ class LdifUser implements DirectoryUser {
                                byte[] hashedPassword = hash(password);
                                return put(LdapAttrs.userPassword.name(), hashedPassword);
                        }
+                       if (key.startsWith("X-")) {
+                               return put(LdapAttrs.authPassword.name(), value);
+                       }
 
                        userAdmin.checkEdit();
                        if (!isEditing())
@@ -343,7 +368,8 @@ class LdifUser implements DirectoryUser {
 
                        try {
                                Attribute attribute = getModifiedAttributes().get(key.toString());
-                               attribute = new BasicAttribute(key.toString());
+                               if (attribute == null)
+                                       attribute = new BasicAttribute(key.toString());
                                if (value instanceof String && !isAsciiPrintable(((String) value)))
                                        attribute.add(((String) value).getBytes(StandardCharsets.UTF_8));
                                else