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