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 }