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    }