1 /**
2 *
3 * Copyright 2003-2004 The Apache Software Foundation
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.geronimo.security.realm.providers;
19
20 import java.io.IOException;
21 import java.text.MessageFormat;
22 import java.util.ArrayList;
23 import java.util.HashSet;
24 import java.util.Hashtable;
25 import java.util.Iterator;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Set;
29
30 import javax.naming.AuthenticationException;
31 import javax.naming.Context;
32 import javax.naming.Name;
33 import javax.naming.NameParser;
34 import javax.naming.NamingEnumeration;
35 import javax.naming.NamingException;
36 import javax.naming.directory.Attribute;
37 import javax.naming.directory.Attributes;
38 import javax.naming.directory.DirContext;
39 import javax.naming.directory.InitialDirContext;
40 import javax.naming.CommunicationException;
41 import javax.naming.directory.SearchControls;
42 import javax.naming.directory.SearchResult;
43 import javax.security.auth.Subject;
44 import javax.security.auth.callback.Callback;
45 import javax.security.auth.callback.CallbackHandler;
46 import javax.security.auth.callback.NameCallback;
47 import javax.security.auth.callback.PasswordCallback;
48 import javax.security.auth.callback.UnsupportedCallbackException;
49 import javax.security.auth.login.LoginException;
50 import javax.security.auth.login.FailedLoginException;
51 import javax.security.auth.spi.LoginModule;
52
53 import org.apache.commons.logging.Log;
54 import org.apache.commons.logging.LogFactory;
55
56 import org.apache.geronimo.security.realm.providers.GeronimoGroupPrincipal;
57 import org.apache.geronimo.security.realm.providers.GeronimoUserPrincipal;
58
59
60 public class LDAPLoginModule implements LoginModule {
61
62 private static Log log = LogFactory.getLog(LDAPLoginModule.class);
63
64 private Subject subject;
65 private CallbackHandler handler;
66
67 private static final String INITIAL_CONTEXT_FACTORY = "initialContextFactory";
68 private static final String CONNECTION_URL = "connectionURL";
69 private static final String CONNECTION_USERNAME = "connectionUsername";
70 private static final String CONNECTION_PASSWORD = "connectionPassword";
71 private static final String CONNECTION_PROTOCOL = "connectionProtocol";
72 private static final String AUTHENTICATION = "authentication";
73 private static final String USER_BASE = "userBase";
74 private static final String USER_SEARCH_MATCHING = "userSearchMatching";
75 private static final String USER_SEARCH_SUBTREE = "userSearchSubtree";
76 private static final String ROLE_BASE = "roleBase";
77 private static final String ROLE_NAME = "roleName";
78 private static final String ROLE_SEARCH_MATCHING = "roleSearchMatching";
79 private static final String ROLE_SEARCH_SUBTREE = "roleSearchSubtree";
80 private static final String USER_ROLE_NAME = "userRoleName";
81
82 private String initialContextFactory;
83 private String connectionURL;
84 private String connectionUsername;
85 private String connectionPassword;
86 private String connectionProtocol;
87 private String authentication;
88 private String userBase;
89 private String userSearchMatching;
90 private String userPassword;
91 private String roleBase;
92 private String roleName;
93 private String roleSearchMatching;
94 private String userSearchSubtree;
95 private String roleSearchSubtree;
96 private String userRoleName;
97
98 private String cbUsername;
99 private String cbPassword;
100
101 protected DirContext context = null;
102
103 private MessageFormat userSearchMatchingFormat;
104 private MessageFormat roleSearchMatchingFormat;
105
106 private boolean userSearchSubtreeBool = false;
107 private boolean roleSearchSubtreeBool = false;
108
109 Set groups = new HashSet();
110
111 public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
112 this.subject = subject;
113 this.handler = callbackHandler;
114 initialContextFactory = (String) options.get(INITIAL_CONTEXT_FACTORY);
115 connectionURL = (String) options.get(CONNECTION_URL);
116 connectionUsername = (String) options.get(CONNECTION_USERNAME);
117 connectionPassword = (String) options.get(CONNECTION_PASSWORD);
118 connectionProtocol = (String) options.get(CONNECTION_PROTOCOL);
119 authentication = (String) options.get(AUTHENTICATION);
120 userBase = (String) options.get(USER_BASE);
121 userSearchMatching = (String) options.get(USER_SEARCH_MATCHING);
122 userSearchSubtree = (String) options.get(USER_SEARCH_SUBTREE);
123 roleBase = (String) options.get(ROLE_BASE);
124 roleName = (String) options.get(ROLE_NAME);
125 roleSearchMatching = (String) options.get(ROLE_SEARCH_MATCHING);
126 roleSearchSubtree = (String) options.get(ROLE_SEARCH_SUBTREE);
127 userRoleName = (String) options.get(USER_ROLE_NAME);
128 userSearchMatchingFormat = new MessageFormat(userSearchMatching);
129 roleSearchMatchingFormat = new MessageFormat(roleSearchMatching);
130 userSearchSubtreeBool = new Boolean(userSearchSubtree).booleanValue();
131 roleSearchSubtreeBool = new Boolean(roleSearchSubtree).booleanValue();
132 }
133
134 public boolean login() throws LoginException {
135 Callback[] callbacks = new Callback[2];
136
137 callbacks[0] = new NameCallback("User name");
138 callbacks[1] = new PasswordCallback("Password", false);
139 try {
140 handler.handle(callbacks);
141 } catch (IOException ioe) {
142 throw (LoginException) new LoginException().initCause(ioe);
143 } catch (UnsupportedCallbackException uce) {
144 throw (LoginException) new LoginException().initCause(uce);
145 }
146 cbUsername = ((NameCallback) callbacks[0]).getName();
147 cbPassword = new String(((PasswordCallback) callbacks[1]).getPassword());
148
149 if (cbUsername == null || "".equals(cbUsername)
150 || cbPassword == null || "".equals(cbPassword)) {
151 return false;
152 }
153
154 try {
155 boolean result = authenticate(cbUsername, cbPassword);
156 if(!result) {
157 throw new FailedLoginException();
158 } else {
159 return true;
160 }
161 } catch (Exception e) {
162 throw (LoginException) new LoginException("LDAP Error").initCause(e);
163 }
164 }
165
166 public boolean logout() throws LoginException {
167 cbUsername = null;
168 cbPassword = null;
169
170 return true;
171 }
172
173 public boolean commit() throws LoginException {
174 Set principals = subject.getPrincipals();
175 principals.add(new GeronimoUserPrincipal(cbUsername));
176 Iterator iter = groups.iterator();
177 while (iter.hasNext()) {
178 principals.add(iter.next());
179 }
180 return true;
181 }
182
183 public boolean abort() throws LoginException {
184 cbUsername = null;
185 cbPassword = null;
186 return true;
187 }
188
189 protected void close(DirContext context) {
190 try {
191 context.close();
192 } catch (Exception e) {
193 log.error(e);
194 }
195 }
196
197 protected boolean authenticate(String username, String password) throws Exception {
198
199 DirContext context = null;
200 context = open();
201
202 try {
203
204 String filter = userSearchMatchingFormat.format(new String[]{username});
205 SearchControls constraints = new SearchControls();
206 if (userSearchSubtreeBool) {
207 constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
208 } else {
209 constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
210 }
211
212
213 ArrayList list = new ArrayList();
214 if (userRoleName != null) {
215 list.add(userRoleName);
216 }
217 String[] attribs = new String[list.size()];
218 list.toArray(attribs);
219 constraints.setReturningAttributes(attribs);
220
221
222 NamingEnumeration results = context.search(userBase, filter, constraints);
223
224 if (results == null || !results.hasMore()) {
225 return false;
226 }
227
228 SearchResult result = (SearchResult) results.next();
229
230 if (results.hasMore()) {
231
232 }
233 NameParser parser = context.getNameParser("");
234 Name contextName = parser.parse(context.getNameInNamespace());
235 Name baseName = parser.parse(userBase);
236 Name entryName = parser.parse(result.getName());
237 Name name = contextName.addAll(baseName);
238 name = name.addAll(entryName);
239 String dn = name.toString();
240
241 Attributes attrs = result.getAttributes();
242 if (attrs == null) {
243 return false;
244 }
245 ArrayList roles = null;
246 if (userRoleName != null) {
247 roles = addAttributeValues(userRoleName, attrs, roles);
248 }
249
250
251 if (bindUser(context, dn, password)) {
252
253 roles = getRoles(context, dn, username, roles);
254 for (int i = 0; i < roles.size(); i++) {
255 groups.add(new GeronimoGroupPrincipal((String) roles.get(i)));
256 }
257 } else {
258 return false;
259 }
260 } catch (CommunicationException e) {
261
262 } catch (NamingException e) {
263 if (context != null) {
264 close(context);
265 }
266 return false;
267 }
268
269
270 return true;
271 }
272
273 protected ArrayList getRoles(DirContext context, String dn, String username, ArrayList currentRoles) throws NamingException {
274 ArrayList list = currentRoles;
275 if (list == null) {
276 list = new ArrayList();
277 }
278 if (roleName == null || "".equals(roleName)) {
279 return list;
280 }
281 String filter = roleSearchMatchingFormat.format(new String[]{doRFC2254Encoding(dn), username});
282
283 SearchControls constraints = new SearchControls();
284 if (roleSearchSubtreeBool) {
285 constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
286 } else {
287 constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
288 }
289 NamingEnumeration results =
290 context.search(roleBase, filter, constraints);
291 while (results.hasMore()) {
292 SearchResult result = (SearchResult) results.next();
293 Attributes attrs = result.getAttributes();
294 if (attrs == null) {
295 continue;
296 }
297 list = addAttributeValues(roleName, attrs, list);
298 }
299 return list;
300
301 }
302
303
304 protected String doRFC2254Encoding(String inputString) {
305 StringBuffer buf = new StringBuffer(inputString.length());
306 for (int i = 0; i < inputString.length(); i++) {
307 char c = inputString.charAt(i);
308 switch (c) {
309 case '\\':
310 buf.append("\\5c");
311 break;
312 case '*':
313 buf.append("\\2a");
314 break;
315 case '(':
316 buf.append("\\28");
317 break;
318 case ')':
319 buf.append("\\29");
320 break;
321 case '\0':
322 buf.append("\\00");
323 break;
324 default:
325 buf.append(c);
326 break;
327 }
328 }
329 return buf.toString();
330 }
331
332 protected boolean bindUser(DirContext context, String dn, String password) throws NamingException {
333 boolean isValid = false;
334 Attributes attr;
335
336 context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn);
337 context.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
338 try {
339 attr = context.getAttributes("", null);
340 isValid = true;
341 } catch (AuthenticationException e) {
342 isValid = false;
343 log.debug("Authentication failed for dn=" + dn);
344 }
345
346 if (connectionUsername != null) {
347 context.addToEnvironment(Context.SECURITY_PRINCIPAL,
348 connectionUsername);
349 } else {
350 context.removeFromEnvironment(Context.SECURITY_PRINCIPAL);
351 }
352
353 if (connectionPassword != null) {
354 context.addToEnvironment(Context.SECURITY_CREDENTIALS,
355 connectionPassword);
356 } else {
357 context.removeFromEnvironment(Context.SECURITY_CREDENTIALS);
358 }
359
360 return isValid;
361 }
362
363 private String getAttributeValue(String attrId, Attributes attrs)
364 throws NamingException {
365
366 if (attrId == null || attrs == null) {
367 return null;
368 }
369
370 Attribute attr = attrs.get(attrId);
371 if (attr == null) {
372 return (null);
373 }
374 Object value = attr.get();
375 if (value == null) {
376 return (null);
377 }
378 String valueString = null;
379 if (value instanceof byte[]) {
380 valueString = new String((byte[]) value);
381 } else {
382 valueString = value.toString();
383 }
384 return valueString;
385 }
386
387 private ArrayList addAttributeValues(String attrId, Attributes attrs, ArrayList values)
388 throws NamingException {
389
390 if (attrId == null || attrs == null) {
391 return values;
392 }
393 if (values == null) {
394 values = new ArrayList();
395 }
396 Attribute attr = attrs.get(attrId);
397 if (attr == null) {
398 return (values);
399 }
400 NamingEnumeration e = attr.getAll();
401 while (e.hasMore()) {
402 String value = (String) e.next();
403 values.add(value);
404 }
405 return values;
406 }
407
408 protected DirContext open() throws NamingException {
409 if (context != null) {
410 return context;
411 }
412
413 try {
414 Hashtable env = new Hashtable();
415 env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory);
416 if (connectionUsername != null || !"".equals(connectionUsername)) {
417 env.put(Context.SECURITY_PRINCIPAL, connectionUsername);
418 }
419 if (connectionPassword != null || !"".equals(connectionPassword)) {
420 env.put(Context.SECURITY_CREDENTIALS, connectionPassword);
421 }
422 env.put(Context.SECURITY_PROTOCOL, connectionProtocol);
423 env.put(Context.PROVIDER_URL, connectionURL);
424 env.put(Context.SECURITY_AUTHENTICATION, authentication);
425 context = new InitialDirContext(env);
426
427 } catch (NamingException e) {
428 log.error(e);
429 throw e;
430 }
431 return context;
432 }
433
434 }