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    }