View Javadoc

1   /**
2    *
3    * Copyright 2003-2004 The Apache Software Foundation
4    *
5    *  Licensed under the Apache License, Version 2.0 (the "License");
6    *  you may not use this file except in compliance with the License.
7    *  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   *  Unless required by applicable law or agreed to in writing, software
12   *  distributed under the License is distributed on an "AS IS" BASIS,
13   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   *  See the License for the specific language governing permissions and
15   *  limitations under the License.
16   */
17  
18  package org.apache.geronimo.security.realm.providers;
19  
20  import java.io.IOException;
21  import java.text.MessageFormat;
22  import java.util.ArrayList;
23  import java.util.HashSet;
24  import java.util.Hashtable;
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Set;
29  
30  import javax.naming.AuthenticationException;
31  import javax.naming.Context;
32  import javax.naming.Name;
33  import javax.naming.NameParser;
34  import javax.naming.NamingEnumeration;
35  import javax.naming.NamingException;
36  import javax.naming.directory.Attribute;
37  import javax.naming.directory.Attributes;
38  import javax.naming.directory.DirContext;
39  import javax.naming.directory.InitialDirContext;
40  import javax.naming.CommunicationException;
41  import javax.naming.directory.SearchControls;
42  import javax.naming.directory.SearchResult;
43  import javax.security.auth.Subject;
44  import javax.security.auth.callback.Callback;
45  import javax.security.auth.callback.CallbackHandler;
46  import javax.security.auth.callback.NameCallback;
47  import javax.security.auth.callback.PasswordCallback;
48  import javax.security.auth.callback.UnsupportedCallbackException;
49  import javax.security.auth.login.LoginException;
50  import javax.security.auth.login.FailedLoginException;
51  import javax.security.auth.spi.LoginModule;
52  
53  import org.apache.commons.logging.Log;
54  import org.apache.commons.logging.LogFactory;
55  
56  import org.apache.geronimo.security.realm.providers.GeronimoGroupPrincipal;
57  import org.apache.geronimo.security.realm.providers.GeronimoUserPrincipal;
58  
59  
60  public class LDAPLoginModule implements LoginModule {
61  
62      private static Log log = LogFactory.getLog(LDAPLoginModule.class);
63  
64      private Subject subject;
65      private CallbackHandler handler;
66  
67      private static final String INITIAL_CONTEXT_FACTORY = "initialContextFactory";
68      private static final String CONNECTION_URL = "connectionURL";
69      private static final String CONNECTION_USERNAME = "connectionUsername";
70      private static final String CONNECTION_PASSWORD = "connectionPassword";
71      private static final String CONNECTION_PROTOCOL = "connectionProtocol";
72      private static final String AUTHENTICATION = "authentication";
73      private static final String USER_BASE = "userBase";
74      private static final String USER_SEARCH_MATCHING = "userSearchMatching";
75      private static final String USER_SEARCH_SUBTREE = "userSearchSubtree";
76      private static final String ROLE_BASE = "roleBase";
77      private static final String ROLE_NAME = "roleName";
78      private static final String ROLE_SEARCH_MATCHING = "roleSearchMatching";
79      private static final String ROLE_SEARCH_SUBTREE = "roleSearchSubtree";
80      private static final String USER_ROLE_NAME = "userRoleName";
81  
82      private String initialContextFactory;
83      private String connectionURL;
84      private String connectionUsername;
85      private String connectionPassword;
86      private String connectionProtocol;
87      private String authentication;
88      private String userBase;
89      private String userSearchMatching;
90      private String userPassword;
91      private String roleBase;
92      private String roleName;
93      private String roleSearchMatching;
94      private String userSearchSubtree;
95      private String roleSearchSubtree;
96      private String userRoleName;
97  
98      private String cbUsername;
99      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 }