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 */ 017package org.apache.activemq.jaas; 018 019import java.io.IOException; 020import java.net.URI; 021import java.net.URISyntaxException; 022import java.security.Principal; 023import java.text.MessageFormat; 024import java.util.*; 025 026import javax.naming.*; 027import javax.naming.directory.Attribute; 028import javax.naming.directory.Attributes; 029import javax.naming.directory.DirContext; 030import javax.naming.directory.InitialDirContext; 031import javax.naming.directory.SearchControls; 032import javax.naming.directory.SearchResult; 033import javax.security.auth.Subject; 034import javax.security.auth.callback.Callback; 035import javax.security.auth.callback.CallbackHandler; 036import javax.security.auth.callback.NameCallback; 037import javax.security.auth.callback.PasswordCallback; 038import javax.security.auth.callback.UnsupportedCallbackException; 039import javax.security.auth.login.FailedLoginException; 040import javax.security.auth.login.LoginException; 041import javax.security.auth.spi.LoginModule; 042 043import org.slf4j.Logger; 044import org.slf4j.LoggerFactory; 045 046/** 047 * @version $Rev: $ $Date: $ 048 */ 049public class LDAPLoginModule implements LoginModule { 050 051 private static final String INITIAL_CONTEXT_FACTORY = "initialContextFactory"; 052 private static final String CONNECTION_URL = "connectionURL"; 053 private static final String CONNECTION_USERNAME = "connectionUsername"; 054 private static final String CONNECTION_PASSWORD = "connectionPassword"; 055 private static final String CONNECTION_PROTOCOL = "connectionProtocol"; 056 private static final String AUTHENTICATION = "authentication"; 057 private static final String USER_BASE = "userBase"; 058 private static final String USER_SEARCH_MATCHING = "userSearchMatching"; 059 private static final String USER_SEARCH_SUBTREE = "userSearchSubtree"; 060 private static final String ROLE_BASE = "roleBase"; 061 private static final String ROLE_NAME = "roleName"; 062 private static final String ROLE_SEARCH_MATCHING = "roleSearchMatching"; 063 private static final String ROLE_SEARCH_SUBTREE = "roleSearchSubtree"; 064 private static final String USER_ROLE_NAME = "userRoleName"; 065 private static final String EXPAND_ROLES = "expandRoles"; 066 private static final String EXPAND_ROLES_MATCHING = "expandRolesMatching"; 067 068 private static Logger log = LoggerFactory.getLogger(LDAPLoginModule.class); 069 070 protected DirContext context; 071 072 private Subject subject; 073 private CallbackHandler handler; 074 private LDAPLoginProperty [] config; 075 private Principal user; 076 private Set<GroupPrincipal> groups = new HashSet<GroupPrincipal>(); 077 078 /** the authentication status*/ 079 private boolean succeeded = false; 080 private boolean commitSucceeded = false; 081 082 @Override 083 public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { 084 this.subject = subject; 085 this.handler = callbackHandler; 086 087 config = new LDAPLoginProperty [] { 088 new LDAPLoginProperty (INITIAL_CONTEXT_FACTORY, (String)options.get(INITIAL_CONTEXT_FACTORY)), 089 new LDAPLoginProperty (CONNECTION_URL, (String)options.get(CONNECTION_URL)), 090 new LDAPLoginProperty (CONNECTION_USERNAME, (String)options.get(CONNECTION_USERNAME)), 091 new LDAPLoginProperty (CONNECTION_PASSWORD, (String)options.get(CONNECTION_PASSWORD)), 092 new LDAPLoginProperty (CONNECTION_PROTOCOL, (String)options.get(CONNECTION_PROTOCOL)), 093 new LDAPLoginProperty (AUTHENTICATION, (String)options.get(AUTHENTICATION)), 094 new LDAPLoginProperty (USER_BASE, (String)options.get(USER_BASE)), 095 new LDAPLoginProperty (USER_SEARCH_MATCHING, (String)options.get(USER_SEARCH_MATCHING)), 096 new LDAPLoginProperty (USER_SEARCH_SUBTREE, (String)options.get(USER_SEARCH_SUBTREE)), 097 new LDAPLoginProperty (ROLE_BASE, (String)options.get(ROLE_BASE)), 098 new LDAPLoginProperty (ROLE_NAME, (String)options.get(ROLE_NAME)), 099 new LDAPLoginProperty (ROLE_SEARCH_MATCHING, (String)options.get(ROLE_SEARCH_MATCHING)), 100 new LDAPLoginProperty (ROLE_SEARCH_SUBTREE, (String)options.get(ROLE_SEARCH_SUBTREE)), 101 new LDAPLoginProperty (USER_ROLE_NAME, (String)options.get(USER_ROLE_NAME)), 102 new LDAPLoginProperty (EXPAND_ROLES, (String) options.get(EXPAND_ROLES)), 103 new LDAPLoginProperty (EXPAND_ROLES_MATCHING, (String) options.get(EXPAND_ROLES_MATCHING)), 104 105 }; 106 } 107 108 @Override 109 public boolean login() throws LoginException { 110 111 Callback[] callbacks = new Callback[2]; 112 113 callbacks[0] = new NameCallback("User name"); 114 callbacks[1] = new PasswordCallback("Password", false); 115 try { 116 handler.handle(callbacks); 117 } catch (IOException ioe) { 118 throw (LoginException)new LoginException().initCause(ioe); 119 } catch (UnsupportedCallbackException uce) { 120 throw (LoginException)new LoginException().initCause(uce); 121 } 122 123 String password; 124 125 String username = ((NameCallback)callbacks[0]).getName(); 126 if (username == null) 127 return false; 128 129 if (((PasswordCallback)callbacks[1]).getPassword() != null) 130 password = new String(((PasswordCallback)callbacks[1]).getPassword()); 131 else 132 password=""; 133 134 // authenticate will throw LoginException 135 // in case of failed authentication 136 authenticate(username, password); 137 138 user = new UserPrincipal(username); 139 succeeded = true; 140 return true; 141 } 142 143 @Override 144 public boolean logout() throws LoginException { 145 subject.getPrincipals().remove(user); 146 subject.getPrincipals().removeAll(groups); 147 148 user = null; 149 groups.clear(); 150 151 succeeded = false; 152 commitSucceeded = false; 153 return true; 154 } 155 156 @Override 157 public boolean commit() throws LoginException { 158 if (!succeeded) { 159 user = null; 160 groups.clear(); 161 return false; 162 } 163 164 Set<Principal> principals = subject.getPrincipals(); 165 principals.add(user); 166 for (GroupPrincipal gp : groups) { 167 principals.add(gp); 168 } 169 170 commitSucceeded = true; 171 return true; 172 } 173 174 @Override 175 public boolean abort() throws LoginException { 176 if (!succeeded) { 177 return false; 178 } else if (succeeded && commitSucceeded) { 179 // we succeeded, but another required module failed 180 logout(); 181 } else { 182 // our commit failed 183 user = null; 184 groups.clear(); 185 succeeded = false; 186 } 187 return true; 188 } 189 190 protected void close(DirContext context) { 191 try { 192 context.close(); 193 } catch (Exception e) { 194 log.error(e.toString()); 195 } 196 } 197 198 protected boolean authenticate(String username, String password) throws LoginException { 199 200 MessageFormat userSearchMatchingFormat; 201 boolean userSearchSubtreeBool; 202 203 DirContext context = null; 204 205 if (log.isDebugEnabled()) { 206 log.debug("Create the LDAP initial context."); 207 } 208 try { 209 context = open(); 210 } catch (NamingException ne) { 211 FailedLoginException ex = new FailedLoginException("Error opening LDAP connection"); 212 ex.initCause(ne); 213 throw ex; 214 } 215 216 if (!isLoginPropertySet(USER_SEARCH_MATCHING)) 217 return false; 218 219 userSearchMatchingFormat = new MessageFormat(getLDAPPropertyValue(USER_SEARCH_MATCHING)); 220 userSearchSubtreeBool = Boolean.valueOf(getLDAPPropertyValue(USER_SEARCH_SUBTREE)).booleanValue(); 221 222 try { 223 224 String filter = userSearchMatchingFormat.format(new String[] { 225 doRFC2254Encoding(username) 226 }); 227 SearchControls constraints = new SearchControls(); 228 if (userSearchSubtreeBool) { 229 constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); 230 } else { 231 constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE); 232 } 233 234 // setup attributes 235 List<String> list = new ArrayList<String>(); 236 if (isLoginPropertySet(USER_ROLE_NAME)) { 237 list.add(getLDAPPropertyValue(USER_ROLE_NAME)); 238 } 239 String[] attribs = new String[list.size()]; 240 list.toArray(attribs); 241 constraints.setReturningAttributes(attribs); 242 243 if (log.isDebugEnabled()) { 244 log.debug("Get the user DN."); 245 log.debug("Looking for the user in LDAP with "); 246 log.debug(" base DN: " + getLDAPPropertyValue(USER_BASE)); 247 log.debug(" filter: " + filter); 248 } 249 250 NamingEnumeration<SearchResult> results = context.search(getLDAPPropertyValue(USER_BASE), filter, constraints); 251 252 if (results == null || !results.hasMore()) { 253 log.warn("User " + username + " not found in LDAP."); 254 throw new FailedLoginException("User " + username + " not found in LDAP."); 255 } 256 257 SearchResult result = results.next(); 258 259 if (results.hasMore()) { 260 // ignore for now 261 } 262 263 String dn; 264 if (result.isRelative()) { 265 log.debug("LDAP returned a relative name: {}", result.getName()); 266 267 NameParser parser = context.getNameParser(""); 268 Name contextName = parser.parse(context.getNameInNamespace()); 269 Name baseName = parser.parse(getLDAPPropertyValue(USER_BASE)); 270 Name entryName = parser.parse(result.getName()); 271 Name name = contextName.addAll(baseName); 272 name = name.addAll(entryName); 273 dn = name.toString(); 274 } else { 275 log.debug("LDAP returned an absolute name: {}", result.getName()); 276 277 try { 278 URI uri = new URI(result.getName()); 279 String path = uri.getPath(); 280 281 if (path.startsWith("/")) { 282 dn = path.substring(1); 283 } else { 284 dn = path; 285 } 286 } catch (URISyntaxException e) { 287 if (context != null) { 288 close(context); 289 } 290 FailedLoginException ex = new FailedLoginException("Error parsing absolute name as URI."); 291 ex.initCause(e); 292 throw ex; 293 } 294 } 295 296 if (log.isDebugEnabled()) { 297 log.debug("Using DN [" + dn + "] for binding."); 298 } 299 300 Attributes attrs = result.getAttributes(); 301 if (attrs == null) { 302 throw new FailedLoginException("User found, but LDAP entry malformed: " + username); 303 } 304 List<String> roles = null; 305 if (isLoginPropertySet(USER_ROLE_NAME)) { 306 roles = addAttributeValues(getLDAPPropertyValue(USER_ROLE_NAME), attrs, roles); 307 } 308 309 // check the credentials by binding to server 310 if (bindUser(context, dn, password)) { 311 // if authenticated add more roles 312 roles = getRoles(context, dn, username, roles); 313 if (log.isDebugEnabled()) { 314 log.debug("Roles " + roles + " for user " + username); 315 } 316 for (int i = 0; i < roles.size(); i++) { 317 groups.add(new GroupPrincipal(roles.get(i))); 318 } 319 } else { 320 throw new FailedLoginException("Password does not match for user: " + username); 321 } 322 } catch (CommunicationException e) { 323 FailedLoginException ex = new FailedLoginException("Error contacting LDAP"); 324 ex.initCause(e); 325 throw ex; 326 } catch (NamingException e) { 327 if (context != null) { 328 close(context); 329 } 330 FailedLoginException ex = new FailedLoginException("Error contacting LDAP"); 331 ex.initCause(e); 332 throw ex; 333 } 334 335 if (context != null) { 336 close(context); 337 } 338 339 return true; 340 } 341 342 protected List<String> getRoles(DirContext context, String dn, String username, List<String> currentRoles) throws NamingException { 343 List<String> list = currentRoles; 344 MessageFormat roleSearchMatchingFormat; 345 boolean roleSearchSubtreeBool; 346 boolean expandRolesBool; 347 roleSearchMatchingFormat = new MessageFormat(getLDAPPropertyValue(ROLE_SEARCH_MATCHING)); 348 roleSearchSubtreeBool = Boolean.valueOf(getLDAPPropertyValue(ROLE_SEARCH_SUBTREE)).booleanValue(); 349 expandRolesBool = Boolean.valueOf(getLDAPPropertyValue(EXPAND_ROLES)).booleanValue(); 350 351 if (list == null) { 352 list = new ArrayList<String>(); 353 } 354 if (!isLoginPropertySet(ROLE_NAME)) { 355 return list; 356 } 357 String filter = roleSearchMatchingFormat.format(new String[] { 358 doRFC2254Encoding(dn), doRFC2254Encoding(username) 359 }); 360 361 SearchControls constraints = new SearchControls(); 362 if (roleSearchSubtreeBool) { 363 constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); 364 } else { 365 constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE); 366 } 367 if (log.isDebugEnabled()) { 368 log.debug("Get user roles."); 369 log.debug("Looking for the user roles in LDAP with "); 370 log.debug(" base DN: " + getLDAPPropertyValue(ROLE_BASE)); 371 log.debug(" filter: " + filter); 372 } 373 HashSet<String> haveSeenNames = new HashSet<String>(); 374 Queue<String> pendingNameExpansion = new LinkedList<String>(); 375 NamingEnumeration<SearchResult> results = context.search(getLDAPPropertyValue(ROLE_BASE), filter, constraints); 376 while (results.hasMore()) { 377 SearchResult result = results.next(); 378 Attributes attrs = result.getAttributes(); 379 if (expandRolesBool) { 380 haveSeenNames.add(result.getNameInNamespace()); 381 pendingNameExpansion.add(result.getNameInNamespace()); 382 } 383 if (attrs == null) { 384 continue; 385 } 386 list = addAttributeValues(getLDAPPropertyValue(ROLE_NAME), attrs, list); 387 } 388 if (expandRolesBool) { 389 MessageFormat expandRolesMatchingFormat = new MessageFormat(getLDAPPropertyValue(EXPAND_ROLES_MATCHING)); 390 while (!pendingNameExpansion.isEmpty()) { 391 String name = pendingNameExpansion.remove(); 392 filter = expandRolesMatchingFormat.format(new String[]{name}); 393 results = context.search(getLDAPPropertyValue(ROLE_BASE), filter, constraints); 394 while (results.hasMore()) { 395 SearchResult result = results.next(); 396 name = result.getNameInNamespace(); 397 if (!haveSeenNames.contains(name)) { 398 Attributes attrs = result.getAttributes(); 399 list = addAttributeValues(getLDAPPropertyValue(ROLE_NAME), attrs, list); 400 haveSeenNames.add(name); 401 pendingNameExpansion.add(name); 402 } 403 } 404 } 405 } 406 return list; 407 } 408 409 protected String doRFC2254Encoding(String inputString) { 410 StringBuffer buf = new StringBuffer(inputString.length()); 411 for (int i = 0; i < inputString.length(); i++) { 412 char c = inputString.charAt(i); 413 switch (c) { 414 case '\\': 415 buf.append("\\5c"); 416 break; 417 case '*': 418 buf.append("\\2a"); 419 break; 420 case '(': 421 buf.append("\\28"); 422 break; 423 case ')': 424 buf.append("\\29"); 425 break; 426 case '\0': 427 buf.append("\\00"); 428 break; 429 default: 430 buf.append(c); 431 break; 432 } 433 } 434 return buf.toString(); 435 } 436 437 protected boolean bindUser(DirContext context, String dn, String password) throws NamingException { 438 boolean isValid = false; 439 440 if (log.isDebugEnabled()) { 441 log.debug("Binding the user."); 442 } 443 context.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple"); 444 context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn); 445 context.addToEnvironment(Context.SECURITY_CREDENTIALS, password); 446 try { 447 context.getAttributes("", null); 448 isValid = true; 449 if (log.isDebugEnabled()) { 450 log.debug("User " + dn + " successfully bound."); 451 } 452 } catch (AuthenticationException e) { 453 isValid = false; 454 if (log.isDebugEnabled()) { 455 log.debug("Authentication failed for dn=" + dn); 456 } 457 } 458 459 if (isLoginPropertySet(CONNECTION_USERNAME)) { 460 context.addToEnvironment(Context.SECURITY_PRINCIPAL, getLDAPPropertyValue(CONNECTION_USERNAME)); 461 } else { 462 context.removeFromEnvironment(Context.SECURITY_PRINCIPAL); 463 } 464 if (isLoginPropertySet(CONNECTION_PASSWORD)) { 465 context.addToEnvironment(Context.SECURITY_CREDENTIALS, getLDAPPropertyValue(CONNECTION_PASSWORD)); 466 } else { 467 context.removeFromEnvironment(Context.SECURITY_CREDENTIALS); 468 } 469 context.addToEnvironment(Context.SECURITY_AUTHENTICATION, getLDAPPropertyValue(AUTHENTICATION)); 470 return isValid; 471 } 472 473 private List<String> addAttributeValues(String attrId, Attributes attrs, List<String> values) throws NamingException { 474 475 if (attrId == null || attrs == null) { 476 return values; 477 } 478 if (values == null) { 479 values = new ArrayList<String>(); 480 } 481 Attribute attr = attrs.get(attrId); 482 if (attr == null) { 483 return values; 484 } 485 NamingEnumeration<?> e = attr.getAll(); 486 while (e.hasMore()) { 487 String value = (String)e.next(); 488 values.add(value); 489 } 490 return values; 491 } 492 493 protected DirContext open() throws NamingException { 494 try { 495 Hashtable<String, String> env = new Hashtable<String, String>(); 496 env.put(Context.INITIAL_CONTEXT_FACTORY, getLDAPPropertyValue(INITIAL_CONTEXT_FACTORY)); 497 if (isLoginPropertySet(CONNECTION_USERNAME)) { 498 env.put(Context.SECURITY_PRINCIPAL, getLDAPPropertyValue(CONNECTION_USERNAME)); 499 } else { 500 throw new NamingException("Empty username is not allowed"); 501 } 502 503 if (isLoginPropertySet(CONNECTION_PASSWORD)) { 504 env.put(Context.SECURITY_CREDENTIALS, getLDAPPropertyValue(CONNECTION_PASSWORD)); 505 } else { 506 throw new NamingException("Empty password is not allowed"); 507 } 508 env.put(Context.SECURITY_PROTOCOL, getLDAPPropertyValue(CONNECTION_PROTOCOL)); 509 env.put(Context.PROVIDER_URL, getLDAPPropertyValue(CONNECTION_URL)); 510 env.put(Context.SECURITY_AUTHENTICATION, getLDAPPropertyValue(AUTHENTICATION)); 511 context = new InitialDirContext(env); 512 513 } catch (NamingException e) { 514 log.error(e.toString()); 515 throw e; 516 } 517 return context; 518 } 519 520 private String getLDAPPropertyValue (String propertyName){ 521 for (int i=0; i < config.length; i++ ) 522 if (config[i].getPropertyName() == propertyName) 523 return config[i].getPropertyValue(); 524 return null; 525 } 526 527 private boolean isLoginPropertySet(String propertyName) { 528 for (int i=0; i < config.length; i++ ) { 529 if (config[i].getPropertyName() == propertyName && (config[i].getPropertyValue() != null && !"".equals(config[i].getPropertyValue()))) 530 return true; 531 } 532 return false; 533 } 534 535}