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 doRFC2254Encoding(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), doRFC2254Encoding(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 } else {
412 throw new NamingException("Empty username is not allowed");
413 }
414
415 if (isLoginPropertySet(CONNECTION_PASSWORD)) {
416 env.put(Context.SECURITY_CREDENTIALS, getLDAPPropertyValue(CONNECTION_PASSWORD));
417 } else {
418 throw new NamingException("Empty password is not allowed");
419 }
420 env.put(Context.SECURITY_PROTOCOL, getLDAPPropertyValue(CONNECTION_PROTOCOL));
421 env.put(Context.PROVIDER_URL, getLDAPPropertyValue(CONNECTION_URL));
422 env.put(Context.SECURITY_AUTHENTICATION, getLDAPPropertyValue(AUTHENTICATION));
423 context = new InitialDirContext(env);
424
425 } catch (NamingException e) {
426 log.error(e.toString());
427 throw e;
428 }
429 return context;
430 }
431
432 private String getLDAPPropertyValue (String propertyName){
433 for (int i=0; i < config.length; i++ )
434 if (config[i].getPropertyName() == propertyName)
435 return config[i].getPropertyValue();
436 return null;
437 }
438
439 private boolean isLoginPropertySet(String propertyName) {
440 for (int i=0; i < config.length; i++ ) {
441 if (config[i].getPropertyName() == propertyName && (config[i].getPropertyValue() != null && !"".equals(config[i].getPropertyValue())))
442 return true;
443 }
444 return false;
445 }
446
447 }