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.Iterator;
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     * @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 Log log = LogFactory.getLog(LDAPLoginModule.class);
076    
077        protected DirContext context;
078    
079        private Subject subject;
080        private CallbackHandler handler;
081        private String initialContextFactory;
082        private String connectionURL;
083        private String connectionUsername;
084        private String connectionPassword;
085        private String connectionProtocol;
086        private String authentication;
087        private String userBase;
088        private String roleBase;
089        private String roleName;
090        private String userRoleName;
091        private String username;
092        private MessageFormat userSearchMatchingFormat;
093        private MessageFormat roleSearchMatchingFormat;
094        private boolean userSearchSubtreeBool;
095        private boolean roleSearchSubtreeBool;
096        private Set<GroupPrincipal> groups = new HashSet<GroupPrincipal>();
097    
098        public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
099            this.subject = subject;
100            this.handler = callbackHandler;
101            initialContextFactory = (String)options.get(INITIAL_CONTEXT_FACTORY);
102            connectionURL = (String)options.get(CONNECTION_URL);
103            connectionUsername = (String)options.get(CONNECTION_USERNAME);
104            connectionPassword = (String)options.get(CONNECTION_PASSWORD);
105            connectionProtocol = (String)options.get(CONNECTION_PROTOCOL);
106            authentication = (String)options.get(AUTHENTICATION);
107            userBase = (String)options.get(USER_BASE);
108            String userSearchMatching = (String)options.get(USER_SEARCH_MATCHING);
109            String userSearchSubtree = (String)options.get(USER_SEARCH_SUBTREE);
110            roleBase = (String)options.get(ROLE_BASE);
111            roleName = (String)options.get(ROLE_NAME);
112            String roleSearchMatching = (String)options.get(ROLE_SEARCH_MATCHING);
113            String roleSearchSubtree = (String)options.get(ROLE_SEARCH_SUBTREE);
114            userRoleName = (String)options.get(USER_ROLE_NAME);
115            userSearchMatchingFormat = new MessageFormat(userSearchMatching);
116            roleSearchMatchingFormat = new MessageFormat(roleSearchMatching);
117            userSearchSubtreeBool = Boolean.valueOf(userSearchSubtree).booleanValue();
118            roleSearchSubtreeBool = Boolean.valueOf(roleSearchSubtree).booleanValue();
119        }
120    
121        public boolean login() throws LoginException {
122            Callback[] callbacks = new Callback[2];
123    
124            callbacks[0] = new NameCallback("User name");
125            callbacks[1] = new PasswordCallback("Password", false);
126            try {
127                handler.handle(callbacks);
128            } catch (IOException ioe) {
129                throw (LoginException)new LoginException().initCause(ioe);
130            } catch (UnsupportedCallbackException uce) {
131                throw (LoginException)new LoginException().initCause(uce);
132            }
133            username = ((NameCallback)callbacks[0]).getName();
134            String password = new String(((PasswordCallback)callbacks[1]).getPassword());
135    
136            if (username == null || "".equals(username) || password == null || "".equals(password)) {
137                return false;
138            }
139    
140            try {
141                boolean result = authenticate(username, password);
142                if (!result) {
143                    throw new FailedLoginException();
144                } else {
145                    return true;
146                }
147            } catch (Exception e) {
148                throw (LoginException)new LoginException("LDAP Error").initCause(e);
149            }
150        }
151    
152        public boolean logout() throws LoginException {
153            username = null;
154            return true;
155        }
156    
157        public boolean commit() throws LoginException {
158            Set<Principal> principals = subject.getPrincipals();
159            principals.add(new UserPrincipal(username));
160            Iterator<GroupPrincipal> iter = groups.iterator();
161            while (iter.hasNext()) {
162                principals.add(iter.next());
163            }
164            return true;
165        }
166    
167        public boolean abort() throws LoginException {
168            username = null;
169            return true;
170        }
171    
172        protected void close(DirContext context) {
173            try {
174                context.close();
175            } catch (Exception e) {
176                log.error(e);
177            }
178        }
179    
180        protected boolean authenticate(String username, String password) throws Exception {
181    
182            DirContext context = null;
183            context = open();
184    
185            try {
186    
187                String filter = userSearchMatchingFormat.format(new String[] {
188                    username
189                });
190                SearchControls constraints = new SearchControls();
191                if (userSearchSubtreeBool) {
192                    constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
193                } else {
194                    constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
195                }
196    
197                // setup attributes
198                ArrayList<String> list = new ArrayList<String>();
199                if (userRoleName != null) {
200                    list.add(userRoleName);
201                }
202                String[] attribs = new String[list.size()];
203                list.toArray(attribs);
204                constraints.setReturningAttributes(attribs);
205    
206                NamingEnumeration results = context.search(userBase, filter, constraints);
207    
208                if (results == null || !results.hasMore()) {
209                    return false;
210                }
211    
212                SearchResult result = (SearchResult)results.next();
213    
214                if (results.hasMore()) {
215                    // ignore for now
216                }
217                NameParser parser = context.getNameParser("");
218                Name contextName = parser.parse(context.getNameInNamespace());
219                Name baseName = parser.parse(userBase);
220                Name entryName = parser.parse(result.getName());
221                Name name = contextName.addAll(baseName);
222                name = name.addAll(entryName);
223                String dn = name.toString();
224    
225                Attributes attrs = result.getAttributes();
226                if (attrs == null) {
227                    return false;
228                }
229                ArrayList<String> roles = null;
230                if (userRoleName != null) {
231                    roles = addAttributeValues(userRoleName, attrs, roles);
232                }
233    
234                // check the credentials by binding to server
235                if (bindUser(context, dn, password)) {
236                    // if authenticated add more roles
237                    roles = getRoles(context, dn, username, roles);
238                    for (int i = 0; i < roles.size(); i++) {
239                        groups.add(new GroupPrincipal(roles.get(i)));
240                    }
241                } else {
242                    return false;
243                }
244            } catch (CommunicationException e) {
245    
246            } catch (NamingException e) {
247                if (context != null) {
248                    close(context);
249                }
250                return false;
251            }
252    
253            return true;
254        }
255    
256        protected ArrayList<String> getRoles(DirContext context, String dn, String username, ArrayList<String> currentRoles) throws NamingException {
257            ArrayList<String> list = currentRoles;
258            if (list == null) {
259                list = new ArrayList<String>();
260            }
261            if (roleName == null || "".equals(roleName)) {
262                return list;
263            }
264            String filter = roleSearchMatchingFormat.format(new String[] {
265                doRFC2254Encoding(dn), username
266            });
267    
268            SearchControls constraints = new SearchControls();
269            if (roleSearchSubtreeBool) {
270                constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
271            } else {
272                constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
273            }
274            NamingEnumeration results = context.search(roleBase, filter, constraints);
275            while (results.hasMore()) {
276                SearchResult result = (SearchResult)results.next();
277                Attributes attrs = result.getAttributes();
278                if (attrs == null) {
279                    continue;
280                }
281                list = addAttributeValues(roleName, attrs, list);
282            }
283            return list;
284    
285        }
286    
287        protected String doRFC2254Encoding(String inputString) {
288            StringBuffer buf = new StringBuffer(inputString.length());
289            for (int i = 0; i < inputString.length(); i++) {
290                char c = inputString.charAt(i);
291                switch (c) {
292                case '\\':
293                    buf.append("\\5c");
294                    break;
295                case '*':
296                    buf.append("\\2a");
297                    break;
298                case '(':
299                    buf.append("\\28");
300                    break;
301                case ')':
302                    buf.append("\\29");
303                    break;
304                case '\0':
305                    buf.append("\\00");
306                    break;
307                default:
308                    buf.append(c);
309                    break;
310                }
311            }
312            return buf.toString();
313        }
314    
315        protected boolean bindUser(DirContext context, String dn, String password) throws NamingException {
316            boolean isValid = false;
317    
318            context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn);
319            context.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
320            try {
321                context.getAttributes("", null);
322                isValid = true;
323            } catch (AuthenticationException e) {
324                isValid = false;
325                log.debug("Authentication failed for dn=" + dn);
326            }
327    
328            if (connectionUsername != null) {
329                context.addToEnvironment(Context.SECURITY_PRINCIPAL, connectionUsername);
330            } else {
331                context.removeFromEnvironment(Context.SECURITY_PRINCIPAL);
332            }
333    
334            if (connectionPassword != null) {
335                context.addToEnvironment(Context.SECURITY_CREDENTIALS, connectionPassword);
336            } else {
337                context.removeFromEnvironment(Context.SECURITY_CREDENTIALS);
338            }
339    
340            return isValid;
341        }
342    
343        private ArrayList<String> addAttributeValues(String attrId, Attributes attrs, ArrayList<String> values) throws NamingException {
344    
345            if (attrId == null || attrs == null) {
346                return values;
347            }
348            if (values == null) {
349                values = new ArrayList<String>();
350            }
351            Attribute attr = attrs.get(attrId);
352            if (attr == null) {
353                return values;
354            }
355            NamingEnumeration e = attr.getAll();
356            while (e.hasMore()) {
357                String value = (String)e.next();
358                values.add(value);
359            }
360            return values;
361        }
362    
363        protected DirContext open() throws NamingException {
364            if (context != null) {
365                return context;
366            }
367    
368            try {
369                Hashtable<String, String> env = new Hashtable<String, String>();
370                env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory);
371                if (connectionUsername != null || !"".equals(connectionUsername)) {
372                    env.put(Context.SECURITY_PRINCIPAL, connectionUsername);
373                }
374                if (connectionPassword != null || !"".equals(connectionPassword)) {
375                    env.put(Context.SECURITY_CREDENTIALS, connectionPassword);
376                }
377                env.put(Context.SECURITY_PROTOCOL, connectionProtocol);
378                env.put(Context.PROVIDER_URL, connectionURL);
379                env.put(Context.SECURITY_AUTHENTICATION, authentication);
380                context = new InitialDirContext(env);
381    
382            } catch (NamingException e) {
383                log.error(e);
384                throw e;
385            }
386            return context;
387        }
388    
389    }