/**
* (C) Copyright 2013 Jabylon (http://www.jabylon.org) and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.jabylon.security.internal;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
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.login.LoginException;
import javax.security.auth.spi.LoginModule;
import org.jabylon.security.CommonPermissions;
import org.jabylon.security.GroupMemberAttribute;
import org.jabylon.security.SubjectAttribute;
import org.jabylon.users.UsersPackage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LDAPLoginModule implements LoginModule {
/** the ldap server url */
public static final String KEY_LDAP = "ldap";
/** the ldap server port */
public static final String KEY_LDAP_PORT = "ldap.port";
/** the uid attribute of a user */
public static final String KEY_USER_NAME = "user.id";
/** the full name attribute of a user */
public static final String KEY_USER_FULL_NAME = "user.name";
/** the email attribute of a user */
public static final String KEY_USER_MAIL = "user.mail";
/** the root dn to query agains */
public static final String KEY_ROOT_DN = "root.dn";
/** the ldap manager id */
public static final String KEY_MANAGER = "manager";
/** the ldap manager password */
public static final String KEY_MANAGER_PASSWORD = "manager.password";
/** the attribute that determines what groups a user is in */
public static final String KEY_MEMBER_OF = "member.of";
/** the attribute that determines how the group is named */
public static final String KEY_GROUP_NAME = "group.name";
private Subject subj;
private CallbackHandler cbHandler;
private Map<String, ?> options;
private boolean authenticated;
private String user;
private static final Logger logger = LoggerFactory.getLogger(LDAPLoginModule.class);
private DirContext ctx;
private String email;
private String fullName;
private Set<String> groups;
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
this.subj = subject;
this.cbHandler = callbackHandler;
this.options = options;
}
@Override
public boolean login() throws LoginException {
NameCallback nameCallback = new NameCallback("User:");
PasswordCallback passwordCallback = new PasswordCallback("Password:", false);
try {
cbHandler.handle(new Callback[] { nameCallback, passwordCallback });
} catch (Exception e) {
logger.error("Login failed", e);
}
user = nameCallback.getName();
String pw = null;
if (passwordCallback.getPassword() != null) {
pw = String.valueOf(passwordCallback.getPassword());
}
this.authenticated = checkLogin(user, pw);
return this.authenticated;
}
private boolean checkLogin(String user, String pw) {
ctx = createContext((String)options.get(KEY_MANAGER), (String)options.get(KEY_MANAGER_PASSWORD));
if(ctx!=null) {
try {
String userDN = findUser(user, ctx);
if(userDN!=null)
{
DirContext context = createContext(userDN, pw);
if(context!=null) {
context.close();
return true;
}
}
} catch (NamingException e) {
logger.error("LDAP search failed for user "+user,e);
}
finally {
try {
ctx.close();
} catch (NamingException e) {
logger.error("Failed to close directory context",e);
}
}
}
return false;
}
private String findUser(String user, DirContext ctx) throws NamingException {
SearchControls controls = new SearchControls();
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
controls.setReturningAttributes(getUserAttributes());
String filter = MessageFormat.format("({0}={1})",options.get(KEY_USER_NAME),user);
NamingEnumeration<SearchResult> result = ctx.search("", filter, controls);
if(result.hasMore()){
SearchResult searchResult = result.next();
String userId = searchResult.getNameInNamespace();
Attributes attributes = searchResult.getAttributes();
if(options.get(KEY_USER_MAIL) instanceof String) {
Attribute attribute = attributes.get((String) options.get(KEY_USER_MAIL));
if(attribute!=null)
email = (String) attribute.get();
}
if(options.get(KEY_USER_FULL_NAME) instanceof String) {
Attribute attribute = attributes.get((String) options.get(KEY_USER_FULL_NAME));
if(attribute!=null)
fullName = (String) attribute.get();
}
if(options.get(KEY_USER_FULL_NAME) instanceof String) {
Attribute attribute = attributes.get(((String)options.get(KEY_MEMBER_OF)));
if(attribute!=null) {
NamingEnumeration<?> groupsEnum = attribute.getAll();
groups = new HashSet<String>();
groups.add(CommonPermissions.ROLE_LDAP_REGISTERED);
while (groupsEnum.hasMoreElements()) {
Object object = groupsEnum.nextElement();
if (object instanceof String) {
String groupName = (String) object;
groups.add(lookupGroupName(ctx, groupName));
}
}
}
}
return userId;
}
return null;
}
private String lookupGroupName(DirContext ctx, String groupName) throws NamingException
{
try {
String nameWithoutRootDn = groupName.substring(0, groupName.indexOf((String)options.get(KEY_ROOT_DN))-1);
Attributes attr = ctx.getAttributes(nameWithoutRootDn, new String[] {(String)options.get(KEY_GROUP_NAME)});
String groupDisplayName = (String)attr.get((String)options.get(KEY_GROUP_NAME)).get();
return groupDisplayName;
} catch (Exception e) {
return groupName;
}
}
private String[] getUserAttributes() {
List<String> attributes = new ArrayList<String>();
if(options.get(KEY_USER_MAIL) instanceof String)
attributes.add((String) options.get(KEY_USER_MAIL));
if(options.get(KEY_USER_FULL_NAME) instanceof String)
attributes.add((String) options.get(KEY_USER_FULL_NAME));
if(options.get(KEY_MEMBER_OF) instanceof String)
attributes.add((String) options.get(KEY_MEMBER_OF));
return attributes.toArray(new String[attributes.size()]);
}
public DirContext createContext(String userDN, String userPassword) {
if(!options.containsKey(KEY_LDAP) || !options.containsKey(KEY_LDAP_PORT)) {
logger.debug("No LDAP url configured, skipping LDAP authentication");
return null;
}
DirContext ctx = null;
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
String providerUrl = MessageFormat.format("ldap://{0}:{1}/{2}", options.get(KEY_LDAP), options.get(KEY_LDAP_PORT), options.get(KEY_ROOT_DN));
env.put(Context.PROVIDER_URL, providerUrl);
if(userDN!=null)
env.put(Context.SECURITY_PRINCIPAL, userDN);
env.put(Context.SECURITY_AUTHENTICATION, "simple");
if(userPassword!=null)
env.put(Context.SECURITY_CREDENTIALS, userPassword);
try {
ctx = new InitialDirContext(env);
} catch (NamingException e) {
logger.warn("Cannot bind user with userDN = " + userDN + " with exception " + e.getLocalizedMessage());
}
return ctx;
}
@Override
public boolean commit() throws LoginException {
if (this.authenticated) {
subj.getPublicCredentials().add(user);
if(email!=null && !email.isEmpty())
subj.getPublicCredentials().add(new SubjectAttribute(UsersPackage.Literals.USER__EMAIL, email));
if(fullName!=null && !fullName.isEmpty())
subj.getPublicCredentials().add(new SubjectAttribute(UsersPackage.Literals.USER__DISPLAY_NAME, fullName));
if(groups!=null && !groups.isEmpty())
subj.getPublicCredentials().add(new GroupMemberAttribute(groups));
subj.getPublicCredentials().add(new SubjectAttribute(UsersPackage.Literals.USER__TYPE, CommonPermissions.AUTH_TYPE_LDAP));
} else {
subj.getPublicCredentials().remove(user);
return false;
}
return true;
}
@Override
public boolean abort() throws LoginException {
this.authenticated = false;
return true;
}
@Override
public boolean logout() throws LoginException {
this.authenticated = false;
return true;
}
}