View Javadoc

1   /*
2    * Copyright 2004-2006 the Seasar Foundation and the Others.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 
13   * either express or implied. See the License for the specific language
14   * governing permissions and limitations under the License.
15   */
16  package org.seasar.tuigwaa.security.auth;
17  
18  import java.io.IOException;
19  import java.util.ArrayList;
20  import java.util.Hashtable;
21  import java.util.Iterator;
22  import java.util.List;
23  import java.util.Map;
24  import java.util.MissingResourceException;
25  import java.util.ResourceBundle;
26  import java.util.regex.Matcher;
27  import java.util.regex.Pattern;
28  
29  import javax.naming.AuthenticationException;
30  import javax.naming.Context;
31  import javax.naming.InitialContext;
32  import javax.naming.NamingEnumeration;
33  import javax.naming.NamingException;
34  import javax.naming.directory.Attribute;
35  import javax.naming.directory.Attributes;
36  import javax.naming.directory.BasicAttributes;
37  import javax.naming.directory.DirContext;
38  import javax.naming.directory.InitialDirContext;
39  import javax.naming.directory.SearchResult;
40  import javax.security.auth.Subject;
41  import javax.security.auth.callback.Callback;
42  import javax.security.auth.callback.CallbackHandler;
43  import javax.security.auth.callback.NameCallback;
44  import javax.security.auth.callback.PasswordCallback;
45  import javax.security.auth.callback.UnsupportedCallbackException;
46  import javax.security.auth.login.FailedLoginException;
47  import javax.security.auth.login.LoginException;
48  import javax.security.auth.spi.LoginModule;
49  
50  import org.seasar.tuigwaa.security.DirectoryUtils;
51  
52  
53  /***
54   * @author someda
55   */
56  public class TgwLoginModule implements LoginModule {
57  
58  	private Subject subject;
59  	private CallbackHandler callbackHandler;
60  	private Map sharedState;
61  	
62  	private Hashtable environment = new Hashtable();
63  	
64  	private static final String SYSTEM_PROPERTY_PATH = "tuigwaa";
65  	private static ResourceBundle rb;	
66  	static{
67  		try{
68  			rb = ResourceBundle.getBundle(SYSTEM_PROPERTY_PATH);
69  		}catch(MissingResourceException mre){
70  			// do nothing, use default values
71  			mre.printStackTrace();
72  		}		
73  	}
74  	
75  	private String username;
76  	private char[] password;
77  		
78  	// LoginModule Supported Properties
79  	private boolean tryFirstPass;
80  	private static final String TRY_FIRST_PASS = "try_first_pass";
81  	
82  	private boolean useFirstPass;	
83  	private static final String USE_FIRST_PASS = "use_first_pass";
84  	
85  	// environmental properties
86  	private String providerUrl = "ldap://localhost:10389/dc=seasar,dc=org";
87  	private static final String PROVIDER_URL = "ldap.provider.url";
88  	
89  	private String securityAuthentication = "simple";
90  	private static final String SECURITY_AUTHENTICATION = "ldap.security.authentication";
91  	
92  	private String securityPrincipal = "uid=admin,ou=system";
93  	private static final String SECURITY_PRINCIPAL = "ldap.security.principal";
94  	
95  	private String securityCredential = "tuigwaa";
96  	private static final String SECURITY_CREDENTIAL = "ldap.security.credentials";	
97  		
98  	// search properties
99  	private String searchBase = "dc=seasar,dc=org";
100 	private static final String SEARCH_BASE = "ldap.search.base";	
101 	
102 	private String userSearchBase = "ou=users";
103 	private static final String USER_SEARCH_BASE = "ldap.user.search.base";
104 	
105 	private String userSearchPrefix = "uid";
106 	private static final String USER_SEARCH_PREFIX = "ldap.user.search.prefix";
107 	
108 	private String userRoleAttribute = "memberOf";
109 	private static final String USER_ROLE_ATTRIBUTE = "ldap.user.role.attribute";
110 	
111 //	private String passwordAttribute = "userPassword";
112 //	private static final String PASSWORD_ATTRIBUTE = "ldap.password.attribute";
113 
114 	private String roleSearchBase = "ou=roles";
115 	private static final String ROLE_SEARCH_BASE = "ldap.role.search.base";
116 	
117 	private String roleSearchPrefix = "cn";
118 	private static final String ROLE_SEARCH_PREFIX = "ldap.role.search.prefix";
119 	
120 	private String roleUserAttribute = "member";
121 	private static final String ROLE_MEMBER_ATTRIBUTE = "ldap.role.user.attribute";
122 	
123 	private boolean roleUserPrefixOnly = false;
124 	private static final String ROLE_USER_PREFIXONLY = "ldap.role.user.prefixonly";
125 	
126 	// principals
127 	private TgwUser userPrincipal;	
128 	private List roleList = new ArrayList();
129 	
130 	// state flags
131 	private boolean authenticated = false;
132 	private boolean committed = false;
133 
134 	// sharedStateMap keys
135     private static final String SHARED_NAME = "javax.security.auth.login.name";
136     private static final String SHARED_PWD = "javax.security.auth.login.password";		
137 	
138 	public TgwLoginModule(){		
139 	}	
140 	
141 	// ----- [Start] interface methods -----
142 	public boolean abort() throws LoginException {
143 				
144 		if (!authenticated) {
145 			return false;
146 		} else {
147 			if (!committed ){
148 				cleanup(true);
149 				userPrincipal = null;		
150 				roleList = new ArrayList();
151 			}else{
152 				logout();
153 			}
154 		}
155 		return true;
156 	}
157 
158 	public boolean commit() throws LoginException {
159 		
160 		if(authenticated){
161 			subject.getPrincipals().add(userPrincipal);
162 			for(Iterator i = roleList.iterator();i.hasNext();){			
163 				TgwRole role = (TgwRole) i.next();				
164 				subject.getPrincipals().add(role);
165 			}						
166 		}else{
167 			return false;
168 		}		
169 		committed = true;		
170 		return true;
171 	}
172 
173 	public boolean login() throws LoginException {
174 				
175 		// set username and password
176 		
177 		if(useFirstPass){
178 			setUsernameAndPassword(true);
179 			
180 			try{
181 				authenticate();
182 				return true;
183 			}catch(LoginException le){
184 				cleanup(false);
185 				throw le;
186 			}			
187 			
188 		}else if(tryFirstPass){
189 			setUsernameAndPassword(true);			
190 			try{
191 				authenticate();
192 				return true;
193 			}catch(LoginException le){
194 				cleanup(false);
195 				// don't throw Exception, try input authentication
196 			}			
197 		}				
198 		
199 		setUsernameAndPassword(false);
200 				
201 		try{
202 			authenticate();
203 			return true;
204 		}catch(LoginException le){
205 			cleanup(false);
206 			throw le;
207 		}
208 	}
209 
210 	public boolean logout() throws LoginException {
211 						
212 		subject.getPrincipals().remove(userPrincipal);
213 		for(Iterator i=roleList.iterator();i.hasNext();){
214 			subject.getPrincipals().remove(i.next());
215 		}								
216 		
217 		cleanup(true);
218 		userPrincipal = null;		
219 		roleList = new ArrayList();
220 		
221 		return true;
222 	}
223 
224 	public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
225 		
226 		// private members
227 		this.subject = subject;
228 		this.callbackHandler = callbackHandler;
229 		this.sharedState = sharedState;
230 
231 		// options given in login configuration
232 		tryFirstPass = "true".equalsIgnoreCase((String)options.get(TRY_FIRST_PASS));
233 		useFirstPass = "true".equalsIgnoreCase((String)options.get(USE_FIRST_PASS));
234 		
235 		// environmental properties		
236 		if(rb != null){
237 			providerUrl = rb.getString(PROVIDER_URL);
238 			securityAuthentication = rb.getString(SECURITY_AUTHENTICATION);
239 			securityPrincipal = rb.getString(SECURITY_PRINCIPAL);
240 			securityCredential = rb.getString(SECURITY_CREDENTIAL);
241 			
242 			// search properties
243 			searchBase = rb.getString(SEARCH_BASE);
244 			userSearchBase = rb.getString(USER_SEARCH_BASE);
245 			userSearchPrefix = rb.getString(USER_SEARCH_PREFIX);
246 			userRoleAttribute = rb.getString(USER_ROLE_ATTRIBUTE);
247 //			passwordAttribute = rb.getString(PASSWORD_ATTRIBUTE);
248 			roleSearchBase = rb.getString(ROLE_SEARCH_BASE);
249 			roleSearchPrefix = rb.getString(ROLE_SEARCH_PREFIX);
250 			roleUserAttribute = rb.getString(ROLE_MEMBER_ATTRIBUTE);
251 			String prefixOnly = rb.getString(ROLE_USER_PREFIXONLY);
252 			if(prefixOnly != null && !"".equals(prefixOnly)){
253 				roleUserPrefixOnly = Boolean.valueOf(prefixOnly).booleanValue();
254 			}			
255 		}
256 		
257 		// build environment hashtable
258 		environment.put(Context.INITIAL_CONTEXT_FACTORY,com.sun.jndi.ldap.LdapCtxFactory.class.getName());
259 		environment.put(Context.PROVIDER_URL,providerUrl);
260 		environment.put(Context.SECURITY_AUTHENTICATION,securityAuthentication);
261 		environment.put(Context.SECURITY_PRINCIPAL,securityPrincipal);
262 		environment.put(Context.SECURITY_CREDENTIALS,securityCredential);				
263 	}
264 	
265 	// ----- [End] interface methods -----
266 	
267 	private void setUsernameAndPassword(boolean shared) throws LoginException{
268 		
269 		if(shared){
270 			username = (String) sharedState.get(SHARED_NAME);
271 			password = (char[]) sharedState.get(SHARED_PWD);
272 			return;
273 		}
274 	
275 		if(callbackHandler == null)
276 			throw new LoginException("Callback handler needed.");
277 		
278 		Callback[] callbacks = new Callback[2];
279 		callbacks[0] = new NameCallback("ldap username: ");
280 		callbacks[1] = new PasswordCallback("ldap password: ",false);
281 		
282 		try{
283 			callbackHandler.handle(callbacks);				
284 			username = ((NameCallback) callbacks[0]).getName();
285 			password = ((PasswordCallback) callbacks[1]).getPassword();
286 			
287 		}catch(IOException ioe){
288 			throw new LoginException("login failed cased by " + ioe.getLocalizedMessage());
289 		}catch(UnsupportedCallbackException uce){
290 			throw new LoginException("login failed caused by " + uce.getLocalizedMessage());
291 		}			
292 	}
293 	
294 	private void authenticate() throws LoginException{
295 		
296 		DirContext ctx = null;		
297 		try{			
298 			if(bindContext()){
299 				userPrincipal = new TgwUser(username);
300 				
301 				// optional
302 				if(!"".equals(userRoleAttribute)){
303 					addUserRoleAttribute();
304 				}
305 				
306 				// mandatory
307 				addRoleUserAttribute();					
308 				
309 				if(roleList != null){
310 					List tmpList = new ArrayList();
311 					for(Iterator i=roleList.iterator();i.hasNext();){
312 						TgwRole role = (TgwRole) i.next();
313 						tmpList.add(role.getName());
314 					}					
315 					String[] roles = (String[])tmpList.toArray(new String[tmpList.size()]);
316 					userPrincipal.setRoles(roles);					
317 				}
318 					
319 				if(!sharedState.containsKey(SHARED_NAME) && !sharedState.containsKey(SHARED_PWD)){
320 					sharedState.put(SHARED_NAME,username);
321 					sharedState.put(SHARED_PWD,password);
322 				}
323 				authenticated = true;				
324 				
325 			}else{
326 				throw new FailedLoginException("bind failed");				
327 			}			
328 			return;			
329 		}catch(NamingException ne){			
330 			throw new LoginException("login failed caused by " + ne.getLocalizedMessage());			
331 		}finally{
332 			DirectoryUtils.closeQuietly(ctx);
333 		}
334 	}
335 		
336 	private Hashtable getCurrentBindEnvironment(){
337 			
338 		Hashtable env = new Hashtable();
339 		String bindUserDN = DirectoryUtils.getDN(username,userSearchPrefix,userSearchBase,searchBase,true);		
340 		env.put(Context.INITIAL_CONTEXT_FACTORY,com.sun.jndi.ldap.LdapCtxFactory.class.getName());
341 		env.put(Context.PROVIDER_URL,providerUrl);
342 		env.put(Context.SECURITY_AUTHENTICATION,securityAuthentication);
343 		env.put(Context.SECURITY_PRINCIPAL,bindUserDN);
344 		env.put(Context.SECURITY_CREDENTIALS,new String(password));		
345 		
346 		return env;		
347 	}
348 
349 	private boolean bindContext() throws NamingException{
350 		
351 		if(username == null || "".equals(username)){
352 			return false;
353 		}			
354 		
355 		Hashtable bindenv = getCurrentBindEnvironment();		
356 		try{
357 			new InitialContext(bindenv);
358 			return true;
359 		}catch(AuthenticationException ae){
360 			return false;			
361 		}
362 	}	
363 
364 	private void addUserRoleAttribute(){
365 		
366 		DirContext ctx = null;
367 		Hashtable bindenv = getCurrentBindEnvironment();
368 		
369 		try{
370 			ctx = new InitialDirContext(bindenv);
371 		
372 			Attributes attrs = ctx.getAttributes(DirectoryUtils.getDN(username,userSearchPrefix,userSearchBase,searchBase,false));			
373 			Attribute attr = attrs.get(userRoleAttribute);				
374 			if(attr != null){
375 				NamingEnumeration roles = attr.getAll();
376 				while(roles.hasMore()){							
377 					String rolename = (String)roles.next();							
378 					Pattern p = Pattern.compile("^" + roleSearchPrefix + "=([0-9a-zA-Z]*)[,0-9a-zA-Z=]?");
379 					Matcher m = p.matcher(rolename);
380 					if(m.find()){
381 						rolename = m.group(1);
382 					}
383 					TgwRole role = new TgwRole(rolename);
384 					roleList.add(role);
385 				}
386 			}	
387 		}catch(NamingException ne){
388 			ne.printStackTrace();
389 		}finally{
390 			DirectoryUtils.closeQuietly(ctx);
391 		}
392 	}
393 	
394 	private void addRoleUserAttribute(){
395 		
396 		DirContext ctx = null;
397 		Hashtable bindenv = getCurrentBindEnvironment();
398 		
399 		try{
400 			ctx = new InitialDirContext(bindenv);
401 			
402 			String userDn = DirectoryUtils.getDN(username,userSearchPrefix,userSearchBase,searchBase,true);
403 			
404 			Attributes member = null;
405 			if(roleUserPrefixOnly){
406 				member = new BasicAttributes(roleUserAttribute,username);				
407 			}else{
408 				member = new BasicAttributes(roleUserAttribute,userDn);				
409 			}
410 			
411 			String roleSuffix = DirectoryUtils.buildSuffix(roleSearchBase,searchBase,false,false);
412 			NamingEnumeration nenum = ctx.search(roleSuffix,member);			
413 			
414 			while(nenum.hasMore()){
415 				SearchResult result = (SearchResult) nenum.next();
416 				Attributes attrs =result.getAttributes();			
417 
418 				Attribute cn = attrs.get(roleSearchPrefix);
419 				NamingEnumeration values = cn.getAll();
420 				if(values.hasMore()){
421 					String value = (String)values.next();
422 					TgwRole role = new TgwRole(value);
423 					
424 					if(!roleList.contains(role))					
425 						roleList.add(role);					
426 				}
427 			}		
428 		}catch(NamingException ne){
429 			ne.printStackTrace();
430 		}finally{
431 			DirectoryUtils.closeQuietly(ctx);		
432 		}
433 	}
434 		
435 	private void cleanup(boolean flagclean){	
436 						
437 		if(flagclean){
438 			authenticated = false;
439 			committed = false;
440 		}		
441 		username = null;
442 		password = null;		
443 		
444 		sharedState.remove(SHARED_NAME);
445 		sharedState.remove(SHARED_PWD);		
446 	}	
447 }