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