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;
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;
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;
}
// 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 {
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;
--- /dev/null
+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());
+ }
+ }
+
+}
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"),
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;
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) {
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;
--- /dev/null
+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());
+ }
+
+}
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();
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;
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;
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 {
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[]))
byte[] hashedPassword = hash(password);
return put(LdapAttrs.userPassword.name(), hashedPassword);
}
+ if (key.startsWith("X-")) {
+ return put(LdapAttrs.authPassword.name(), value);
+ }
userAdmin.checkEdit();
if (!isEditing())
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