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    }