/*
* File: LdapModule.java
*
* Copyright 2009 Muradora
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.fcrepo.server.jaas.auth.module;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import org.apache.log4j.Logger;
import org.fcrepo.server.jaas.auth.UserPrincipal;
import org.fcrepo.server.jaas.util.Base64;
public class LdapModule
implements LoginModule {
private static final Logger log = Logger.getLogger(LdapModule.class);
private Subject subject = null;
private CallbackHandler handler = null;
// private Map<String, ?> sharedState = null;
private Map<String, ?> options = null;
private String username = null;
private UserPrincipal principal = null;
private Map<String, Set<String>> attributes = null;
private boolean debug = false;
private boolean successLogin = false;
public void initialize(Subject subject,
CallbackHandler handler,
Map<String, ?> sharedState,
Map<String, ?> options) {
this.subject = subject;
this.handler = handler;
// this.sharedState = sharedState;
this.options = options;
String debugOption = (String) this.options.get("debug");
if (debugOption != null && "true".equalsIgnoreCase(debugOption)) {
debug = true;
}
attributes = new HashMap<String, Set<String>>();
if (debug) {
log.debug("login module initialised: " + this.getClass().getName());
}
}
public boolean login() throws LoginException {
if (debug) {
log.debug(this.getClass().getName() + " login called.");
}
// The only 2 callback types that are supported.
Callback[] callbacks = new Callback[2];
callbacks[0] = new NameCallback("username");
callbacks[1] = new PasswordCallback("password", false);
String password = null;
try {
// sets the username and password from the callback handler
handler.handle(callbacks);
username = ((NameCallback) callbacks[0]).getName();
char[] passwordCharArray =
((PasswordCallback) callbacks[1]).getPassword();
password = new String(passwordCharArray);
} catch (IOException ioe) {
ioe.printStackTrace();
throw new LoginException("IOException occured: " + ioe.getMessage());
} catch (UnsupportedCallbackException ucbe) {
ucbe.printStackTrace();
throw new LoginException("UnsupportedCallbackException encountered: "
+ ucbe.getMessage());
}
successLogin = authenticate(username, password);
return successLogin;
}
public boolean commit() throws LoginException {
if (!successLogin) {
return false;
}
try {
subject.getPrincipals().add(principal);
subject.getPublicCredentials().add(attributes);
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
return true;
}
public boolean abort() throws LoginException {
try {
clear();
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
return true;
}
public boolean logout() throws LoginException {
try {
clear();
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
return true;
}
private void clear() {
subject.getPrincipals().clear();
subject.getPublicCredentials().clear();
subject.getPrivateCredentials().clear();
principal = null;
username = null;
}
private boolean authenticate(String username, String password) {
try {
// required attributes
String hostUrl = getOption("host.url", true);
String authType = getOption("auth.type", true);
String bindMode = getOption("bind.mode", true);
// retrieve the attributes to fetch from ldap
String[] attrList = null;
String attrsFetch = getOption("attrs.fetch", false);
if (attrsFetch != null && !"".equals(attrsFetch)) {
attrList = attrsFetch.split(" *, *");
} else if (attrList == null || attrList.length == 0) {
attrList = new String[] {"cn", "sn", "mail", "displayName"};
}
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.SECURITY_AUTHENTICATION, authType);
env.put(Context.PROVIDER_URL, hostUrl);
if ("bind".equals(bindMode)) {
if (debug) {
log.debug("authenticating with mode: " + bindMode);
}
return bind(username, password, env, attrList);
} else if ("bind-search-compare".equals(bindMode)) {
if (debug) {
log.debug("authenticating with mode: " + bindMode);
}
return bindSearchX(username, password, env, attrList, false);
} else if ("bind-search-bind".equals(bindMode)) {
if (debug) {
log.debug("authenticating with mode: " + bindMode);
}
return bindSearchX(username, password, env, attrList, true);
}
} catch (NamingException ne) {
log.error(ne.getMessage());
} catch (Exception e) {
log.error(e.getMessage());
}
return false;
}
private boolean bind(String username,
String password,
Hashtable<String, String> env,
String[] attrList) throws Exception {
String bindFilter = getOption("bind.filter", true);
String dn = MessageFormat.format(bindFilter, username);
if (debug) {
log.debug("authenticating user: " + dn);
}
env.put(Context.SECURITY_PRINCIPAL, dn);
env.put(Context.SECURITY_CREDENTIALS, password);
DirContext ctx = new InitialDirContext(env);
// we've successfully bound at this point. Auth is good.
// we instantiate the principal.
Attributes attributes = ctx.getAttributes(dn, attrList);
makePrincipal(username, attributes);
return true;
}
private boolean bindSearchX(String username,
String password,
Hashtable<String, String> env,
String[] attrList,
boolean bind) throws Exception {
String bindUser = getOption("bind.user", true);
String bindPass = getOption("bind.pass", true);
String searchBase = getOption("search.base", true);
String searchFilter = getOption("search.filter", true);
env.put(Context.SECURITY_PRINCIPAL, bindUser);
env.put(Context.SECURITY_CREDENTIALS, bindPass);
DirContext ctx = null;
try {
ctx = new InitialDirContext(env);
} catch (NamingException ne) {
log.error("Failed to bind as bindUser: " + bindUser);
throw ne;
}
// ensure we have the userPassword attribute at a minimum
String[] attributeList = null;
if (attrList == null) {
attributeList = new String[] {"userPassword"};
} else if (!Arrays.asList(attrList).contains("userPassword")) {
attributeList = new String[attrList.length + 1];
for (int x = 0; x < attrList.length; x++) {
attributeList[x] = attrList[x];
}
attributeList[attrList.length] = "userPassword";
}
SearchControls sc = new SearchControls();
sc.setSearchScope(SearchControls.SUBTREE_SCOPE);
sc.setReturningAttributes(attributeList);
sc.setDerefLinkFlag(true);
sc.setReturningObjFlag(false);
sc.setTimeLimit(5000);
String filter = MessageFormat.format(searchFilter, username);
NamingEnumeration<SearchResult> results =
ctx.search(searchBase, filter, sc);
if (!results.hasMore()) {
log.warn("no valid user found.");
return false;
}
SearchResult result = results.next();
if (debug) {
log.debug("authenticating user: " + result.getNameInNamespace());
}
if (bind) {
// setup user context for binding
Hashtable<String, String> userEnv = new Hashtable<String, String>();
userEnv.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
userEnv.put(Context.SECURITY_AUTHENTICATION, getOption("auth.type",
true));
userEnv.put(Context.PROVIDER_URL, getOption("host.url", true));
userEnv
.put(Context.SECURITY_PRINCIPAL, result
.getNameInNamespace());
userEnv.put(Context.SECURITY_CREDENTIALS, password);
try {
new InitialDirContext(userEnv);
} catch (NamingException ne) {
log.error("failed to authenticate user: "
+ result.getNameInNamespace());
throw ne;
}
} else {
// get userPassword attribute
Attribute up = result.getAttributes().get("userPassword");
if (up == null) {
log.error("unable to read userPassword attribute for: "
+ result.getNameInNamespace());
return false;
}
byte[] userPasswordBytes = (byte[]) up.get();
String userPassword = new String(userPasswordBytes);
// compare passwords - also handles encodings
if (!passwordsMatch(password, userPassword)) {
return false;
}
}
Attributes attributes = result.getAttributes();
makePrincipal(username, attributes);
return true;
}
private void makePrincipal(String username, Attributes ldapAttributes)
throws NamingException {
principal = new UserPrincipal(username);
NamingEnumeration<? extends Attribute> attributeList =
ldapAttributes.getAll();
while (attributeList.hasMore()) {
Attribute attribute = attributeList.next();
NamingEnumeration<?> values = attribute.getAll();
while (values.hasMore()) {
Object value = values.next();
if (value instanceof String) {
Set<String> aValues = attributes.get(attribute.getID());
if (aValues == null) {
aValues = new HashSet<String>();
attributes.put(attribute.getID(), aValues);
}
aValues.add((String) value);
if (debug) {
log.debug("added to principal: " + attribute.getID()
+ "/" + value);
}
}
}
}
}
/**
* Method to compare two passwords. The method attempts to encode the user
* password based on the ldap password encoding extracted from the storage
* format (e.g. {SHA}g0bbl3d3g00ka12@#19/=).
*
* @param userPassword
* the password that the user entered
* @param ldapPassword
* the password from the ldap directory
* @return true if userPassword equals ldapPassword with respect to encoding
*/
private static boolean passwordsMatch(String userPassword,
String ldapPassword) {
final String LDAP_PASSWORD_REGEX = "\\{(.+)\\}(.+)";
Pattern p = Pattern.compile(LDAP_PASSWORD_REGEX);
Matcher m = p.matcher(ldapPassword);
boolean match = false;
if (m.find() && m.groupCount() == 2) {
// if password is encoded in the LDAP, encode the password before
// compare
String encoding = m.group(1);
String password = m.group(2);
if (log.isDebugEnabled()) {
log.debug("Encoding: " + encoding + ", Password: " + password);
}
MessageDigest digest = null;
try {
digest = MessageDigest.getInstance(encoding.toUpperCase());
} catch (NoSuchAlgorithmException e) {
log.error("Unsupported Algorithm used: " + encoding);
log.error(e.getMessage());
return false;
}
byte[] resultBytes = digest.digest(userPassword.getBytes());
byte[] result = Base64.encodeBytesToBytes(resultBytes);
String pwd = new String(password);
String ldp = new String(result);
match = pwd.equals(ldp);
} else {
// if passwords are not encoded, just do raw compare
match = userPassword.equals(ldapPassword);
}
return match;
}
private String getOption(String key, boolean required) throws Exception {
String value = (String) options.get(key);
if (required && (value == null || "".equals(value))) {
throw new Exception("Missing required option in JAAS Config file: "
+ key);
}
return value;
}
}