001 /** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018 package org.apache.geronimo.security.realm.providers; 019 020 import java.io.IOException; 021 import java.security.Principal; 022 import java.text.MessageFormat; 023 import java.util.ArrayList; 024 import java.util.Arrays; 025 import java.util.Collections; 026 import java.util.HashSet; 027 import java.util.Hashtable; 028 import java.util.List; 029 import java.util.Map; 030 import java.util.Set; 031 032 import javax.naming.AuthenticationException; 033 import javax.naming.CommunicationException; 034 import javax.naming.Context; 035 import javax.naming.Name; 036 import javax.naming.NameParser; 037 import javax.naming.NamingEnumeration; 038 import javax.naming.NamingException; 039 import javax.naming.directory.Attribute; 040 import javax.naming.directory.Attributes; 041 import javax.naming.directory.DirContext; 042 import javax.naming.directory.InitialDirContext; 043 import javax.naming.directory.SearchControls; 044 import javax.naming.directory.SearchResult; 045 import javax.security.auth.Subject; 046 import javax.security.auth.callback.Callback; 047 import javax.security.auth.callback.CallbackHandler; 048 import javax.security.auth.callback.NameCallback; 049 import javax.security.auth.callback.PasswordCallback; 050 import javax.security.auth.callback.UnsupportedCallbackException; 051 import javax.security.auth.login.FailedLoginException; 052 import javax.security.auth.login.LoginException; 053 import javax.security.auth.spi.LoginModule; 054 055 import org.apache.commons.logging.Log; 056 import org.apache.commons.logging.LogFactory; 057 import org.apache.geronimo.security.jaas.JaasLoginModuleUse; 058 import org.apache.geronimo.security.jaas.WrappingLoginModule; 059 060 061 /** 062 * LDAPLoginModule is a login module using ldap as an authentication store. 063 * <p/> 064 * This login module checks security credentials so the lifecycle methods must return true to indicate success 065 * or throw LoginException to indicate failure. 066 * 067 * @version $Rev: 706640 $ $Date: 2008-10-21 14:44:05 +0000 (Tue, 21 Oct 2008) $ 068 */ 069 public class LDAPLoginModule implements LoginModule { 070 071 private static Log log = LogFactory.getLog(LDAPLoginModule.class); 072 073 private Subject subject; 074 private CallbackHandler handler; 075 076 private static final String INITIAL_CONTEXT_FACTORY = "initialContextFactory"; 077 private static final String CONNECTION_URL = "connectionURL"; 078 private static final String CONNECTION_USERNAME = "connectionUsername"; 079 private static final String CONNECTION_PASSWORD = "connectionPassword"; 080 private static final String CONNECTION_PROTOCOL = "connectionProtocol"; 081 private static final String AUTHENTICATION = "authentication"; 082 private static final String USER_BASE = "userBase"; 083 private static final String USER_SEARCH_MATCHING = "userSearchMatching"; 084 private static final String USER_SEARCH_SUBTREE = "userSearchSubtree"; 085 private static final String ROLE_BASE = "roleBase"; 086 private static final String ROLE_NAME = "roleName"; 087 private static final String ROLE_SEARCH_MATCHING = "roleSearchMatching"; 088 private static final String ROLE_SEARCH_SUBTREE = "roleSearchSubtree"; 089 private static final String USER_ROLE_NAME = "userRoleName"; 090 public final static List<String> supportedOptions = Collections.unmodifiableList(Arrays.asList(INITIAL_CONTEXT_FACTORY, CONNECTION_URL, 091 CONNECTION_USERNAME, CONNECTION_PASSWORD, CONNECTION_PROTOCOL, AUTHENTICATION, USER_BASE, 092 USER_SEARCH_MATCHING, USER_SEARCH_SUBTREE, ROLE_BASE, ROLE_NAME, ROLE_SEARCH_MATCHING, ROLE_SEARCH_SUBTREE, 093 USER_ROLE_NAME)); 094 095 private String initialContextFactory; 096 private String connectionURL; 097 private String connectionUsername; 098 private String connectionPassword; 099 private String connectionProtocol; 100 private String authentication; 101 private String userBase; 102 private String roleBase; 103 private String roleName; 104 private String userRoleName; 105 106 private String cbUsername; 107 private String cbPassword; 108 109 protected DirContext context = null; 110 111 private MessageFormat userSearchMatchingFormat; 112 private MessageFormat roleSearchMatchingFormat; 113 114 private boolean userSearchSubtreeBool = false; 115 private boolean roleSearchSubtreeBool = false; 116 117 private boolean loginSucceeded; 118 private final Set<String> groups = new HashSet<String>(); 119 private final Set<Principal> allPrincipals = new HashSet<Principal>(); 120 121 public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { 122 this.subject = subject; 123 this.handler = callbackHandler; 124 for(Object option: options.keySet()) { 125 if(!supportedOptions.contains(option) && !JaasLoginModuleUse.supportedOptions.contains(option) 126 && !WrappingLoginModule.supportedOptions.contains(option)) { 127 log.warn("Ignoring option: "+option+". Not supported."); 128 } 129 } 130 initialContextFactory = (String) options.get(INITIAL_CONTEXT_FACTORY); 131 connectionURL = (String) options.get(CONNECTION_URL); 132 connectionUsername = (String) options.get(CONNECTION_USERNAME); 133 connectionPassword = (String) options.get(CONNECTION_PASSWORD); 134 connectionProtocol = (String) options.get(CONNECTION_PROTOCOL); 135 authentication = (String) options.get(AUTHENTICATION); 136 userBase = (String) options.get(USER_BASE); 137 String userSearchMatching = (String) options.get(USER_SEARCH_MATCHING); 138 String userSearchSubtree = (String) options.get(USER_SEARCH_SUBTREE); 139 roleBase = (String) options.get(ROLE_BASE); 140 roleName = (String) options.get(ROLE_NAME); 141 String roleSearchMatching = (String) options.get(ROLE_SEARCH_MATCHING); 142 String roleSearchSubtree = (String) options.get(ROLE_SEARCH_SUBTREE); 143 userRoleName = (String) options.get(USER_ROLE_NAME); 144 userSearchMatchingFormat = new MessageFormat(userSearchMatching); 145 roleSearchMatchingFormat = new MessageFormat(roleSearchMatching); 146 userSearchSubtreeBool = Boolean.valueOf(userSearchSubtree); 147 roleSearchSubtreeBool = Boolean.valueOf(roleSearchSubtree); 148 } 149 150 /** 151 * This LoginModule is not to be ignored. So, this method should never return false. 152 * @return true if authentication succeeds, or throw a LoginException such as FailedLoginException 153 * if authentication fails 154 */ 155 public boolean login() throws LoginException { 156 loginSucceeded = false; 157 Callback[] callbacks = new Callback[2]; 158 159 callbacks[0] = new NameCallback("User name"); 160 callbacks[1] = new PasswordCallback("Password", false); 161 try { 162 handler.handle(callbacks); 163 } catch (IOException ioe) { 164 throw (LoginException) new LoginException().initCause(ioe); 165 } catch (UnsupportedCallbackException uce) { 166 throw (LoginException) new LoginException().initCause(uce); 167 } 168 cbUsername = ((NameCallback) callbacks[0]).getName(); 169 cbPassword = new String(((PasswordCallback) callbacks[1]).getPassword()); 170 171 if (cbUsername == null || "".equals(cbUsername) 172 || cbPassword == null || "".equals(cbPassword)) { 173 // Clear out the private state 174 cbUsername = null; 175 cbPassword = null; 176 groups.clear(); 177 throw new FailedLoginException(); 178 } 179 180 try { 181 boolean result = authenticate(cbUsername, cbPassword); 182 if (!result) { 183 throw new FailedLoginException(); 184 } 185 } catch (LoginException e) { 186 // Clear out the private state 187 cbUsername = null; 188 cbPassword = null; 189 groups.clear(); 190 throw e; 191 } catch (Exception e) { 192 // Clear out the private state 193 cbUsername = null; 194 cbPassword = null; 195 groups.clear(); 196 throw (LoginException) new LoginException("LDAP Error").initCause(e); 197 } 198 199 loginSucceeded = true; 200 return true; 201 } 202 203 /* 204 * @exception LoginException if login succeeded but commit failed. 205 * 206 * @return true if login succeeded and commit succeeded, or false if login failed but commit succeeded. 207 */ 208 public boolean commit() throws LoginException { 209 if(loginSucceeded) { 210 if(cbUsername != null) { 211 allPrincipals.add(new GeronimoUserPrincipal(cbUsername)); 212 } 213 for(String group: groups) { 214 allPrincipals.add(new GeronimoGroupPrincipal(group)); 215 } 216 subject.getPrincipals().addAll(allPrincipals); 217 } 218 219 // Clear out the private state 220 cbUsername = null; 221 cbPassword = null; 222 groups.clear(); 223 224 return loginSucceeded; 225 } 226 227 public boolean abort() throws LoginException { 228 if(loginSucceeded) { 229 // Clear out the private state 230 cbUsername = null; 231 cbPassword = null; 232 groups.clear(); 233 allPrincipals.clear(); 234 } 235 return loginSucceeded; 236 } 237 238 public boolean logout() throws LoginException { 239 // Clear out the private state 240 loginSucceeded = false; 241 cbUsername = null; 242 cbPassword = null; 243 groups.clear(); 244 if(!subject.isReadOnly()) { 245 // Remove principals added by this LoginModule 246 subject.getPrincipals().removeAll(allPrincipals); 247 } 248 allPrincipals.clear(); 249 return true; 250 } 251 252 protected void close(DirContext context) { 253 try { 254 context.close(); 255 } catch (Exception e) { 256 log.error(e); 257 } 258 } 259 260 protected boolean authenticate(String username, String password) throws Exception { 261 262 DirContext context = open(); 263 264 try { 265 266 String filter = userSearchMatchingFormat.format(new String[]{username}); 267 SearchControls constraints = new SearchControls(); 268 if (userSearchSubtreeBool) { 269 constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); 270 } else { 271 constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE); 272 } 273 274 //setup attributes 275 String[] attribs; 276 if (userRoleName == null) { 277 attribs = new String[]{}; 278 } else { 279 attribs = new String[]{userRoleName}; 280 } 281 constraints.setReturningAttributes(attribs); 282 283 284 NamingEnumeration results = context.search(userBase, filter, constraints); 285 286 if (results == null || !results.hasMore()) { 287 return false; 288 } 289 290 SearchResult result = (SearchResult) results.next(); 291 292 if (results.hasMore()) { 293 //ignore for now 294 } 295 NameParser parser = context.getNameParser(""); 296 Name contextName = parser.parse(context.getNameInNamespace()); 297 Name baseName = parser.parse(userBase); 298 Name entryName = parser.parse(result.getName()); 299 Name name = contextName.addAll(baseName); 300 name = name.addAll(entryName); 301 String dn = name.toString(); 302 303 Attributes attrs = result.getAttributes(); 304 if (attrs == null) { 305 return false; 306 } 307 ArrayList<String> roles = null; 308 if (userRoleName != null) { 309 roles = addAttributeValues(userRoleName, attrs, roles); 310 } 311 312 //check the credentials by binding to server 313 bindUser(context, dn, password); 314 //if authenticated add more roles 315 roles = getRoles(context, dn, username, roles); 316 for (String role : roles) { 317 groups.add(role); 318 } 319 } catch (CommunicationException e) { 320 close(context); 321 throw (LoginException) new FailedLoginException().initCause(e); 322 } catch (NamingException e) { 323 close(context); 324 throw (LoginException) new FailedLoginException().initCause(e); 325 } 326 327 328 return true; 329 } 330 331 protected ArrayList<String> getRoles(DirContext context, String dn, String username, ArrayList<String> list) throws NamingException { 332 if (list == null) { 333 list = new ArrayList<String>(); 334 } 335 if (roleName == null || "".equals(roleName)) { 336 return list; 337 } 338 String filter = roleSearchMatchingFormat.format(new String[]{doRFC2254Encoding(dn), username}); 339 340 SearchControls constraints = new SearchControls(); 341 if (roleSearchSubtreeBool) { 342 constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); 343 } else { 344 constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE); 345 } 346 NamingEnumeration results = 347 context.search(roleBase, filter, constraints); 348 while (results.hasMore()) { 349 SearchResult result = (SearchResult) results.next(); 350 Attributes attrs = result.getAttributes(); 351 if (attrs == null) { 352 continue; 353 } 354 list = addAttributeValues(roleName, attrs, list); 355 } 356 return list; 357 358 } 359 360 361 protected String doRFC2254Encoding(String inputString) { 362 StringBuffer buf = new StringBuffer(inputString.length()); 363 for (int i = 0; i < inputString.length(); i++) { 364 char c = inputString.charAt(i); 365 switch (c) { 366 case'\\': 367 buf.append("\\5c"); 368 break; 369 case'*': 370 buf.append("\\2a"); 371 break; 372 case'(': 373 buf.append("\\28"); 374 break; 375 case')': 376 buf.append("\\29"); 377 break; 378 case'\0': 379 buf.append("\\00"); 380 break; 381 default: 382 buf.append(c); 383 break; 384 } 385 } 386 return buf.toString(); 387 } 388 389 protected void bindUser(DirContext context, String dn, String password) throws NamingException, FailedLoginException { 390 391 context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn); 392 context.addToEnvironment(Context.SECURITY_CREDENTIALS, password); 393 try { 394 context.getAttributes("", null); 395 } catch (AuthenticationException e) { 396 log.debug("Authentication failed for dn=" + dn); 397 throw new FailedLoginException(); 398 } finally { 399 400 if (connectionUsername != null) { 401 context.addToEnvironment(Context.SECURITY_PRINCIPAL, 402 connectionUsername); 403 } else { 404 context.removeFromEnvironment(Context.SECURITY_PRINCIPAL); 405 } 406 407 if (connectionPassword != null) { 408 context.addToEnvironment(Context.SECURITY_CREDENTIALS, 409 connectionPassword); 410 } else { 411 context.removeFromEnvironment(Context.SECURITY_CREDENTIALS); 412 } 413 } 414 } 415 416 private ArrayList<String> addAttributeValues(String attrId, Attributes attrs, ArrayList<String> values) 417 throws NamingException { 418 419 if (attrId == null || attrs == null) { 420 return values; 421 } 422 if (values == null) { 423 values = new ArrayList<String>(); 424 } 425 Attribute attr = attrs.get(attrId); 426 if (attr == null) { 427 return (values); 428 } 429 NamingEnumeration e = attr.getAll(); 430 while (e.hasMore()) { 431 String value = (String) e.next(); 432 values.add(value); 433 } 434 return values; 435 } 436 437 protected DirContext open() throws NamingException { 438 if (context != null) { 439 return context; 440 } 441 442 try { 443 Hashtable<String, String> env = new Hashtable<String, String>(); 444 env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory); 445 if (connectionUsername != null || !"".equals(connectionUsername)) { 446 env.put(Context.SECURITY_PRINCIPAL, connectionUsername); 447 } 448 if (connectionPassword != null || !"".equals(connectionPassword)) { 449 env.put(Context.SECURITY_CREDENTIALS, connectionPassword); 450 } 451 env.put(Context.SECURITY_PROTOCOL, connectionProtocol == null ? "" : connectionProtocol); 452 env.put(Context.PROVIDER_URL, connectionURL == null ? "" : connectionURL); 453 env.put(Context.SECURITY_AUTHENTICATION, authentication == null ? "" : authentication); 454 context = new InitialDirContext(env); 455 456 } catch (NamingException e) { 457 log.error(e); 458 throw e; 459 } 460 return context; 461 } 462 463 }