001    /*
002     * Copyright 2010 the original author or authors.
003     * 
004     *  Licensed under the Apache License, Version 2.0 (the "License");
005     *  you may not use this file except in compliance with the License.
006     *  You may obtain a copy of the License at
007     *
008     *      http://www.apache.org/licenses/LICENSE-2.0
009     *
010     *  Unless required by applicable law or agreed to in writing, software
011     *  distributed under the License is distributed on an "AS IS" BASIS,
012     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     *  See the License for the specific language governing permissions and
014     *  limitations under the License.
015     */
016    package com.hs.mail.security.login;
017    
018    import java.security.Principal;
019    import java.text.MessageFormat;
020    import java.util.Hashtable;
021    import java.util.Map;
022    
023    import javax.naming.AuthenticationException;
024    import javax.naming.Context;
025    import javax.naming.NamingEnumeration;
026    import javax.naming.NamingException;
027    import javax.naming.directory.DirContext;
028    import javax.naming.directory.InitialDirContext;
029    import javax.naming.directory.SearchControls;
030    import javax.naming.directory.SearchResult;
031    import javax.security.auth.Subject;
032    import javax.security.auth.callback.Callback;
033    import javax.security.auth.callback.CallbackHandler;
034    import javax.security.auth.callback.NameCallback;
035    import javax.security.auth.callback.PasswordCallback;
036    import javax.security.auth.login.CredentialException;
037    import javax.security.auth.login.LoginException;
038    
039    import org.apache.commons.lang.StringUtils;
040    
041    import com.hs.mail.security.UserPrincipal;
042    
043    /**
044     * A LoginModule that allows for authentication based on LDAP directory.
045     * 
046     * @author Won Chul Doh
047     * @since Aug 7, 2010
048     * 
049     */
050    public class JndiLoginModule extends BasicLoginModule {
051    
052            private String contextFactory;
053            private String url;
054            private String username; 
055            private String password;
056            private String authentication;
057            private String base;
058            private String searchFilter;
059            private String returnAttribute;
060            private boolean subtree;
061            private MessageFormat searchFilterFormat; 
062        protected DirContext context = null;
063    
064            @Override
065            public void initialize(Subject subject, CallbackHandler callbackHandler,
066                            Map<String, ?> sharedState, Map<String, ?> options) {
067                    super.initialize(subject, callbackHandler, sharedState, options);
068                    contextFactory = getOption("context.factory", "com.sun.jndi.ldap.LdapCtxFactory");
069                    url = getOption("url", null);
070                    if (url == null) {
071                            throw new Error("No JNDI URL specified");
072                    }
073                    username = getOption("username", null);
074                    password = getOption("password", null);
075                    authentication = getOption("authentication", "simple");
076                    base = getOption("base", null);
077                    String filter = getOption("searchFilter", "(uid={0})");
078                    searchFilterFormat = new MessageFormat(filter);
079                    returnAttribute = getOption("returnAttribute", null);
080                    subtree = new Boolean(getOption("subtree", "true")).booleanValue();
081            }
082            
083            @Override
084            protected Principal[] validate(Callback[] callbacks) throws LoginException {
085                    String username = ((NameCallback) callbacks[0]).getName();
086                    char[] password = ((PasswordCallback) callbacks[1]).getPassword();
087    
088                    Principal[] principals = new Principal[1];
089                    principals[0] = new UserPrincipal(username);
090                    try {
091                            boolean ok = authenticate(username, String.valueOf(password));
092                            if (!ok)
093                                    throw new CredentialException("Incorrect password for "
094                                                    + username);
095                            else
096                                    return principals;
097                    } catch (Exception e) {
098                            throw (LoginException) new LoginException("LDAP Error")
099                                            .initCause(e);
100                    }
101            }
102    
103            @SuppressWarnings("unchecked")
104            protected boolean authenticate(String username, String password)
105                            throws Exception {
106                    DirContext context = null;
107                    try {
108                            context = open();
109                            searchFilterFormat.format(new String[] { username });
110                            SearchControls constraints = new SearchControls(); 
111                            constraints.setSearchScope(subtree ? SearchControls.SUBTREE_SCOPE
112                                            : SearchControls.ONELEVEL_SCOPE);
113                            if (returnAttribute != null) {
114                                    String[] attribs = StringUtils.split(returnAttribute, ",");
115                                    constraints.setReturningAttributes(attribs);
116                            }
117                            NamingEnumeration ne = context.search(base, searchFilter,
118                                            constraints);
119                            if (ne == null || !ne.hasMore()) {
120                                    return false;
121                            }
122                            SearchResult sr = (SearchResult) ne.next();
123                            if (ne.hasMore()) {
124                                    // Ignore for now
125                            }
126                            // Check the credentials by binding to server
127                            if (bindUser(context, sr.getNameInNamespace(), password)) {
128                                    return true;
129                            } else {
130                                    return true;
131                            }
132                    } catch (NamingException e) {
133                            close(context);
134                            return false;
135                    }
136            }
137            
138            @SuppressWarnings("unchecked")
139            protected DirContext open() throws NamingException {
140                    if (context == null) {
141                            try {
142                                    // Set up the environment for creating the initial context
143                                    Hashtable env = new Hashtable();
144                                    env.put(Context.INITIAL_CONTEXT_FACTORY, contextFactory);
145                                    if (StringUtils.isNotEmpty(username)) {
146                                            env.put(Context.SECURITY_PRINCIPAL, username);
147                                    }
148                                    if (StringUtils.isNotEmpty(password)) {
149                                            env.put(Context.SECURITY_CREDENTIALS, password);
150                                    }
151                                    env.put(Context.PROVIDER_URL, url);
152                                    env.put(Context.SECURITY_AUTHENTICATION, authentication);
153                                    context = new InitialDirContext(env);
154                            } catch (NamingException e) {
155                                    throw e;
156                            }
157                    }
158                    return context;
159            }
160            
161            private boolean bindUser(DirContext context, String dn, String password)
162                            throws NamingException {
163                    boolean isValid = false;
164                    context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn);
165                    context.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
166                    try {
167                            context.getAttributes("", null);
168                            isValid = true;
169                    } catch (AuthenticationException e) {
170                    }
171                    if (StringUtils.isNotEmpty(this.username)) {
172                            context.addToEnvironment(Context.SECURITY_PRINCIPAL, this.username);
173                    } else {
174                            context.removeFromEnvironment(Context.SECURITY_PRINCIPAL);
175                    }
176                    if (StringUtils.isNotEmpty(this.password)) {
177                            context.addToEnvironment(Context.SECURITY_CREDENTIALS,
178                                            this.password);
179                    } else {
180                            context.removeFromEnvironment(Context.SECURITY_CREDENTIALS);
181                    }
182                    return isValid;
183            }
184    
185            protected void close(DirContext context) {
186                    if (context != null) {
187                            try {
188                                    context.close();
189                                    context = null;
190                            } catch (Exception e) {
191                            }
192                    }
193            }
194            
195    }