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.net.URI;
021import java.net.URISyntaxException;
022import java.security.Principal;
023import java.text.MessageFormat;
024import java.util.*;
025
026import javax.naming.*;
027import javax.naming.directory.Attribute;
028import javax.naming.directory.Attributes;
029import javax.naming.directory.DirContext;
030import javax.naming.directory.InitialDirContext;
031import javax.naming.directory.SearchControls;
032import javax.naming.directory.SearchResult;
033import javax.security.auth.Subject;
034import javax.security.auth.callback.Callback;
035import javax.security.auth.callback.CallbackHandler;
036import javax.security.auth.callback.NameCallback;
037import javax.security.auth.callback.PasswordCallback;
038import javax.security.auth.callback.UnsupportedCallbackException;
039import javax.security.auth.login.FailedLoginException;
040import javax.security.auth.login.LoginException;
041import javax.security.auth.spi.LoginModule;
042
043import org.slf4j.Logger;
044import org.slf4j.LoggerFactory;
045
046/**
047 * @version $Rev: $ $Date: $
048 */
049public class LDAPLoginModule implements LoginModule {
050
051    private static final String INITIAL_CONTEXT_FACTORY = "initialContextFactory";
052    private static final String CONNECTION_URL = "connectionURL";
053    private static final String CONNECTION_USERNAME = "connectionUsername";
054    private static final String CONNECTION_PASSWORD = "connectionPassword";
055    private static final String CONNECTION_PROTOCOL = "connectionProtocol";
056    private static final String AUTHENTICATION = "authentication";
057    private static final String USER_BASE = "userBase";
058    private static final String USER_SEARCH_MATCHING = "userSearchMatching";
059    private static final String USER_SEARCH_SUBTREE = "userSearchSubtree";
060    private static final String ROLE_BASE = "roleBase";
061    private static final String ROLE_NAME = "roleName";
062    private static final String ROLE_SEARCH_MATCHING = "roleSearchMatching";
063    private static final String ROLE_SEARCH_SUBTREE = "roleSearchSubtree";
064    private static final String USER_ROLE_NAME = "userRoleName";
065    private static final String EXPAND_ROLES = "expandRoles";
066    private static final String EXPAND_ROLES_MATCHING = "expandRolesMatching";
067
068    private static Logger log = LoggerFactory.getLogger(LDAPLoginModule.class);
069
070    protected DirContext context;
071
072    private Subject subject;
073    private CallbackHandler handler;
074    private LDAPLoginProperty [] config;
075    private Principal user;
076    private Set<GroupPrincipal> groups = new HashSet<>();
077
078    /** the authentication status*/
079    private boolean succeeded = false;
080    private boolean commitSucceeded = false;
081
082    @Override
083    public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
084        this.subject = subject;
085        this.handler = callbackHandler;
086
087        config = new LDAPLoginProperty [] {
088                        new LDAPLoginProperty (INITIAL_CONTEXT_FACTORY, (String)options.get(INITIAL_CONTEXT_FACTORY)),
089                        new LDAPLoginProperty (CONNECTION_URL, (String)options.get(CONNECTION_URL)),
090                        new LDAPLoginProperty (CONNECTION_USERNAME, (String)options.get(CONNECTION_USERNAME)),
091                        new LDAPLoginProperty (CONNECTION_PASSWORD, (String)options.get(CONNECTION_PASSWORD)),
092                        new LDAPLoginProperty (CONNECTION_PROTOCOL, (String)options.get(CONNECTION_PROTOCOL)),
093                        new LDAPLoginProperty (AUTHENTICATION, (String)options.get(AUTHENTICATION)),
094                        new LDAPLoginProperty (USER_BASE, (String)options.get(USER_BASE)),
095                        new LDAPLoginProperty (USER_SEARCH_MATCHING, (String)options.get(USER_SEARCH_MATCHING)),
096                        new LDAPLoginProperty (USER_SEARCH_SUBTREE, (String)options.get(USER_SEARCH_SUBTREE)),
097                        new LDAPLoginProperty (ROLE_BASE, (String)options.get(ROLE_BASE)),
098                        new LDAPLoginProperty (ROLE_NAME, (String)options.get(ROLE_NAME)),
099                        new LDAPLoginProperty (ROLE_SEARCH_MATCHING, (String)options.get(ROLE_SEARCH_MATCHING)),
100                        new LDAPLoginProperty (ROLE_SEARCH_SUBTREE, (String)options.get(ROLE_SEARCH_SUBTREE)),
101                        new LDAPLoginProperty (USER_ROLE_NAME, (String)options.get(USER_ROLE_NAME)),
102                new LDAPLoginProperty (EXPAND_ROLES, (String) options.get(EXPAND_ROLES)),
103                new LDAPLoginProperty (EXPAND_ROLES_MATCHING, (String) options.get(EXPAND_ROLES_MATCHING)),
104
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 | UnsupportedCallbackException ioe) {
118            throw (LoginException)new LoginException().initCause(ioe);
119        }
120
121        String password;
122
123        String username = ((NameCallback)callbacks[0]).getName();
124        if (username == null)
125                return false;
126
127        if (((PasswordCallback)callbacks[1]).getPassword() != null)
128                password = new String(((PasswordCallback)callbacks[1]).getPassword());
129        else
130                password="";
131
132        // authenticate will throw LoginException
133        // in case of failed authentication
134        authenticate(username, password);
135
136        user = new UserPrincipal(username);
137        succeeded = true;
138        return true;
139    }
140
141    @Override
142    public boolean logout() throws LoginException {
143        subject.getPrincipals().remove(user);
144        subject.getPrincipals().removeAll(groups);
145
146        user = null;
147        groups.clear();
148
149        succeeded = false;
150        commitSucceeded = false;
151        return true;
152    }
153
154    @Override
155    public boolean commit() throws LoginException {
156        if (!succeeded) {
157            user = null;
158            groups.clear();
159            return false;
160        }
161
162        Set<Principal> principals = subject.getPrincipals();
163        principals.add(user);
164        principals.addAll(groups);
165
166        commitSucceeded = true;
167        return true;
168    }
169
170    @Override
171    public boolean abort() throws LoginException {
172        if (!succeeded) {
173            return false;
174        } else if (commitSucceeded) {
175            // we succeeded, but another required module failed
176            logout();
177        } else {
178            // our commit failed
179            user = null;
180            groups.clear();
181            succeeded = false;
182        }
183        return true;
184    }
185
186    protected void closeContext() {
187        if (context == null) {
188            return;
189        }
190        try {
191            context.close();
192        } catch (Exception e) {
193            log.error(e.toString());
194        } finally {
195            context = null;
196        }
197    }
198
199    protected boolean authenticate(String username, String password) throws LoginException {
200
201        MessageFormat userSearchMatchingFormat;
202        boolean userSearchSubtreeBool;
203
204        if (log.isDebugEnabled()) {
205            log.debug("Create the LDAP initial context.");
206        }
207        try {
208            openContext();
209        } catch (NamingException ne) {
210            FailedLoginException ex = new FailedLoginException("Error opening LDAP connection");
211            ex.initCause(ne);
212            throw ex;
213        }
214
215        if (!isLoginPropertySet(USER_SEARCH_MATCHING))
216                return false;
217
218        userSearchMatchingFormat = new MessageFormat(getLDAPPropertyValue(USER_SEARCH_MATCHING));
219        userSearchSubtreeBool = Boolean.valueOf(getLDAPPropertyValue(USER_SEARCH_SUBTREE));
220
221        try {
222
223            String filter = userSearchMatchingFormat.format(new String[] {
224                doRFC2254Encoding(username)
225            });
226            SearchControls constraints = new SearchControls();
227            if (userSearchSubtreeBool) {
228                constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
229            } else {
230                constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
231            }
232
233            // setup attributes
234            List<String> list = new ArrayList<>();
235            if (isLoginPropertySet(USER_ROLE_NAME)) {
236                list.add(getLDAPPropertyValue(USER_ROLE_NAME));
237            }
238            String[] attribs = new String[list.size()];
239            list.toArray(attribs);
240            constraints.setReturningAttributes(attribs);
241
242            if (log.isDebugEnabled()) {
243                log.debug("Get the user DN.");
244                log.debug("Looking for the user in LDAP with ");
245                log.debug("  base DN: " + getLDAPPropertyValue(USER_BASE));
246                log.debug("  filter: " + filter);
247            }
248
249            NamingEnumeration<SearchResult> results = context.search(getLDAPPropertyValue(USER_BASE), filter, constraints);
250
251            if (results == null || !results.hasMore()) {
252                log.warn("User " + username + " not found in LDAP.");
253                throw new FailedLoginException("User " + username + " not found in LDAP.");
254            }
255
256            SearchResult result = results.next();
257
258            if (results.hasMore()) {
259                // ignore for now
260            }
261
262            String dn;
263            if (result.isRelative()) {
264                log.debug("LDAP returned a relative name: {}", result.getName());
265
266                NameParser parser = context.getNameParser("");
267                Name contextName = parser.parse(context.getNameInNamespace());
268                Name baseName = parser.parse(getLDAPPropertyValue(USER_BASE));
269                Name entryName = parser.parse(result.getName());
270                Name name = contextName.addAll(baseName);
271                name = name.addAll(entryName);
272                dn = name.toString();
273            } else {
274                log.debug("LDAP returned an absolute name: {}", result.getName());
275
276                try {
277                    URI uri = new URI(result.getName());
278                    String path = uri.getPath();
279
280                    if (path.startsWith("/")) {
281                        dn = path.substring(1);
282                    } else {
283                        dn = path;
284                    }
285                } catch (URISyntaxException e) {
286                    closeContext();
287                    FailedLoginException ex = new FailedLoginException("Error parsing absolute name as URI.");
288                    ex.initCause(e);
289                    throw ex;
290                }
291            }
292
293            if (log.isDebugEnabled()) {
294                log.debug("Using DN [" + dn + "] for binding.");
295            }
296
297            Attributes attrs = result.getAttributes();
298            if (attrs == null) {
299                throw new FailedLoginException("User found, but LDAP entry malformed: " + username);
300            }
301            List<String> roles = null;
302            if (isLoginPropertySet(USER_ROLE_NAME)) {
303                roles = addAttributeValues(getLDAPPropertyValue(USER_ROLE_NAME), attrs, roles);
304            }
305
306            // check the credentials by binding to server
307            if (bindUser(context, dn, password)) {
308                // if authenticated add more roles
309                roles = getRoles(context, dn, username, roles);
310                if (log.isDebugEnabled()) {
311                    log.debug("Roles " + roles + " for user " + username);
312                }
313                for (int i = 0; i < roles.size(); i++) {
314                    groups.add(new GroupPrincipal(roles.get(i)));
315                }
316            } else {
317                throw new FailedLoginException("Password does not match for user: " + username);
318            }
319        } catch (CommunicationException e) {
320            FailedLoginException ex = new FailedLoginException("Error contacting LDAP");
321            ex.initCause(e);
322            throw ex;
323        } catch (NamingException e) {
324            FailedLoginException ex = new FailedLoginException("Error contacting LDAP");
325            ex.initCause(e);
326            throw ex;
327        } finally {
328            closeContext();
329        }
330        return true;
331    }
332
333    protected List<String> getRoles(DirContext context, String dn, String username, List<String> currentRoles) throws NamingException {
334        List<String> list = currentRoles;
335        MessageFormat roleSearchMatchingFormat;
336        boolean roleSearchSubtreeBool;
337        boolean expandRolesBool;
338        roleSearchMatchingFormat = new MessageFormat(getLDAPPropertyValue(ROLE_SEARCH_MATCHING));
339        roleSearchSubtreeBool = Boolean.parseBoolean(getLDAPPropertyValue(ROLE_SEARCH_SUBTREE));
340        expandRolesBool = Boolean.parseBoolean(getLDAPPropertyValue(EXPAND_ROLES));
341
342        if (list == null) {
343            list = new ArrayList<>();
344        }
345        if (!isLoginPropertySet(ROLE_NAME)) {
346            return list;
347        }
348        String filter = roleSearchMatchingFormat.format(new String[] {
349            doRFC2254Encoding(dn), doRFC2254Encoding(username)
350        });
351
352        SearchControls constraints = new SearchControls();
353        if (roleSearchSubtreeBool) {
354            constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
355        } else {
356            constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
357        }
358        if (log.isDebugEnabled()) {
359            log.debug("Get user roles.");
360            log.debug("Looking for the user roles in LDAP with ");
361            log.debug("  base DN: " + getLDAPPropertyValue(ROLE_BASE));
362            log.debug("  filter: " + filter);
363        }
364        HashSet<String> haveSeenNames = new HashSet<>();
365        Queue<String> pendingNameExpansion = new LinkedList<>();
366        NamingEnumeration<SearchResult> results = context.search(getLDAPPropertyValue(ROLE_BASE), filter, constraints);
367        while (results.hasMore()) {
368            SearchResult result = results.next();
369            Attributes attrs = result.getAttributes();
370            if (expandRolesBool) {
371                haveSeenNames.add(result.getNameInNamespace());
372                pendingNameExpansion.add(result.getNameInNamespace());
373            }
374            if (attrs == null) {
375                continue;
376            }
377            list = addAttributeValues(getLDAPPropertyValue(ROLE_NAME), attrs, list);
378        }
379        if (expandRolesBool) {
380            MessageFormat expandRolesMatchingFormat = new MessageFormat(getLDAPPropertyValue(EXPAND_ROLES_MATCHING));
381            while (!pendingNameExpansion.isEmpty()) {
382                String name = pendingNameExpansion.remove();
383                filter = expandRolesMatchingFormat.format(new String[]{name});
384                results = context.search(getLDAPPropertyValue(ROLE_BASE), filter, constraints);
385                while (results.hasMore()) {
386                    SearchResult result = results.next();
387                    name = result.getNameInNamespace();
388                    if (!haveSeenNames.contains(name)) {
389                        Attributes attrs = result.getAttributes();
390                        list = addAttributeValues(getLDAPPropertyValue(ROLE_NAME), attrs, list);
391                        haveSeenNames.add(name);
392                        pendingNameExpansion.add(name);
393                    }
394                }
395            }
396        }
397        return list;
398    }
399
400    protected String doRFC2254Encoding(String inputString) {
401        StringBuffer buf = new StringBuffer(inputString.length());
402        for (int i = 0; i < inputString.length(); i++) {
403            char c = inputString.charAt(i);
404            switch (c) {
405            case '\\':
406                buf.append("\\5c");
407                break;
408            case '*':
409                buf.append("\\2a");
410                break;
411            case '(':
412                buf.append("\\28");
413                break;
414            case ')':
415                buf.append("\\29");
416                break;
417            case '\0':
418                buf.append("\\00");
419                break;
420            default:
421                buf.append(c);
422                break;
423            }
424        }
425        return buf.toString();
426    }
427
428    protected boolean bindUser(DirContext context, String dn, String password) throws NamingException {
429        boolean isValid = false;
430        if (log.isDebugEnabled()) {
431            log.debug("Binding the user.");
432        }
433        context.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple");
434        context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn);
435        context.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
436        try {
437            context.getAttributes("", null);
438            isValid = true;
439            if (log.isDebugEnabled()) {
440                log.debug("User " + dn + " successfully bound.");
441            }
442        } catch (AuthenticationException e) {
443            if (log.isDebugEnabled()) {
444                log.debug("Authentication failed for dn=" + dn);
445            }
446        }
447
448        if (isLoginPropertySet(CONNECTION_USERNAME)) {
449            context.addToEnvironment(Context.SECURITY_PRINCIPAL, getLDAPPropertyValue(CONNECTION_USERNAME));
450        } else {
451            context.removeFromEnvironment(Context.SECURITY_PRINCIPAL);
452        }
453        if (isLoginPropertySet(CONNECTION_PASSWORD)) {
454            context.addToEnvironment(Context.SECURITY_CREDENTIALS, getLDAPPropertyValue(CONNECTION_PASSWORD));
455        } else {
456            context.removeFromEnvironment(Context.SECURITY_CREDENTIALS);
457        }
458        context.addToEnvironment(Context.SECURITY_AUTHENTICATION, getLDAPPropertyValue(AUTHENTICATION));
459        return isValid;
460    }
461
462    private List<String> addAttributeValues(String attrId, Attributes attrs, List<String> values) throws NamingException {
463
464        if (attrId == null || attrs == null) {
465            return values;
466        }
467        if (values == null) {
468            values = new ArrayList<>();
469        }
470        Attribute attr = attrs.get(attrId);
471        if (attr == null) {
472            return values;
473        }
474        NamingEnumeration<?> e = attr.getAll();
475        while (e.hasMore()) {
476            String value = (String)e.next();
477            values.add(value);
478        }
479        return values;
480    }
481
482    protected void openContext() throws NamingException {
483        if (context != null) {
484            return;
485        }
486        try {
487            Hashtable<String, String> env = new Hashtable<>();
488            env.put(Context.INITIAL_CONTEXT_FACTORY, getLDAPPropertyValue(INITIAL_CONTEXT_FACTORY));
489            if (isLoginPropertySet(CONNECTION_USERNAME)) {
490                env.put(Context.SECURITY_PRINCIPAL, getLDAPPropertyValue(CONNECTION_USERNAME));
491            } else {
492                throw new NamingException("Empty username is not allowed");
493            }
494
495            if (isLoginPropertySet(CONNECTION_PASSWORD)) {
496                env.put(Context.SECURITY_CREDENTIALS, getLDAPPropertyValue(CONNECTION_PASSWORD));
497            } else {
498                throw new NamingException("Empty password is not allowed");
499            }
500            env.put(Context.SECURITY_PROTOCOL, getLDAPPropertyValue(CONNECTION_PROTOCOL));
501            env.put(Context.PROVIDER_URL, getLDAPPropertyValue(CONNECTION_URL));
502            env.put(Context.SECURITY_AUTHENTICATION, getLDAPPropertyValue(AUTHENTICATION));
503            context = new InitialDirContext(env);
504
505        } catch (NamingException e) {
506            closeContext();
507            log.error(e.toString());
508            throw e;
509        }
510    }
511
512    private String getLDAPPropertyValue (String propertyName){
513        for (LDAPLoginProperty ldapLoginProperty : config)
514            if (ldapLoginProperty.getPropertyName().equals(propertyName))
515                return ldapLoginProperty.getPropertyValue();
516        return null;
517    }
518
519    private boolean isLoginPropertySet(String propertyName) {
520        for (LDAPLoginProperty ldapLoginProperty : config) {
521            if (ldapLoginProperty.getPropertyName().equals(propertyName) && (ldapLoginProperty.getPropertyValue() != null && !"".equals(ldapLoginProperty.getPropertyValue())))
522                return true;
523        }
524        return false;
525    }
526
527}