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