From: Mathieu Baudier Date: Wed, 8 Nov 2017 16:10:49 +0000 (+0100) Subject: Improve token authentication X-Git-Tag: argeo-commons-2.1.70~35 X-Git-Url: https://git.argeo.org/?a=commitdiff_plain;h=2111a76162534100967eb0e7733632e80d9ddc5f;p=lgpl%2Fargeo-commons.git Improve token authentication --- diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/AbstractCmsEntryPoint.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/AbstractCmsEntryPoint.java index 43906fae7..9d56e5eae 100644 --- a/org.argeo.cms.ui/src/org/argeo/cms/ui/AbstractCmsEntryPoint.java +++ b/org.argeo.cms.ui/src/org/argeo/cms/ui/AbstractCmsEntryPoint.java @@ -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 { 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 8124c7d0f..331f1af05 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java @@ -42,7 +42,7 @@ public class UserAdminLoginModule implements LoginModule { private Map sharedState = null; private List 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 index 000000000..f9cfc69c8 --- /dev/null +++ b/org.argeo.enterprise/src/org/argeo/naming/AuthPassword.java @@ -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()); + } + } + +} diff --git a/org.argeo.enterprise/src/org/argeo/naming/LdapAttrs.java b/org.argeo.enterprise/src/org/argeo/naming/LdapAttrs.java index 006f0e767..fe03e82f3 100644 --- a/org.argeo.enterprise/src/org/argeo/naming/LdapAttrs.java +++ b/org.argeo.enterprise/src/org/argeo/naming/LdapAttrs.java @@ -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"), diff --git a/org.argeo.enterprise/src/org/argeo/naming/NamingUtils.java b/org.argeo.enterprise/src/org/argeo/naming/NamingUtils.java index 64fd65faa..7d5c7b7d3 100644 --- a/org.argeo.enterprise/src/org/argeo/naming/NamingUtils.java +++ b/org.argeo.enterprise/src/org/argeo/naming/NamingUtils.java @@ -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()); } 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 index 000000000..d1f0b91d2 --- /dev/null +++ b/org.argeo.enterprise/src/org/argeo/naming/SharedSecret.java @@ -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()); + } + +} 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 66b6e91e2..957a1c953 100644 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java +++ b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java @@ -173,7 +173,7 @@ public abstract class AbstractUserDirectory implements UserAdmin, UserDirectory private void collectRoles(DirectoryUser user, List 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(); 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 4eab8cd87..90b8daa0e 100644 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUser.java +++ b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUser.java @@ -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> 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> 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