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    package org.apache.activemq.jaas;
018    
019    import java.io.IOException;
020    import java.security.Principal;
021    import java.text.MessageFormat;
022    import java.util.ArrayList;
023    import java.util.HashSet;
024    import java.util.Hashtable;
025    import java.util.List;
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.slf4j.Logger;
053    import org.slf4j.LoggerFactory;
054    
055    /**
056     * @version $Rev: $ $Date: $
057     */
058    public class LDAPLoginModule implements LoginModule {
059    
060        private static final String INITIAL_CONTEXT_FACTORY = "initialContextFactory";
061        private static final String CONNECTION_URL = "connectionURL";
062        private static final String CONNECTION_USERNAME = "connectionUsername";
063        private static final String CONNECTION_PASSWORD = "connectionPassword";
064        private static final String CONNECTION_PROTOCOL = "connectionProtocol";
065        private static final String AUTHENTICATION = "authentication";
066        private static final String USER_BASE = "userBase";
067        private static final String USER_SEARCH_MATCHING = "userSearchMatching";
068        private static final String USER_SEARCH_SUBTREE = "userSearchSubtree";
069        private static final String ROLE_BASE = "roleBase";
070        private static final String ROLE_NAME = "roleName";
071        private static final String ROLE_SEARCH_MATCHING = "roleSearchMatching";
072        private static final String ROLE_SEARCH_SUBTREE = "roleSearchSubtree";
073        private static final String USER_ROLE_NAME = "userRoleName";
074    
075        private static Logger log = LoggerFactory.getLogger(LDAPLoginModule.class);
076    
077        protected DirContext context;
078    
079        private Subject subject;
080        private CallbackHandler handler;  
081        private LDAPLoginProperty [] config;
082        private String username;
083        private Set<GroupPrincipal> groups = new HashSet<GroupPrincipal>();
084    
085        @Override
086        public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
087            this.subject = subject;
088            this.handler = callbackHandler;
089            
090            config = new LDAPLoginProperty [] {
091                            new LDAPLoginProperty (INITIAL_CONTEXT_FACTORY, (String)options.get(INITIAL_CONTEXT_FACTORY)),
092                            new LDAPLoginProperty (CONNECTION_URL, (String)options.get(CONNECTION_URL)),
093                            new LDAPLoginProperty (CONNECTION_USERNAME, (String)options.get(CONNECTION_USERNAME)),
094                            new LDAPLoginProperty (CONNECTION_PASSWORD, (String)options.get(CONNECTION_PASSWORD)),
095                            new LDAPLoginProperty (CONNECTION_PROTOCOL, (String)options.get(CONNECTION_PROTOCOL)),
096                            new LDAPLoginProperty (AUTHENTICATION, (String)options.get(AUTHENTICATION)),
097                            new LDAPLoginProperty (USER_BASE, (String)options.get(USER_BASE)),
098                            new LDAPLoginProperty (USER_SEARCH_MATCHING, (String)options.get(USER_SEARCH_MATCHING)),
099                            new LDAPLoginProperty (USER_SEARCH_SUBTREE, (String)options.get(USER_SEARCH_SUBTREE)),
100                            new LDAPLoginProperty (ROLE_BASE, (String)options.get(ROLE_BASE)),
101                            new LDAPLoginProperty (ROLE_NAME, (String)options.get(ROLE_NAME)),
102                            new LDAPLoginProperty (ROLE_SEARCH_MATCHING, (String)options.get(ROLE_SEARCH_MATCHING)),
103                            new LDAPLoginProperty (ROLE_SEARCH_SUBTREE, (String)options.get(ROLE_SEARCH_SUBTREE)),
104                            new LDAPLoginProperty (USER_ROLE_NAME, (String)options.get(USER_ROLE_NAME)),
105                            };
106        }
107    
108        @Override
109        public boolean login() throws LoginException {
110    
111            Callback[] callbacks = new Callback[2];
112    
113            callbacks[0] = new NameCallback("User name");
114            callbacks[1] = new PasswordCallback("Password", false);
115            try {
116                handler.handle(callbacks);
117            } catch (IOException ioe) {
118                throw (LoginException)new LoginException().initCause(ioe);
119            } catch (UnsupportedCallbackException uce) {
120                throw (LoginException)new LoginException().initCause(uce);
121            }
122            
123            String password;
124            
125            username = ((NameCallback)callbacks[0]).getName();
126            if (username == null)
127                    return false;
128                    
129            if (((PasswordCallback)callbacks[1]).getPassword() != null)
130                    password = new String(((PasswordCallback)callbacks[1]).getPassword());
131            else
132                    password="";
133    
134            // authenticate will throw LoginException
135            // in case of failed authentication
136            authenticate(username, password);
137            return true;
138        }
139    
140        @Override
141        public boolean logout() throws LoginException {
142            username = null;
143            return true;
144        }
145    
146        @Override
147        public boolean commit() throws LoginException {
148            Set<Principal> principals = subject.getPrincipals();
149            principals.add(new UserPrincipal(username));
150            for (GroupPrincipal gp : groups) {
151                principals.add(gp);
152            }
153            return true;
154        }
155    
156        @Override
157        public boolean abort() throws LoginException {
158            username = null;
159            return true;
160        }
161    
162        protected void close(DirContext context) {
163            try {
164                context.close();
165            } catch (Exception e) {
166                log.error(e.toString());
167            }
168        }
169    
170        protected boolean authenticate(String username, String password) throws LoginException {
171    
172            MessageFormat userSearchMatchingFormat;
173            boolean userSearchSubtreeBool;
174            
175            DirContext context = null;
176    
177            if (log.isDebugEnabled()) {
178                log.debug("Create the LDAP initial context.");
179            }
180            try {
181                context = open();
182            } catch (NamingException ne) {
183                FailedLoginException ex = new FailedLoginException("Error opening LDAP connection");
184                ex.initCause(ne);
185                throw ex;
186            }
187            
188            if (!isLoginPropertySet(USER_SEARCH_MATCHING))
189                    return false;
190    
191            userSearchMatchingFormat = new MessageFormat(getLDAPPropertyValue(USER_SEARCH_MATCHING));
192            userSearchSubtreeBool = Boolean.valueOf(getLDAPPropertyValue(USER_SEARCH_SUBTREE)).booleanValue();
193    
194            try {
195    
196                String filter = userSearchMatchingFormat.format(new String[] {
197                    username
198                });
199                SearchControls constraints = new SearchControls();
200                if (userSearchSubtreeBool) {
201                    constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
202                } else {
203                    constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
204                }
205    
206                // setup attributes
207                List<String> list = new ArrayList<String>();
208                if (isLoginPropertySet(USER_ROLE_NAME)) {
209                    list.add(getLDAPPropertyValue(USER_ROLE_NAME));
210                }
211                String[] attribs = new String[list.size()];
212                list.toArray(attribs);
213                constraints.setReturningAttributes(attribs);
214    
215                if (log.isDebugEnabled()) {
216                    log.debug("Get the user DN.");
217                    log.debug("Looking for the user in LDAP with ");
218                    log.debug("  base DN: " + getLDAPPropertyValue(USER_BASE));
219                    log.debug("  filter: " + filter);
220                }
221    
222                NamingEnumeration<SearchResult> results = context.search(getLDAPPropertyValue(USER_BASE), filter, constraints);
223    
224                if (results == null || !results.hasMore()) {
225                    log.warn("User " + username + " not found in LDAP.");
226                    throw new FailedLoginException("User " + username + " not found in LDAP.");
227                }
228    
229                SearchResult result = results.next();
230    
231                if (results.hasMore()) {
232                    // ignore for now
233                }
234                NameParser parser = context.getNameParser("");
235                Name contextName = parser.parse(context.getNameInNamespace());
236                Name baseName = parser.parse(getLDAPPropertyValue(USER_BASE));
237                Name entryName = parser.parse(result.getName());
238                Name name = contextName.addAll(baseName);
239                name = name.addAll(entryName);
240                String dn = name.toString();
241    
242                Attributes attrs = result.getAttributes();
243                if (attrs == null) {
244                    throw new FailedLoginException("User found, but LDAP entry malformed: " + username);
245                }
246                List<String> roles = null;
247                if (isLoginPropertySet(USER_ROLE_NAME)) {
248                    roles = addAttributeValues(getLDAPPropertyValue(USER_ROLE_NAME), attrs, roles);
249                }
250    
251                // check the credentials by binding to server
252                if (bindUser(context, dn, password)) {
253                    // if authenticated add more roles
254                    roles = getRoles(context, dn, username, roles);
255                    if (log.isDebugEnabled()) {
256                        log.debug("Roles " + roles + " for user " + username);
257                    }
258                    for (int i = 0; i < roles.size(); i++) {
259                        groups.add(new GroupPrincipal(roles.get(i)));
260                    }
261                } else {
262                    throw new FailedLoginException("Password does not match for user: " + username);
263                }
264            } catch (CommunicationException e) {
265                FailedLoginException ex = new FailedLoginException("Error contacting LDAP");
266                ex.initCause(e);
267                throw ex;
268            } catch (NamingException e) {
269                if (context != null) {
270                    close(context);
271                }
272                FailedLoginException ex = new FailedLoginException("Error contacting LDAP");
273                ex.initCause(e);
274                throw ex;
275            }
276    
277            return true;
278        }
279    
280        protected List<String> getRoles(DirContext context, String dn, String username, List<String> currentRoles) throws NamingException {
281            List<String> list = currentRoles;
282            MessageFormat roleSearchMatchingFormat;
283            boolean roleSearchSubtreeBool;
284            roleSearchMatchingFormat = new MessageFormat(getLDAPPropertyValue(ROLE_SEARCH_MATCHING));
285            roleSearchSubtreeBool = Boolean.valueOf(getLDAPPropertyValue(ROLE_SEARCH_SUBTREE)).booleanValue();
286            
287            if (list == null) {
288                list = new ArrayList<String>();
289            }
290            if (!isLoginPropertySet(ROLE_NAME)) {
291                return list;
292            }
293            String filter = roleSearchMatchingFormat.format(new String[] {
294                doRFC2254Encoding(dn), username
295            });
296    
297            SearchControls constraints = new SearchControls();
298            if (roleSearchSubtreeBool) {
299                constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
300            } else {
301                constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
302            }
303            if (log.isDebugEnabled()) {
304                log.debug("Get user roles.");
305                log.debug("Looking for the user roles in LDAP with ");
306                log.debug("  base DN: " + getLDAPPropertyValue(ROLE_BASE));
307                log.debug("  filter: " + filter);
308            }
309            NamingEnumeration<SearchResult> results = context.search(getLDAPPropertyValue(ROLE_BASE), filter, constraints);
310            while (results.hasMore()) {
311                SearchResult result = results.next();
312                Attributes attrs = result.getAttributes();
313                if (attrs == null) {
314                    continue;
315                }
316                list = addAttributeValues(getLDAPPropertyValue(ROLE_NAME), attrs, list);
317            }
318            return list;
319    
320        }
321    
322        protected String doRFC2254Encoding(String inputString) {
323            StringBuffer buf = new StringBuffer(inputString.length());
324            for (int i = 0; i < inputString.length(); i++) {
325                char c = inputString.charAt(i);
326                switch (c) {
327                case '\\':
328                    buf.append("\\5c");
329                    break;
330                case '*':
331                    buf.append("\\2a");
332                    break;
333                case '(':
334                    buf.append("\\28");
335                    break;
336                case ')':
337                    buf.append("\\29");
338                    break;
339                case '\0':
340                    buf.append("\\00");
341                    break;
342                default:
343                    buf.append(c);
344                    break;
345                }
346            }
347            return buf.toString();
348        }
349    
350        protected boolean bindUser(DirContext context, String dn, String password) throws NamingException {
351            boolean isValid = false;
352    
353            if (log.isDebugEnabled()) {
354                log.debug("Binding the user.");
355            }
356            context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn);
357            context.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
358            try {
359                context.getAttributes("", null);
360                isValid = true;
361                if (log.isDebugEnabled()) {
362                    log.debug("User " + dn + " successfully bound.");
363                }
364            } catch (AuthenticationException e) {
365                isValid = false;
366                if (log.isDebugEnabled()) {
367                    log.debug("Authentication failed for dn=" + dn);
368                }
369            }
370    
371            if (isLoginPropertySet(CONNECTION_USERNAME)) {
372                context.addToEnvironment(Context.SECURITY_PRINCIPAL, getLDAPPropertyValue(CONNECTION_USERNAME));
373            } else {
374                context.removeFromEnvironment(Context.SECURITY_PRINCIPAL);
375            }
376            if (isLoginPropertySet(CONNECTION_PASSWORD)) {
377                context.addToEnvironment(Context.SECURITY_CREDENTIALS, getLDAPPropertyValue(CONNECTION_PASSWORD));
378            } else {
379                context.removeFromEnvironment(Context.SECURITY_CREDENTIALS);
380            }
381    
382            return isValid;
383        }
384    
385        private List<String> addAttributeValues(String attrId, Attributes attrs, List<String> values) throws NamingException {
386    
387            if (attrId == null || attrs == null) {
388                return values;
389            }
390            if (values == null) {
391                values = new ArrayList<String>();
392            }
393            Attribute attr = attrs.get(attrId);
394            if (attr == null) {
395                return values;
396            }
397            NamingEnumeration<?> e = attr.getAll();
398            while (e.hasMore()) {
399                String value = (String)e.next();
400                values.add(value);
401            }
402            return values;
403        }
404    
405        protected DirContext open() throws NamingException {
406            try {
407                Hashtable<String, String> env = new Hashtable<String, String>();
408                env.put(Context.INITIAL_CONTEXT_FACTORY, getLDAPPropertyValue(INITIAL_CONTEXT_FACTORY));
409                if (isLoginPropertySet(CONNECTION_USERNAME)) {
410                    env.put(Context.SECURITY_PRINCIPAL, getLDAPPropertyValue(CONNECTION_USERNAME));
411                }
412                if (isLoginPropertySet(CONNECTION_PASSWORD)) {
413                    env.put(Context.SECURITY_CREDENTIALS, getLDAPPropertyValue(CONNECTION_PASSWORD));
414                }
415                env.put(Context.SECURITY_PROTOCOL, getLDAPPropertyValue(CONNECTION_PROTOCOL));
416                env.put(Context.PROVIDER_URL, getLDAPPropertyValue(CONNECTION_URL));
417                env.put(Context.SECURITY_AUTHENTICATION, getLDAPPropertyValue(AUTHENTICATION));
418                context = new InitialDirContext(env);
419    
420            } catch (NamingException e) {
421                log.error(e.toString());
422                throw e;
423            }
424            return context;
425        }
426        
427        private String getLDAPPropertyValue (String propertyName){
428            for (int i=0; i < config.length; i++ )
429                    if (config[i].getPropertyName() == propertyName)
430                            return config[i].getPropertyValue();
431            return null;
432        }
433        
434        private boolean isLoginPropertySet(String propertyName) {
435            for (int i=0; i < config.length; i++ ) {
436                    if (config[i].getPropertyName() == propertyName && config[i].getPropertyValue() != null)
437                                    return true;
438            }
439            return false;
440        }
441    
442    }