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.Arrays;
025 import java.util.Collections;
026 import java.util.HashSet;
027 import java.util.Hashtable;
028 import java.util.List;
029 import java.util.Map;
030 import java.util.Set;
031
032 import javax.naming.AuthenticationException;
033 import javax.naming.CommunicationException;
034 import javax.naming.Context;
035 import javax.naming.Name;
036 import javax.naming.NameParser;
037 import javax.naming.NamingEnumeration;
038 import javax.naming.NamingException;
039 import javax.naming.directory.Attribute;
040 import javax.naming.directory.Attributes;
041 import javax.naming.directory.DirContext;
042 import javax.naming.directory.InitialDirContext;
043 import javax.naming.directory.SearchControls;
044 import javax.naming.directory.SearchResult;
045 import javax.security.auth.Subject;
046 import javax.security.auth.callback.Callback;
047 import javax.security.auth.callback.CallbackHandler;
048 import javax.security.auth.callback.NameCallback;
049 import javax.security.auth.callback.PasswordCallback;
050 import javax.security.auth.callback.UnsupportedCallbackException;
051 import javax.security.auth.login.FailedLoginException;
052 import javax.security.auth.login.LoginException;
053 import javax.security.auth.spi.LoginModule;
054
055 import org.apache.commons.logging.Log;
056 import org.apache.commons.logging.LogFactory;
057 import org.apache.geronimo.security.jaas.JaasLoginModuleUse;
058 import org.apache.geronimo.security.jaas.WrappingLoginModule;
059
060
061 /**
062 * LDAPLoginModule is a login module using ldap as an authentication store.
063 * <p/>
064 * This login module checks security credentials so the lifecycle methods must return true to indicate success
065 * or throw LoginException to indicate failure.
066 *
067 * @version $Rev: 706640 $ $Date: 2008-10-21 14:44:05 +0000 (Tue, 21 Oct 2008) $
068 */
069 public class LDAPLoginModule implements LoginModule {
070
071 private static Log log = LogFactory.getLog(LDAPLoginModule.class);
072
073 private Subject subject;
074 private CallbackHandler handler;
075
076 private static final String INITIAL_CONTEXT_FACTORY = "initialContextFactory";
077 private static final String CONNECTION_URL = "connectionURL";
078 private static final String CONNECTION_USERNAME = "connectionUsername";
079 private static final String CONNECTION_PASSWORD = "connectionPassword";
080 private static final String CONNECTION_PROTOCOL = "connectionProtocol";
081 private static final String AUTHENTICATION = "authentication";
082 private static final String USER_BASE = "userBase";
083 private static final String USER_SEARCH_MATCHING = "userSearchMatching";
084 private static final String USER_SEARCH_SUBTREE = "userSearchSubtree";
085 private static final String ROLE_BASE = "roleBase";
086 private static final String ROLE_NAME = "roleName";
087 private static final String ROLE_SEARCH_MATCHING = "roleSearchMatching";
088 private static final String ROLE_SEARCH_SUBTREE = "roleSearchSubtree";
089 private static final String USER_ROLE_NAME = "userRoleName";
090 public final static List<String> supportedOptions = Collections.unmodifiableList(Arrays.asList(INITIAL_CONTEXT_FACTORY, CONNECTION_URL,
091 CONNECTION_USERNAME, CONNECTION_PASSWORD, CONNECTION_PROTOCOL, AUTHENTICATION, USER_BASE,
092 USER_SEARCH_MATCHING, USER_SEARCH_SUBTREE, ROLE_BASE, ROLE_NAME, ROLE_SEARCH_MATCHING, ROLE_SEARCH_SUBTREE,
093 USER_ROLE_NAME));
094
095 private String initialContextFactory;
096 private String connectionURL;
097 private String connectionUsername;
098 private String connectionPassword;
099 private String connectionProtocol;
100 private String authentication;
101 private String userBase;
102 private String roleBase;
103 private String roleName;
104 private String userRoleName;
105
106 private String cbUsername;
107 private String cbPassword;
108
109 protected DirContext context = null;
110
111 private MessageFormat userSearchMatchingFormat;
112 private MessageFormat roleSearchMatchingFormat;
113
114 private boolean userSearchSubtreeBool = false;
115 private boolean roleSearchSubtreeBool = false;
116
117 private boolean loginSucceeded;
118 private final Set<String> groups = new HashSet<String>();
119 private final Set<Principal> allPrincipals = new HashSet<Principal>();
120
121 public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
122 this.subject = subject;
123 this.handler = callbackHandler;
124 for(Object option: options.keySet()) {
125 if(!supportedOptions.contains(option) && !JaasLoginModuleUse.supportedOptions.contains(option)
126 && !WrappingLoginModule.supportedOptions.contains(option)) {
127 log.warn("Ignoring option: "+option+". Not supported.");
128 }
129 }
130 initialContextFactory = (String) options.get(INITIAL_CONTEXT_FACTORY);
131 connectionURL = (String) options.get(CONNECTION_URL);
132 connectionUsername = (String) options.get(CONNECTION_USERNAME);
133 connectionPassword = (String) options.get(CONNECTION_PASSWORD);
134 connectionProtocol = (String) options.get(CONNECTION_PROTOCOL);
135 authentication = (String) options.get(AUTHENTICATION);
136 userBase = (String) options.get(USER_BASE);
137 String userSearchMatching = (String) options.get(USER_SEARCH_MATCHING);
138 String userSearchSubtree = (String) options.get(USER_SEARCH_SUBTREE);
139 roleBase = (String) options.get(ROLE_BASE);
140 roleName = (String) options.get(ROLE_NAME);
141 String roleSearchMatching = (String) options.get(ROLE_SEARCH_MATCHING);
142 String roleSearchSubtree = (String) options.get(ROLE_SEARCH_SUBTREE);
143 userRoleName = (String) options.get(USER_ROLE_NAME);
144 userSearchMatchingFormat = new MessageFormat(userSearchMatching);
145 roleSearchMatchingFormat = new MessageFormat(roleSearchMatching);
146 userSearchSubtreeBool = Boolean.valueOf(userSearchSubtree);
147 roleSearchSubtreeBool = Boolean.valueOf(roleSearchSubtree);
148 }
149
150 /**
151 * This LoginModule is not to be ignored. So, this method should never return false.
152 * @return true if authentication succeeds, or throw a LoginException such as FailedLoginException
153 * if authentication fails
154 */
155 public boolean login() throws LoginException {
156 loginSucceeded = false;
157 Callback[] callbacks = new Callback[2];
158
159 callbacks[0] = new NameCallback("User name");
160 callbacks[1] = new PasswordCallback("Password", false);
161 try {
162 handler.handle(callbacks);
163 } catch (IOException ioe) {
164 throw (LoginException) new LoginException().initCause(ioe);
165 } catch (UnsupportedCallbackException uce) {
166 throw (LoginException) new LoginException().initCause(uce);
167 }
168 cbUsername = ((NameCallback) callbacks[0]).getName();
169 cbPassword = new String(((PasswordCallback) callbacks[1]).getPassword());
170
171 if (cbUsername == null || "".equals(cbUsername)
172 || cbPassword == null || "".equals(cbPassword)) {
173 // Clear out the private state
174 cbUsername = null;
175 cbPassword = null;
176 groups.clear();
177 throw new FailedLoginException();
178 }
179
180 try {
181 boolean result = authenticate(cbUsername, cbPassword);
182 if (!result) {
183 throw new FailedLoginException();
184 }
185 } catch (LoginException e) {
186 // Clear out the private state
187 cbUsername = null;
188 cbPassword = null;
189 groups.clear();
190 throw e;
191 } catch (Exception e) {
192 // Clear out the private state
193 cbUsername = null;
194 cbPassword = null;
195 groups.clear();
196 throw (LoginException) new LoginException("LDAP Error").initCause(e);
197 }
198
199 loginSucceeded = true;
200 return true;
201 }
202
203 /*
204 * @exception LoginException if login succeeded but commit failed.
205 *
206 * @return true if login succeeded and commit succeeded, or false if login failed but commit succeeded.
207 */
208 public boolean commit() throws LoginException {
209 if(loginSucceeded) {
210 if(cbUsername != null) {
211 allPrincipals.add(new GeronimoUserPrincipal(cbUsername));
212 }
213 for(String group: groups) {
214 allPrincipals.add(new GeronimoGroupPrincipal(group));
215 }
216 subject.getPrincipals().addAll(allPrincipals);
217 }
218
219 // Clear out the private state
220 cbUsername = null;
221 cbPassword = null;
222 groups.clear();
223
224 return loginSucceeded;
225 }
226
227 public boolean abort() throws LoginException {
228 if(loginSucceeded) {
229 // Clear out the private state
230 cbUsername = null;
231 cbPassword = null;
232 groups.clear();
233 allPrincipals.clear();
234 }
235 return loginSucceeded;
236 }
237
238 public boolean logout() throws LoginException {
239 // Clear out the private state
240 loginSucceeded = false;
241 cbUsername = null;
242 cbPassword = null;
243 groups.clear();
244 if(!subject.isReadOnly()) {
245 // Remove principals added by this LoginModule
246 subject.getPrincipals().removeAll(allPrincipals);
247 }
248 allPrincipals.clear();
249 return true;
250 }
251
252 protected void close(DirContext context) {
253 try {
254 context.close();
255 } catch (Exception e) {
256 log.error(e);
257 }
258 }
259
260 protected boolean authenticate(String username, String password) throws Exception {
261
262 DirContext context = open();
263
264 try {
265
266 String filter = userSearchMatchingFormat.format(new String[]{username});
267 SearchControls constraints = new SearchControls();
268 if (userSearchSubtreeBool) {
269 constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
270 } else {
271 constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
272 }
273
274 //setup attributes
275 String[] attribs;
276 if (userRoleName == null) {
277 attribs = new String[]{};
278 } else {
279 attribs = new String[]{userRoleName};
280 }
281 constraints.setReturningAttributes(attribs);
282
283
284 NamingEnumeration results = context.search(userBase, filter, constraints);
285
286 if (results == null || !results.hasMore()) {
287 return false;
288 }
289
290 SearchResult result = (SearchResult) results.next();
291
292 if (results.hasMore()) {
293 //ignore for now
294 }
295 NameParser parser = context.getNameParser("");
296 Name contextName = parser.parse(context.getNameInNamespace());
297 Name baseName = parser.parse(userBase);
298 Name entryName = parser.parse(result.getName());
299 Name name = contextName.addAll(baseName);
300 name = name.addAll(entryName);
301 String dn = name.toString();
302
303 Attributes attrs = result.getAttributes();
304 if (attrs == null) {
305 return false;
306 }
307 ArrayList<String> roles = null;
308 if (userRoleName != null) {
309 roles = addAttributeValues(userRoleName, attrs, roles);
310 }
311
312 //check the credentials by binding to server
313 bindUser(context, dn, password);
314 //if authenticated add more roles
315 roles = getRoles(context, dn, username, roles);
316 for (String role : roles) {
317 groups.add(role);
318 }
319 } catch (CommunicationException e) {
320 close(context);
321 throw (LoginException) new FailedLoginException().initCause(e);
322 } catch (NamingException e) {
323 close(context);
324 throw (LoginException) new FailedLoginException().initCause(e);
325 }
326
327
328 return true;
329 }
330
331 protected ArrayList<String> getRoles(DirContext context, String dn, String username, ArrayList<String> list) throws NamingException {
332 if (list == null) {
333 list = new ArrayList<String>();
334 }
335 if (roleName == null || "".equals(roleName)) {
336 return list;
337 }
338 String filter = roleSearchMatchingFormat.format(new String[]{doRFC2254Encoding(dn), username});
339
340 SearchControls constraints = new SearchControls();
341 if (roleSearchSubtreeBool) {
342 constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
343 } else {
344 constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
345 }
346 NamingEnumeration results =
347 context.search(roleBase, filter, constraints);
348 while (results.hasMore()) {
349 SearchResult result = (SearchResult) results.next();
350 Attributes attrs = result.getAttributes();
351 if (attrs == null) {
352 continue;
353 }
354 list = addAttributeValues(roleName, attrs, list);
355 }
356 return list;
357
358 }
359
360
361 protected String doRFC2254Encoding(String inputString) {
362 StringBuffer buf = new StringBuffer(inputString.length());
363 for (int i = 0; i < inputString.length(); i++) {
364 char c = inputString.charAt(i);
365 switch (c) {
366 case'\\':
367 buf.append("\\5c");
368 break;
369 case'*':
370 buf.append("\\2a");
371 break;
372 case'(':
373 buf.append("\\28");
374 break;
375 case')':
376 buf.append("\\29");
377 break;
378 case'\0':
379 buf.append("\\00");
380 break;
381 default:
382 buf.append(c);
383 break;
384 }
385 }
386 return buf.toString();
387 }
388
389 protected void bindUser(DirContext context, String dn, String password) throws NamingException, FailedLoginException {
390
391 context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn);
392 context.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
393 try {
394 context.getAttributes("", null);
395 } catch (AuthenticationException e) {
396 log.debug("Authentication failed for dn=" + dn);
397 throw new FailedLoginException();
398 } finally {
399
400 if (connectionUsername != null) {
401 context.addToEnvironment(Context.SECURITY_PRINCIPAL,
402 connectionUsername);
403 } else {
404 context.removeFromEnvironment(Context.SECURITY_PRINCIPAL);
405 }
406
407 if (connectionPassword != null) {
408 context.addToEnvironment(Context.SECURITY_CREDENTIALS,
409 connectionPassword);
410 } else {
411 context.removeFromEnvironment(Context.SECURITY_CREDENTIALS);
412 }
413 }
414 }
415
416 private ArrayList<String> addAttributeValues(String attrId, Attributes attrs, ArrayList<String> values)
417 throws NamingException {
418
419 if (attrId == null || attrs == null) {
420 return values;
421 }
422 if (values == null) {
423 values = new ArrayList<String>();
424 }
425 Attribute attr = attrs.get(attrId);
426 if (attr == null) {
427 return (values);
428 }
429 NamingEnumeration e = attr.getAll();
430 while (e.hasMore()) {
431 String value = (String) e.next();
432 values.add(value);
433 }
434 return values;
435 }
436
437 protected DirContext open() throws NamingException {
438 if (context != null) {
439 return context;
440 }
441
442 try {
443 Hashtable<String, String> env = new Hashtable<String, String>();
444 env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory);
445 if (connectionUsername != null || !"".equals(connectionUsername)) {
446 env.put(Context.SECURITY_PRINCIPAL, connectionUsername);
447 }
448 if (connectionPassword != null || !"".equals(connectionPassword)) {
449 env.put(Context.SECURITY_CREDENTIALS, connectionPassword);
450 }
451 env.put(Context.SECURITY_PROTOCOL, connectionProtocol == null ? "" : connectionProtocol);
452 env.put(Context.PROVIDER_URL, connectionURL == null ? "" : connectionURL);
453 env.put(Context.SECURITY_AUTHENTICATION, authentication == null ? "" : authentication);
454 context = new InitialDirContext(env);
455
456 } catch (NamingException e) {
457 log.error(e);
458 throw e;
459 }
460 return context;
461 }
462
463 }