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 }