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