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