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 return true; 336 } 337 338 protected List<String> getRoles(DirContext context, String dn, String username, List<String> currentRoles) throws NamingException { 339 List<String> list = currentRoles; 340 MessageFormat roleSearchMatchingFormat; 341 boolean roleSearchSubtreeBool; 342 boolean expandRolesBool; 343 roleSearchMatchingFormat = new MessageFormat(getLDAPPropertyValue(ROLE_SEARCH_MATCHING)); 344 roleSearchSubtreeBool = Boolean.valueOf(getLDAPPropertyValue(ROLE_SEARCH_SUBTREE)).booleanValue(); 345 expandRolesBool = Boolean.valueOf(getLDAPPropertyValue(EXPAND_ROLES)).booleanValue(); 346 347 if (list == null) { 348 list = new ArrayList<String>(); 349 } 350 if (!isLoginPropertySet(ROLE_NAME)) { 351 return list; 352 } 353 String filter = roleSearchMatchingFormat.format(new String[] { 354 doRFC2254Encoding(dn), doRFC2254Encoding(username) 355 }); 356 357 SearchControls constraints = new SearchControls(); 358 if (roleSearchSubtreeBool) { 359 constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); 360 } else { 361 constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE); 362 } 363 if (log.isDebugEnabled()) { 364 log.debug("Get user roles."); 365 log.debug("Looking for the user roles in LDAP with "); 366 log.debug(" base DN: " + getLDAPPropertyValue(ROLE_BASE)); 367 log.debug(" filter: " + filter); 368 } 369 HashSet<String> haveSeenNames = new HashSet<String>(); 370 Queue<String> pendingNameExpansion = new LinkedList<String>(); 371 NamingEnumeration<SearchResult> results = context.search(getLDAPPropertyValue(ROLE_BASE), filter, constraints); 372 while (results.hasMore()) { 373 SearchResult result = results.next(); 374 Attributes attrs = result.getAttributes(); 375 if (expandRolesBool) { 376 haveSeenNames.add(result.getNameInNamespace()); 377 pendingNameExpansion.add(result.getNameInNamespace()); 378 } 379 if (attrs == null) { 380 continue; 381 } 382 list = addAttributeValues(getLDAPPropertyValue(ROLE_NAME), attrs, list); 383 } 384 if (expandRolesBool) { 385 MessageFormat expandRolesMatchingFormat = new MessageFormat(getLDAPPropertyValue(EXPAND_ROLES_MATCHING)); 386 while (!pendingNameExpansion.isEmpty()) { 387 String name = pendingNameExpansion.remove(); 388 filter = expandRolesMatchingFormat.format(new String[]{name}); 389 results = context.search(getLDAPPropertyValue(ROLE_BASE), filter, constraints); 390 while (results.hasMore()) { 391 SearchResult result = results.next(); 392 name = result.getNameInNamespace(); 393 if (!haveSeenNames.contains(name)) { 394 Attributes attrs = result.getAttributes(); 395 list = addAttributeValues(getLDAPPropertyValue(ROLE_NAME), attrs, list); 396 haveSeenNames.add(name); 397 pendingNameExpansion.add(name); 398 } 399 } 400 } 401 } 402 return list; 403 } 404 405 protected String doRFC2254Encoding(String inputString) { 406 StringBuffer buf = new StringBuffer(inputString.length()); 407 for (int i = 0; i < inputString.length(); i++) { 408 char c = inputString.charAt(i); 409 switch (c) { 410 case '\\': 411 buf.append("\\5c"); 412 break; 413 case '*': 414 buf.append("\\2a"); 415 break; 416 case '(': 417 buf.append("\\28"); 418 break; 419 case ')': 420 buf.append("\\29"); 421 break; 422 case '\0': 423 buf.append("\\00"); 424 break; 425 default: 426 buf.append(c); 427 break; 428 } 429 } 430 return buf.toString(); 431 } 432 433 protected boolean bindUser(DirContext context, String dn, String password) throws NamingException { 434 boolean isValid = false; 435 436 if (log.isDebugEnabled()) { 437 log.debug("Binding the user."); 438 } 439 context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn); 440 context.addToEnvironment(Context.SECURITY_CREDENTIALS, password); 441 try { 442 context.getAttributes("", null); 443 isValid = true; 444 if (log.isDebugEnabled()) { 445 log.debug("User " + dn + " successfully bound."); 446 } 447 } catch (AuthenticationException e) { 448 isValid = false; 449 if (log.isDebugEnabled()) { 450 log.debug("Authentication failed for dn=" + dn); 451 } 452 } 453 454 if (isLoginPropertySet(CONNECTION_USERNAME)) { 455 context.addToEnvironment(Context.SECURITY_PRINCIPAL, getLDAPPropertyValue(CONNECTION_USERNAME)); 456 } else { 457 context.removeFromEnvironment(Context.SECURITY_PRINCIPAL); 458 } 459 if (isLoginPropertySet(CONNECTION_PASSWORD)) { 460 context.addToEnvironment(Context.SECURITY_CREDENTIALS, getLDAPPropertyValue(CONNECTION_PASSWORD)); 461 } else { 462 context.removeFromEnvironment(Context.SECURITY_CREDENTIALS); 463 } 464 465 return isValid; 466 } 467 468 private List<String> addAttributeValues(String attrId, Attributes attrs, List<String> values) throws NamingException { 469 470 if (attrId == null || attrs == null) { 471 return values; 472 } 473 if (values == null) { 474 values = new ArrayList<String>(); 475 } 476 Attribute attr = attrs.get(attrId); 477 if (attr == null) { 478 return values; 479 } 480 NamingEnumeration<?> e = attr.getAll(); 481 while (e.hasMore()) { 482 String value = (String)e.next(); 483 values.add(value); 484 } 485 return values; 486 } 487 488 protected DirContext open() throws NamingException { 489 try { 490 Hashtable<String, String> env = new Hashtable<String, String>(); 491 env.put(Context.INITIAL_CONTEXT_FACTORY, getLDAPPropertyValue(INITIAL_CONTEXT_FACTORY)); 492 if (isLoginPropertySet(CONNECTION_USERNAME)) { 493 env.put(Context.SECURITY_PRINCIPAL, getLDAPPropertyValue(CONNECTION_USERNAME)); 494 } else { 495 throw new NamingException("Empty username is not allowed"); 496 } 497 498 if (isLoginPropertySet(CONNECTION_PASSWORD)) { 499 env.put(Context.SECURITY_CREDENTIALS, getLDAPPropertyValue(CONNECTION_PASSWORD)); 500 } else { 501 throw new NamingException("Empty password is not allowed"); 502 } 503 env.put(Context.SECURITY_PROTOCOL, getLDAPPropertyValue(CONNECTION_PROTOCOL)); 504 env.put(Context.PROVIDER_URL, getLDAPPropertyValue(CONNECTION_URL)); 505 env.put(Context.SECURITY_AUTHENTICATION, getLDAPPropertyValue(AUTHENTICATION)); 506 context = new InitialDirContext(env); 507 508 } catch (NamingException e) { 509 log.error(e.toString()); 510 throw e; 511 } 512 return context; 513 } 514 515 private String getLDAPPropertyValue (String propertyName){ 516 for (int i=0; i < config.length; i++ ) 517 if (config[i].getPropertyName() == propertyName) 518 return config[i].getPropertyValue(); 519 return null; 520 } 521 522 private boolean isLoginPropertySet(String propertyName) { 523 for (int i=0; i < config.length; i++ ) { 524 if (config[i].getPropertyName() == propertyName && (config[i].getPropertyValue() != null && !"".equals(config[i].getPropertyValue()))) 525 return true; 526 } 527 return false; 528 } 529 530}