001 /** 002 * 003 * Copyright 2003-2004 The Apache Software Foundation 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * 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 018 package org.apache.geronimo.security.realm.providers; 019 020 import java.io.IOException; 021 import java.text.MessageFormat; 022 import java.util.ArrayList; 023 import java.util.HashSet; 024 import java.util.Hashtable; 025 import java.util.Iterator; 026 import java.util.List; 027 import java.util.Map; 028 import java.util.Set; 029 030 import javax.naming.AuthenticationException; 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.CommunicationException; 041 import javax.naming.directory.SearchControls; 042 import javax.naming.directory.SearchResult; 043 import javax.security.auth.Subject; 044 import javax.security.auth.callback.Callback; 045 import javax.security.auth.callback.CallbackHandler; 046 import javax.security.auth.callback.NameCallback; 047 import javax.security.auth.callback.PasswordCallback; 048 import javax.security.auth.callback.UnsupportedCallbackException; 049 import javax.security.auth.login.LoginException; 050 import javax.security.auth.login.FailedLoginException; 051 import javax.security.auth.spi.LoginModule; 052 053 import org.apache.commons.logging.Log; 054 import org.apache.commons.logging.LogFactory; 055 056 import org.apache.geronimo.security.realm.providers.GeronimoGroupPrincipal; 057 import org.apache.geronimo.security.realm.providers.GeronimoUserPrincipal; 058 059 060 public class LDAPLoginModule implements LoginModule { 061 062 private static Log log = LogFactory.getLog(LDAPLoginModule.class); 063 064 private Subject subject; 065 private CallbackHandler handler; 066 067 private static final String INITIAL_CONTEXT_FACTORY = "initialContextFactory"; 068 private static final String CONNECTION_URL = "connectionURL"; 069 private static final String CONNECTION_USERNAME = "connectionUsername"; 070 private static final String CONNECTION_PASSWORD = "connectionPassword"; 071 private static final String CONNECTION_PROTOCOL = "connectionProtocol"; 072 private static final String AUTHENTICATION = "authentication"; 073 private static final String USER_BASE = "userBase"; 074 private static final String USER_SEARCH_MATCHING = "userSearchMatching"; 075 private static final String USER_SEARCH_SUBTREE = "userSearchSubtree"; 076 private static final String ROLE_BASE = "roleBase"; 077 private static final String ROLE_NAME = "roleName"; 078 private static final String ROLE_SEARCH_MATCHING = "roleSearchMatching"; 079 private static final String ROLE_SEARCH_SUBTREE = "roleSearchSubtree"; 080 private static final String USER_ROLE_NAME = "userRoleName"; 081 082 private String initialContextFactory; 083 private String connectionURL; 084 private String connectionUsername; 085 private String connectionPassword; 086 private String connectionProtocol; 087 private String authentication; 088 private String userBase; 089 private String userSearchMatching; 090 private String userPassword; 091 private String roleBase; 092 private String roleName; 093 private String roleSearchMatching; 094 private String userSearchSubtree; 095 private String roleSearchSubtree; 096 private String userRoleName; 097 098 private String cbUsername; 099 private String cbPassword; 100 101 protected DirContext context = null; 102 103 private MessageFormat userSearchMatchingFormat; 104 private MessageFormat roleSearchMatchingFormat; 105 106 private boolean userSearchSubtreeBool = false; 107 private boolean roleSearchSubtreeBool = false; 108 109 Set groups = new HashSet(); 110 111 public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { 112 this.subject = subject; 113 this.handler = callbackHandler; 114 initialContextFactory = (String) options.get(INITIAL_CONTEXT_FACTORY); 115 connectionURL = (String) options.get(CONNECTION_URL); 116 connectionUsername = (String) options.get(CONNECTION_USERNAME); 117 connectionPassword = (String) options.get(CONNECTION_PASSWORD); 118 connectionProtocol = (String) options.get(CONNECTION_PROTOCOL); 119 authentication = (String) options.get(AUTHENTICATION); 120 userBase = (String) options.get(USER_BASE); 121 userSearchMatching = (String) options.get(USER_SEARCH_MATCHING); 122 userSearchSubtree = (String) options.get(USER_SEARCH_SUBTREE); 123 roleBase = (String) options.get(ROLE_BASE); 124 roleName = (String) options.get(ROLE_NAME); 125 roleSearchMatching = (String) options.get(ROLE_SEARCH_MATCHING); 126 roleSearchSubtree = (String) options.get(ROLE_SEARCH_SUBTREE); 127 userRoleName = (String) options.get(USER_ROLE_NAME); 128 userSearchMatchingFormat = new MessageFormat(userSearchMatching); 129 roleSearchMatchingFormat = new MessageFormat(roleSearchMatching); 130 userSearchSubtreeBool = new Boolean(userSearchSubtree).booleanValue(); 131 roleSearchSubtreeBool = new Boolean(roleSearchSubtree).booleanValue(); 132 } 133 134 public boolean login() throws LoginException { 135 Callback[] callbacks = new Callback[2]; 136 137 callbacks[0] = new NameCallback("User name"); 138 callbacks[1] = new PasswordCallback("Password", false); 139 try { 140 handler.handle(callbacks); 141 } catch (IOException ioe) { 142 throw (LoginException) new LoginException().initCause(ioe); 143 } catch (UnsupportedCallbackException uce) { 144 throw (LoginException) new LoginException().initCause(uce); 145 } 146 cbUsername = ((NameCallback) callbacks[0]).getName(); 147 cbPassword = new String(((PasswordCallback) callbacks[1]).getPassword()); 148 149 if (cbUsername == null || "".equals(cbUsername) 150 || cbPassword == null || "".equals(cbPassword)) { 151 return false; 152 } 153 154 try { 155 boolean result = authenticate(cbUsername, cbPassword); 156 if(!result) { 157 throw new FailedLoginException(); 158 } else { 159 return true; 160 } 161 } catch (Exception e) { 162 throw (LoginException) new LoginException("LDAP Error").initCause(e); 163 } 164 } 165 166 public boolean logout() throws LoginException { 167 cbUsername = null; 168 cbPassword = null; 169 //todo: should remove principals added by commit 170 return true; 171 } 172 173 public boolean commit() throws LoginException { 174 Set principals = subject.getPrincipals(); 175 principals.add(new GeronimoUserPrincipal(cbUsername)); 176 Iterator iter = groups.iterator(); 177 while (iter.hasNext()) { 178 principals.add(iter.next()); 179 } 180 return true; 181 } 182 183 public boolean abort() throws LoginException { 184 cbUsername = null; 185 cbPassword = null; 186 return true; 187 } 188 189 protected void close(DirContext context) { 190 try { 191 context.close(); 192 } catch (Exception e) { 193 log.error(e); 194 } 195 } 196 197 protected boolean authenticate(String username, String password) throws Exception { 198 199 DirContext context = null; 200 context = open(); 201 202 try { 203 204 String filter = userSearchMatchingFormat.format(new String[]{username}); 205 SearchControls constraints = new SearchControls(); 206 if (userSearchSubtreeBool) { 207 constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); 208 } else { 209 constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE); 210 } 211 212 //setup attributes 213 ArrayList list = new ArrayList(); 214 if (userRoleName != null) { 215 list.add(userRoleName); 216 } 217 String[] attribs = new String[list.size()]; 218 list.toArray(attribs); 219 constraints.setReturningAttributes(attribs); 220 221 222 NamingEnumeration results = context.search(userBase, filter, constraints); 223 224 if (results == null || !results.hasMore()) { 225 return false; 226 } 227 228 SearchResult result = (SearchResult) results.next(); 229 230 if (results.hasMore()) { 231 //ignore for now 232 } 233 NameParser parser = context.getNameParser(""); 234 Name contextName = parser.parse(context.getNameInNamespace()); 235 Name baseName = parser.parse(userBase); 236 Name entryName = parser.parse(result.getName()); 237 Name name = contextName.addAll(baseName); 238 name = name.addAll(entryName); 239 String dn = name.toString(); 240 241 Attributes attrs = result.getAttributes(); 242 if (attrs == null) { 243 return false; 244 } 245 ArrayList roles = null; 246 if (userRoleName != null) { 247 roles = addAttributeValues(userRoleName, attrs, roles); 248 } 249 250 //check the credentials by binding to server 251 if (bindUser(context, dn, password)) { 252 //if authenticated add more roles 253 roles = getRoles(context, dn, username, roles); 254 for (int i = 0; i < roles.size(); i++) { 255 groups.add(new GeronimoGroupPrincipal((String) roles.get(i))); 256 } 257 } else { 258 return false; 259 } 260 } catch (CommunicationException e) { 261 262 } catch (NamingException e) { 263 if (context != null) { 264 close(context); 265 } 266 return false; 267 } 268 269 270 return true; 271 } 272 273 protected ArrayList getRoles(DirContext context, String dn, String username, ArrayList currentRoles) throws NamingException { 274 ArrayList list = currentRoles; 275 if (list == null) { 276 list = new ArrayList(); 277 } 278 if (roleName == null || "".equals(roleName)) { 279 return list; 280 } 281 String filter = roleSearchMatchingFormat.format(new String[]{doRFC2254Encoding(dn), username}); 282 283 SearchControls constraints = new SearchControls(); 284 if (roleSearchSubtreeBool) { 285 constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); 286 } else { 287 constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE); 288 } 289 NamingEnumeration results = 290 context.search(roleBase, filter, constraints); 291 while (results.hasMore()) { 292 SearchResult result = (SearchResult) results.next(); 293 Attributes attrs = result.getAttributes(); 294 if (attrs == null) { 295 continue; 296 } 297 list = addAttributeValues(roleName, attrs, list); 298 } 299 return list; 300 301 } 302 303 304 protected String doRFC2254Encoding(String inputString) { 305 StringBuffer buf = new StringBuffer(inputString.length()); 306 for (int i = 0; i < inputString.length(); i++) { 307 char c = inputString.charAt(i); 308 switch (c) { 309 case '\\': 310 buf.append("\\5c"); 311 break; 312 case '*': 313 buf.append("\\2a"); 314 break; 315 case '(': 316 buf.append("\\28"); 317 break; 318 case ')': 319 buf.append("\\29"); 320 break; 321 case '\0': 322 buf.append("\\00"); 323 break; 324 default: 325 buf.append(c); 326 break; 327 } 328 } 329 return buf.toString(); 330 } 331 332 protected boolean bindUser(DirContext context, String dn, String password) throws NamingException { 333 boolean isValid = false; 334 Attributes attr; 335 336 context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn); 337 context.addToEnvironment(Context.SECURITY_CREDENTIALS, password); 338 try { 339 attr = context.getAttributes("", null); 340 isValid = true; 341 } catch (AuthenticationException e) { 342 isValid = false; 343 log.debug("Authentication failed for dn=" + dn); 344 } 345 346 if (connectionUsername != null) { 347 context.addToEnvironment(Context.SECURITY_PRINCIPAL, 348 connectionUsername); 349 } else { 350 context.removeFromEnvironment(Context.SECURITY_PRINCIPAL); 351 } 352 353 if (connectionPassword != null) { 354 context.addToEnvironment(Context.SECURITY_CREDENTIALS, 355 connectionPassword); 356 } else { 357 context.removeFromEnvironment(Context.SECURITY_CREDENTIALS); 358 } 359 360 return isValid; 361 } 362 363 private String getAttributeValue(String attrId, Attributes attrs) 364 throws NamingException { 365 366 if (attrId == null || attrs == null) { 367 return null; 368 } 369 370 Attribute attr = attrs.get(attrId); 371 if (attr == null) { 372 return (null); 373 } 374 Object value = attr.get(); 375 if (value == null) { 376 return (null); 377 } 378 String valueString = null; 379 if (value instanceof byte[]) { 380 valueString = new String((byte[]) value); 381 } else { 382 valueString = value.toString(); 383 } 384 return valueString; 385 } 386 387 private ArrayList addAttributeValues(String attrId, Attributes attrs, ArrayList values) 388 throws NamingException { 389 390 if (attrId == null || attrs == null) { 391 return values; 392 } 393 if (values == null) { 394 values = new ArrayList(); 395 } 396 Attribute attr = attrs.get(attrId); 397 if (attr == null) { 398 return (values); 399 } 400 NamingEnumeration e = attr.getAll(); 401 while (e.hasMore()) { 402 String value = (String) e.next(); 403 values.add(value); 404 } 405 return values; 406 } 407 408 protected DirContext open() throws NamingException { 409 if (context != null) { 410 return context; 411 } 412 413 try { 414 Hashtable env = new Hashtable(); 415 env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory); 416 if (connectionUsername != null || !"".equals(connectionUsername)) { 417 env.put(Context.SECURITY_PRINCIPAL, connectionUsername); 418 } 419 if (connectionPassword != null || !"".equals(connectionPassword)) { 420 env.put(Context.SECURITY_CREDENTIALS, connectionPassword); 421 } 422 env.put(Context.SECURITY_PROTOCOL, connectionProtocol); 423 env.put(Context.PROVIDER_URL, connectionURL); 424 env.put(Context.SECURITY_AUTHENTICATION, authentication); 425 context = new InitialDirContext(env); 426 427 } catch (NamingException e) { 428 log.error(e); 429 throw e; 430 } 431 return context; 432 } 433 434 }