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 */
017package org.apache.activemq.jaas;
018
019import java.io.IOException;
020import java.security.Principal;
021import java.text.MessageFormat;
022import java.util.ArrayList;
023import java.util.HashSet;
024import java.util.Hashtable;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028
029import javax.naming.AuthenticationException;
030import javax.naming.CommunicationException;
031import javax.naming.Context;
032import javax.naming.Name;
033import javax.naming.NameParser;
034import javax.naming.NamingEnumeration;
035import javax.naming.NamingException;
036import javax.naming.directory.Attribute;
037import javax.naming.directory.Attributes;
038import javax.naming.directory.DirContext;
039import javax.naming.directory.InitialDirContext;
040import javax.naming.directory.SearchControls;
041import javax.naming.directory.SearchResult;
042import javax.security.auth.Subject;
043import javax.security.auth.callback.Callback;
044import javax.security.auth.callback.CallbackHandler;
045import javax.security.auth.callback.NameCallback;
046import javax.security.auth.callback.PasswordCallback;
047import javax.security.auth.callback.UnsupportedCallbackException;
048import javax.security.auth.login.FailedLoginException;
049import javax.security.auth.login.LoginException;
050import javax.security.auth.spi.LoginModule;
051
052import org.slf4j.Logger;
053import org.slf4j.LoggerFactory;
054
055/**
056 * @version $Rev: $ $Date: $
057 */
058public 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}