View Javadoc

1   /*
2    jGuard is a security framework based on top of jaas (java authentication and authorization security).
3    it is written for web applications, to resolve simply, access control problems.
4    version $Name$
5    http://sourceforge.net/projects/jguard/
6   
7    Copyright (C) 2004  Charles GAY
8   
9    This library is free software; you can redistribute it and/or
10   modify it under the terms of the GNU Lesser General Public
11   License as published by the Free Software Foundation; either
12   version 2.1 of the License, or (at your option) any later version.
13  
14   This library is distributed in the hope that it will be useful,
15   but WITHOUT ANY WARRANTY; without even the implied warranty of
16   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17   Lesser General Public License for more details.
18  
19   You should have received a copy of the GNU Lesser General Public
20   License along with this library; if not, write to the Free Software
21   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
22  
23  
24   jGuard project home page:
25   http://sourceforge.net/projects/jguard/
26  
27   */
28  package net.sf.jguard.ext.authentication.loginmodules;
29  
30  import java.text.MessageFormat;
31  import java.util.ArrayList;
32  import java.util.HashMap;
33  import java.util.HashSet;
34  import java.util.Hashtable;
35  import java.util.Iterator;
36  import java.util.List;
37  import java.util.Map;
38  import java.util.Set;
39  
40  import java.util.logging.Level;
41  import javax.naming.CompositeName;
42  import javax.naming.Context;
43  import javax.naming.InitialContext;
44  import javax.naming.Name;
45  import javax.naming.NameParser;
46  import javax.naming.NamingEnumeration;
47  import javax.naming.NamingException;
48  import javax.naming.directory.Attribute;
49  import javax.naming.directory.Attributes;
50  import javax.naming.directory.DirContext;
51  import javax.naming.directory.SearchControls;
52  import javax.naming.directory.SearchResult;
53  import javax.naming.ldap.Control;
54  import javax.naming.ldap.InitialLdapContext;
55  import javax.security.auth.Subject;
56  import javax.security.auth.callback.CallbackHandler;
57  import javax.security.auth.login.FailedLoginException;
58  import javax.security.auth.login.LoginException;
59  import javax.security.auth.spi.LoginModule;
60  
61  import net.sf.jguard.core.CoreConstants;
62  import net.sf.jguard.core.authentication.credentials.JGuardCredential;
63  import net.sf.jguard.ext.SecurityConstants;
64  import net.sf.jguard.ext.util.FastBindConnectionControl;
65  import net.sf.jguard.ext.util.JNDIUtils;
66  import org.slf4j.Logger;
67  import org.slf4j.LoggerFactory;
68  
69  /**
70   * JNDI - related LoginModule.
71   * @author <a href="mailto:diabolo512@users.sourceforge.net">Charles Gay</a>
72   * @see LoginModule
73   */
74  public class JNDILoginModule extends UserLoginModule implements LoginModule {
75  
76  	private static final String USER_DN = "userDN";
77  	private static final String CONTEXTFORCOMMIT = "contextforcommit";
78  	private static final String JNDI = "jndi";
79  	private static final String TIMELIMIT = "timelimit";
80  	private static final String SEARCHSCOPE = "searchscope";
81  	private static final String RETURNINGOBJFLAG = "returningobjflag";
82  	private static final String RETURNINGATTRIBUTES = "returningattributes";
83  	private static final String DEREFLINKFLAG = "dereflinkflag";
84  	private static final String COUNTLIMIT = "countlimit";
85  	private static final String SEARCHCONTROLS = "searchcontrols.";
86  	//constants
87  	private static final String PREAUTH = "preauth.";
88  	private static final String AUTH = "auth.";
89  	private static final String FAST_BIND_CONNECTION = "fastBindConnection";
90  	private static final String SEARCH_FILTER = "search.filter";
91  	private static final String SEARCH_BASE_DN = "search.base.dn";
92  	
93  	
94  	private static final Logger logger = LoggerFactory.getLogger(JNDILoginModule.class.getName());
95  	
96  	
97  	private DirContext preAuthContext = null;
98  	private DirContext authContext = null;
99  	
100 	//JNDI SearchControls
101 	private SearchControls preAuthSearchControls = null;
102 	
103 	private Map authOpts = null;
104 	private Map preAuthOpts = null;
105 	private Map preAuthSearchControlsOpts = null;
106 	
107 	private Set credentials = null;
108 	
109 	/**
110 	 *
111 	 * @param subj
112 	 * @param cbkHandler
113 	 * @param sState
114 	 * @param opts
115 	 */
116     @Override
117 	public void initialize(Subject subj, CallbackHandler cbkHandler, Map sState, Map opts) {
118 		super.initialize(subj, cbkHandler, sState, opts);
119 		preAuthOpts = new HashMap();
120 		preAuthSearchControlsOpts = new HashMap();
121 		authOpts = new HashMap();
122 		
123 		//populate specialized maps with opts
124 		fillOptions();
125 		
126 	}
127 	
128 	private DirContext getContext(Map opts) throws LoginException{
129 		DirContext context = null;
130 		if(opts.containsKey(JNDILoginModule.JNDI)){
131 			try {
132 				Context initDirContext = new InitialContext();
133 				context = (DirContext) initDirContext.lookup((String)opts.get(JNDILoginModule.JNDI));
134 			} catch (NamingException e) {
135 				throw new LoginException(" we cannot grab the default initial context ");
136 			}
137 			
138 		}else{
139 			Control[] LDAPcontrols = getLDAPControls(opts);
140 			try {
141 				context = new InitialLdapContext(new Hashtable(opts),LDAPcontrols);
142 			} catch (NamingException e) {
143 				throw new LoginException(e.getMessage());
144 			}
145 		}
146 		if(context == null){
147 			throw new LoginException(" we cannot grab the default initial context ");
148 		}
149 		return context;
150 		
151 	}
152 	
153 	/**
154 	 * grab <strong>opts</strong> options and fill preAuthOpts , preAuthSearchControlsOpts and 
155 	 * authOpts options.
156 	 * @param opts
157 	 * @param preAuthOpts
158 	 * @param preAuthSearchControlsOpts
159 	 * @param authOpts
160 	 */
161 	private void fillOptions() {
162 		Iterator entriesIterator = options.entrySet().iterator();
163 		while(entriesIterator.hasNext()){
164 			Map.Entry entry = (Map.Entry)entriesIterator.next();
165 			String key  = (String)entry.getKey();
166 			String value = (String)entry.getValue();
167 			if(key.startsWith(JNDILoginModule.PREAUTH)){
168 				key = key.substring(8, key.length());
169 				if(key.startsWith(JNDILoginModule.SEARCHCONTROLS)){
170 					key = key.substring(15, key.length());
171 				preAuthSearchControlsOpts.put(key, value);	
172 				}else{
173 				preAuthOpts.put(key, value);
174 				}
175 			}else if(key.startsWith(JNDILoginModule.AUTH)){
176 				key = key.substring(5, key.length());
177 				authOpts.put(key, value);
178 			}
179 		}
180 	}
181 
182 	/**
183 	 * @return true if success, false if ignored.
184 	 * @throws LoginException
185 	 *             when it fails
186 	 */
187     @Override
188 	public boolean login() throws LoginException {
189 		super.login();
190 		if(CoreConstants.GUEST.equals(login)){
191 			//when user is a guest, we have no need to use this loginmodule
192 			loginOK = false;
193 			return false;
194 		}
195 		
196 		
197 		
198 		//userDN is null(not configured)  if preAuth is configured 
199 		//because preAuth is used to find dynamically
200 		//the DN of the user
201 		String userDN = (String)authOpts.get(USER_DN);
202 		if(preAuthOpts.size()==0 &&(userDN==null || userDN.equals(""))){
203 			throw new IllegalArgumentException(" you've configured the JNDILoginmodule in 'auth' mode (options starting by 'preauth.' are not present).\n 'auth.userDN' option used to find the user LDAP Entry is lacking or is empty ");
204 		}
205 		
206 		
207 		userDN = getuserDN(userDN, login);
208 		
209 		if (userDN != null && !equals("")) {
210 			authOpts.put(Context.SECURITY_PRINCIPAL, userDN);
211 			authOpts.put(Context.SECURITY_CREDENTIALS, new String(password));
212 			try {
213 				authContext = getContext(authOpts);
214 			}  finally {
215 				try {
216 					if(authContext!= null){
217 						authContext.close();
218 					}
219 				} catch (NamingException e) {
220 					throw new FailedLoginException(e.getMessage());
221 				}
222 			}
223 		// authentication succeed
224 		}else{
225 			loginOK = false;
226 			throw new LoginException(" Distinguished name is null or empty ");
227 		}
228 		//like we've already check user credentials against the directory
229 		//password check must not be done one more time.
230 		sharedState.put(SecurityConstants.SKIP_PASSWORD_CHECK, "true");
231 		logger.info( " JNDI login phase succeed for user "+login);
232 		return true;
233 	}
234 
235 	/**
236 	 * grab the Distinguished Name of the LDAP entry related to the user.
237 	 * either a pre-authentication can be needed to execute an LDAP search, or
238 	 * the DN can be calculated from the LDAP filter configured.
239 	 * @param userDN
240 	 * @param escapedLogin
241 	 * @return
242 	 * @throws LoginException
243 	 */
244 	private String getuserDN(String userDN, String login) throws LoginException {
245 		//we prevent LDAP injection from the login
246 		String escapedLogin = JNDIUtils.escapeDn(login);
247 		Object[] args = {escapedLogin};
248 		if(preAuthOpts.size()>0){
249 
250 			//preauth initialization
251 			try {
252 				preAuthContext = getContext(preAuthOpts);
253 			} catch (LoginException e) {
254 				loginOK = false;
255 				throw new IllegalArgumentException(e.getMessage());
256 			}
257 			preAuthSearchControlsOpts.put(COUNTLIMIT, "1");
258 			preAuthSearchControls = getSearchControls(preAuthSearchControlsOpts);
259 			
260 			try{
261 			userDN  = preAuthSearch(preAuthContext, preAuthSearchControls);
262 			}catch(LoginException e){
263 				loginOK = false;
264 				throw e;
265 			}finally{
266 				try {
267 					preAuthContext.close();
268 				} catch (NamingException e) {
269 					logger.error(e.getMessage());
270 				}
271 			}
272 		}else{
273 			userDN = MessageFormat.format(userDN, args);
274 			userDN = JNDIUtils.escapeDn(userDN);
275 		}
276 		return userDN;
277 	}
278 
279 
280 
281 	/**
282 	 * @return <strong>true</strong> if success, <strong>false</strong> if ignored,
283 	 *  <strong>LoginException</strong> when it fails.
284 	 */
285     @Override
286 	public boolean commit() throws LoginException {
287 		if(!loginOK){
288 			return false;
289 		}
290 		if(options.containsKey(JNDILoginModule.CONTEXTFORCOMMIT)&&options.get(JNDILoginModule.CONTEXTFORCOMMIT).equals("true")){
291 			credentials = grabAttributes(getContext(authOpts),(String)authOpts.get(USER_DN));
292 		}
293 		
294 		if(credentials!=null){
295 			Set privateCredentials = subject.getPrivateCredentials();
296 			privateCredentials.addAll(credentials);
297 		}
298 		return true;
299 	}
300 
301 	/**
302 	 * grab the attributes of the specified LDAP entry with userDN
303 	 * and return a credential Set.
304 	 * @param contextUsedForCommit
305 	 * @param userDN
306 	 * @return
307 	 * @throws LoginException
308 	 */
309 	private Set grabAttributes(DirContext contextUsedForCommit,String userDN) throws LoginException {
310 		DirContext userCtxt = null;
311 		Set creds = new HashSet();
312 		try {
313 			userCtxt = (DirContext) contextUsedForCommit.lookup(getuserDN(userDN,login));
314 			if(userCtxt==null){
315 				throw new FailedLoginException("login.user.does.not.exist");
316 			}
317 
318 			Attributes attributes = userCtxt.getAttributes("");
319 			creds = grabCredentials(attributes);
320 		} catch (NamingException e) {
321 			throw new LoginException(e.getMessage());
322 		}finally{
323 			try {
324 				if(userCtxt!=null){
325 					userCtxt.close();
326 				}
327 			} catch (NamingException e) {
328 				throw new LoginException(e.getMessage());
329 			}
330 		}
331 		
332 		return creds;
333 	}
334 	
335 	/**
336 	 * grab attributes of the LDAP entry related to the user and
337 	 * build a credential Set which contains attributes informations.
338 	 * @param atts
339 	 * @return
340 	 * @throws NamingException
341 	 */
342 	private  Set grabCredentials(Attributes atts) throws NamingException {
343 		Set credentialSet = new HashSet();
344 		NamingEnumeration enumeration = atts.getAll();
345 
346 		while(enumeration.hasMore()){
347 			Attribute attribute = (Attribute)enumeration.next();
348 			String key = attribute.getID();
349 			String value= JNDIUtils.getAttributeValue(attribute);
350 			JGuardCredential credential = new JGuardCredential();
351 			credential.setName(key);
352 			credential.setValue(value);
353 			credentialSet.add(credential);
354 		}
355 		
356 		return credentialSet;
357 	}
358 	
359 	/**
360 	 * search the Distinguished Name(DN) of the User LDAP entry.
361 	 * @return Distinguised Name of the User found
362 	 * @throws LoginException 
363 	 */
364 	private String preAuthSearch(DirContext context,SearchControls controls) throws LoginException {
365 		NamingEnumeration results = null;
366 		String dn = null;
367 		String baseDN = null;
368 		String searchFilter  = null;
369 		try {
370 				String[] filterArgs = new String[]{super.login};
371 				Hashtable opts = context.getEnvironment();
372 				baseDN = (String)opts.get(JNDILoginModule.SEARCH_BASE_DN);
373 				searchFilter = (String)opts.get(JNDILoginModule.SEARCH_FILTER);
374 				
375 				results = context.search(baseDN,searchFilter,filterArgs, controls);
376 				int userFound = 0;
377 				boolean grabInformations = false;
378 				String contextforcommit = (String)options.get(JNDILoginModule.CONTEXTFORCOMMIT);
379 				if (contextforcommit!=null && "preauth".equals(contextforcommit)){
380 					grabInformations = true;
381 				}
382 				while(results.hasMore()){
383 					SearchResult result = (SearchResult) results.next();
384 					//the dn grabbed with getName follow the CompositeName syntax
385 					dn = result.getName();
386 					//grab the name parser of the LDAP directory
387 					NameParser pn = context.getNameParser("");
388 					//clearly declare the String as a CompositeName
389 					CompositeName compName = new CompositeName(result.getName());
390 					
391 					//grab the Name instance of the CompoundName (first position in the CompositeName)
392 					Name entryName = pn.parse(compName.get(0));
393 					//grab the String representation of the CompoundName
394 					//that's a weird way to escape special characters hadnled normally 
395 					//by Active Directory which has got a special meaning for JNDI
396 					// but it works...
397 					dn = entryName.toString();
398 					
399 					if(grabInformations){
400 						credentials = grabCredentials(result.getAttributes());
401 					}
402 					
403 					userFound++;
404 				}
405 				if(userFound>1){
406 					logger.warn("more than one Distinguished Name has been found in the Directory for the user="+login);
407 					throw new FailedLoginException("login.error");
408 				}
409 		} catch (NamingException e) {
410 			throw new LoginException(" a naming exception has been raised when we are looking for the user Distinguished Name "+e.getMessage());
411 		}finally{
412 			try {
413 				context.close();
414 			} catch (NamingException e) {
415 				throw new LoginException(e.getMessage());
416 			}
417 		}
418 		if(dn==null){
419 			throw new FailedLoginException("login.error");
420 		}
421 		return dn;
422 	}
423 	
424 	private SearchControls getSearchControls(Map opts){
425 		SearchControls controls = new SearchControls();
426 		Iterator itEntries = opts.entrySet().iterator();
427 		while(itEntries.hasNext()){
428 			Map.Entry entry = (Map.Entry)itEntries.next();
429 			String key = (String)entry.getKey();
430 			String value = (String)entry.getValue();
431 			if(JNDILoginModule.COUNTLIMIT.equals(key)){
432 				long countLimit = Long.parseLong(value);
433 				controls.setCountLimit(countLimit);
434 			}else if(JNDILoginModule.DEREFLINKFLAG.equals(key)){
435 				boolean derefLinkFlag = Boolean.valueOf(value).booleanValue();
436 				controls.setDerefLinkFlag(derefLinkFlag);
437 			}else if(JNDILoginModule.RETURNINGATTRIBUTES.equals(key)){
438 				String[] returningAttributes = value.split("#");
439 				controls.setReturningAttributes(returningAttributes);
440 			}else if(JNDILoginModule.RETURNINGOBJFLAG.equals(key)){
441 				boolean returningobjflag = Boolean.valueOf(value).booleanValue();
442 				controls.setReturningObjFlag(returningobjflag);
443 			}else if(JNDILoginModule.SEARCHSCOPE.equals(key)){
444 				int scope = Integer.parseInt(value);
445 				controls.setSearchScope(scope);
446 			}else if(JNDILoginModule.TIMELIMIT.equals(key)){
447 				int timelimit = Integer.parseInt(value);
448 				controls.setTimeLimit(timelimit);
449 			}
450 		}
451 		
452 		return controls;
453 	}
454 
455 	
456 	private Control[] getLDAPControls(Map opts){
457 		List ldapControls = new ArrayList();
458 		if(opts.containsKey(JNDILoginModule.FAST_BIND_CONNECTION)
459 			&& "true".equalsIgnoreCase((String)opts.get(JNDILoginModule.FAST_BIND_CONNECTION))){
460 			ldapControls.add(new FastBindConnectionControl());
461 		}
462 		return (Control[]) ldapControls.toArray(new Control[ldapControls.size()]);
463 		
464 	}
465 
466 }