1
package org.mortbay.jetty.plus.jaas.ldap;
3
// ========================================================================
4
// Copyright 2007 Mort Bay Consulting Pty. Ltd.
5
// ------------------------------------------------------------------------
6
// Licensed under the Apache License, Version 2.0 (the "License");
7
// you may not use this file except in compliance with the License.
8
// You may obtain a copy of the License at
9
// http://www.apache.org/licenses/LICENSE-2.0
10
// Unless required by applicable law or agreed to in writing, software
11
// distributed under the License is distributed on an "AS IS" BASIS,
12
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
// See the License for the specific language governing permissions and
14
// limitations under the License.
15
// ========================================================================
17
import java.io.IOException;
18
import java.util.ArrayList;
19
import java.util.Hashtable;
20
import java.util.List;
22
import java.util.Properties;
24
import javax.naming.Context;
25
import javax.naming.NamingEnumeration;
26
import javax.naming.NamingException;
27
import javax.naming.directory.Attribute;
28
import javax.naming.directory.Attributes;
29
import javax.naming.directory.DirContext;
30
import javax.naming.directory.InitialDirContext;
31
import javax.naming.directory.SearchControls;
32
import javax.naming.directory.SearchResult;
33
import javax.security.auth.Subject;
34
import javax.security.auth.callback.Callback;
35
import javax.security.auth.callback.CallbackHandler;
36
import javax.security.auth.callback.NameCallback;
37
import javax.security.auth.callback.UnsupportedCallbackException;
38
import javax.security.auth.login.LoginException;
40
import org.mortbay.jetty.plus.jaas.callback.ObjectCallback;
41
import org.mortbay.jetty.plus.jaas.spi.AbstractLoginModule;
42
import org.mortbay.jetty.plus.jaas.spi.UserInfo;
43
import org.mortbay.jetty.security.Credential;
44
import org.mortbay.log.Log;
48
* A LdapLoginModule for use with JAAS setups
50
* The jvm should be started with the following parameter:
53
* -Djava.security.auth.login.config=etc/ldap-loginModule.conf
56
* and an example of the ldap-loginModule.conf would be:
60
* org.mortbay.jetty.plus.jaas.spi.LdapLoginModule required
62
* contextFactory="com.sun.jndi.ldap.LdapCtxFactory"
63
* hostname="ldap.example.com"
65
* bindDn="cn=Directory Manager"
66
* bindPassword="directory"
67
* authenticationMethod="simple"
68
* forceBindingLogin="false"
69
* userBaseDn="ou=people,dc=alcatel"
70
* userRdnAttribute="uid"
71
* userIdAttribute="uid"
72
* userPasswordAttribute="userPassword"
73
* userObjectClass="inetOrgPerson"
74
* roleBaseDn="ou=groups,dc=example,dc=com"
75
* roleNameAttribute="cn"
76
* roleMemberAttribute="uniqueMember"
77
* roleObjectClass="groupOfUniqueNames";
81
* @author Jesse McConnell <jesse@codehaus.org>
82
* @author Frederic Nizery <frederic.nizery@alcatel-lucent.fr>
83
* @author Trygve Laugstol <trygvis@codehaus.org>
85
public class LdapLoginModule extends AbstractLoginModule
88
* hostname of the ldap server
90
private String _hostname;
93
* port of the ldap server
98
* Context.SECURITY_AUTHENTICATION
100
private String _authenticationMethod;
103
* Context.INITIAL_CONTEXT_FACTORY
105
private String _contextFactory;
108
* root DN used to connect to
110
private String _bindDn;
113
* password used to connect to the root ldap context
115
private String _bindPassword;
118
* object class of a user
120
private String _userObjectClass = "inetOrgPerson";
123
* attribute that the principal is located
125
private String _userRdnAttribute = "uid";
128
* attribute that the principal is located
130
private String _userIdAttribute = "cn";
133
* name of the attribute that a users password is stored under
135
* NOTE: not always accessible, see force binding login
137
private String _userPasswordAttribute = "userPassword";
140
* base DN where users are to be searched from
142
private String _userBaseDn;
145
* base DN where role membership is to be searched from
147
private String _roleBaseDn;
150
* object class of roles
152
private String _roleObjectClass = "groupOfUniqueNames";
155
* name of the attribute that a username would be under a role class
157
private String _roleMemberAttribute = "uniqueMember";
160
* the name of the attribute that a role would be stored under
162
private String _roleNameAttribute = "roleName";
164
private boolean _debug;
167
* if the getUserInfo can pull a password off of the user then
168
* password comparison is an option for authn, to force binding
169
* login checks, set this to true
171
private boolean _forceBindingLogin = false;
173
private DirContext _rootContext;
176
* get the available information about the user
178
* for this LoginModule, the credential can be null which will result in a
179
* binding ldap authentication scenario
181
* roles are also an optional concept if required
187
public UserInfo getUserInfo(String username) throws Exception
189
String pwdCredential = getUserCredentials(username);
191
if (pwdCredential == null)
196
pwdCredential = convertCredentialLdapToJetty(pwdCredential);
198
//String md5Credential = Credential.MD5.digest("foo");
199
//byte[] ba = digestMD5("foo");
200
//System.out.println(md5Credential + " " + ba );
201
Credential credential = Credential.getCredential(pwdCredential);
202
List roles = getUserRoles(_rootContext, username);
204
return new UserInfo(username, credential, roles);
207
protected String doRFC2254Encoding(String inputString)
209
StringBuffer buf = new StringBuffer(inputString.length());
210
for (int i = 0; i < inputString.length(); i++)
212
char c = inputString.charAt(i);
235
return buf.toString();
239
* attempts to get the users credentials from the users context
241
* NOTE: this is not an user authenticated operation
245
* @throws LoginException
247
private String getUserCredentials(String username) throws LoginException
249
String ldapCredential = null;
251
SearchControls ctls = new SearchControls();
252
ctls.setCountLimit(1);
253
ctls.setDerefLinkFlag(true);
254
ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
256
String filter = "(&(objectClass={0})({1}={2}))";
258
Log.debug("Searching for users with filter: \'" + filter + "\'" + " from base dn: " + _userBaseDn);
262
Object[] filterArguments = {_userObjectClass, _userIdAttribute, username};
263
NamingEnumeration results = _rootContext.search(_userBaseDn, filter, filterArguments, ctls);
265
Log.debug("Found user?: " + results.hasMoreElements());
267
if (!results.hasMoreElements())
269
throw new LoginException("User not found.");
272
SearchResult result = findUser(username);
274
Attributes attributes = result.getAttributes();
276
Attribute attribute = attributes.get(_userPasswordAttribute);
277
if (attribute != null)
281
byte[] value = (byte[]) attribute.get();
283
ldapCredential = new String(value);
285
catch (NamingException e)
287
Log.debug("no password available under attribute: " + _userPasswordAttribute);
291
catch (NamingException e)
293
throw new LoginException("Root context binding failure.");
296
Log.debug("user cred is: " + ldapCredential);
298
return ldapCredential;
302
* attempts to get the users roles from the root context
304
* NOTE: this is not an user authenticated operation
309
* @throws LoginException
311
private List getUserRoles(DirContext dirContext, String username) throws LoginException, NamingException
313
String userDn = _userRdnAttribute + "=" + username + "," + _userBaseDn;
315
return getUserRolesByDn(dirContext, userDn);
318
private List getUserRolesByDn(DirContext dirContext, String userDn) throws LoginException, NamingException
320
ArrayList roleList = new ArrayList();
322
if (dirContext == null || _roleBaseDn == null || _roleMemberAttribute == null || _roleObjectClass == null)
327
SearchControls ctls = new SearchControls();
328
ctls.setDerefLinkFlag(true);
329
ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
331
String filter = "(&(objectClass={0})({1}={2}))";
332
Object[] filterArguments = {_roleObjectClass, _roleMemberAttribute, userDn};
333
NamingEnumeration results = dirContext.search(_roleBaseDn, filter, filterArguments, ctls);
335
Log.debug("Found user roles?: " + results.hasMoreElements());
337
while (results.hasMoreElements())
339
SearchResult result = (SearchResult)results.nextElement();
341
Attributes attributes = result.getAttributes();
343
if (attributes == null)
348
Attribute roleAttribute = attributes.get(_roleNameAttribute);
350
if (roleAttribute == null)
355
NamingEnumeration roles = roleAttribute.getAll();
356
while (roles.hasMore())
358
roleList.add(roles.next());
366
* since ldap uses a context bind for valid authentication checking, we override login()
368
* if credentials are not available from the users context or if we are forcing the binding check
369
* then we try a binding authentication check, otherwise if we have the users encoded password then
370
* we can try authentication via that mechanic
373
* @throws LoginException
375
public boolean login() throws LoginException
379
if (getCallbackHandler() == null)
381
throw new LoginException("No callback handler");
384
Callback[] callbacks = configureCallbacks();
385
getCallbackHandler().handle(callbacks);
387
String webUserName = ((NameCallback) callbacks[0]).getName();
388
Object webCredential = ((ObjectCallback) callbacks[1]).getObject();
390
if (webUserName == null || webCredential == null)
392
setAuthenticated(false);
393
return isAuthenticated();
396
if (_forceBindingLogin)
398
return bindingLogin(webUserName, webCredential);
401
// This sets read and the credential
402
UserInfo userInfo = getUserInfo(webUserName);
404
if( userInfo == null) {
405
setAuthenticated(false);
409
setCurrentUser(new JAASUserInfo(userInfo));
411
if (webCredential instanceof String)
413
return credentialLogin(Credential.getCredential((String) webCredential));
416
return credentialLogin(webCredential);
418
catch (UnsupportedCallbackException e)
420
throw new LoginException("Error obtaining callback information.");
422
catch (IOException e)
428
throw new LoginException("IO Error performing login.");
436
throw new LoginException("Error obtaining user info.");
441
* password supplied authentication check
443
* @param webCredential
445
* @throws LoginException
447
protected boolean credentialLogin(Object webCredential) throws LoginException
449
setAuthenticated(getCurrentUser().checkCredential(webCredential));
450
return isAuthenticated();
454
* binding authentication check
455
* This methode of authentication works only if the user branch of the DIT (ldap tree)
456
* has an ACI (acces control instruction) that allow the access to any user or at least
457
* for the user that logs in.
462
* @throws LoginException
464
protected boolean bindingLogin(String username, Object password) throws LoginException, NamingException
466
SearchResult searchResult = findUser(username);
468
DirContext usrsContext = (DirContext)_rootContext.lookup(_userBaseDn);
469
DirContext usrContext = (DirContext)usrsContext.lookup(searchResult.getName());
470
String userDn = usrContext.getNameInNamespace();
472
Log.info("Attempting authentication: " + userDn);
474
Hashtable environment = getEnvironment();
475
environment.put(Context.SECURITY_PRINCIPAL, userDn);
476
environment.put(Context.SECURITY_CREDENTIALS, password);
478
DirContext dirContext = new InitialDirContext(environment);
480
List roles = getUserRolesByDn(dirContext, userDn);
482
UserInfo userInfo = new UserInfo(username, null, roles);
484
setCurrentUser(new JAASUserInfo(userInfo));
486
setAuthenticated(true);
491
private SearchResult findUser(String username) throws NamingException, LoginException
493
SearchControls ctls = new SearchControls();
494
ctls.setCountLimit(1);
495
ctls.setDerefLinkFlag(true);
496
ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
498
String filter = "(&(objectClass={0})({1}={2}))";
500
Log.info("Searching for users with filter: \'" + filter + "\'" + " from base dn: " + _userBaseDn);
502
Object[] filterArguments = new Object[]{
507
NamingEnumeration results = _rootContext.search(_userBaseDn, filter, filterArguments, ctls);
509
Log.info("Found user?: " + results.hasMoreElements());
511
if (!results.hasMoreElements())
513
throw new LoginException("User not found.");
516
return (SearchResult)results.nextElement();
519
public void initialize(Subject subject,
520
CallbackHandler callbackHandler,
524
super.initialize(subject, callbackHandler, sharedState, options);
526
_hostname = (String) options.get("hostname");
527
_port = Integer.parseInt((String) options.get("port"));
528
_contextFactory = (String) options.get("contextFactory");
529
_bindDn = (String) options.get("bindDn");
530
_bindPassword = (String) options.get("bindPassword");
531
_authenticationMethod = (String) options.get("authenticationMethod");
533
_userBaseDn = (String) options.get("userBaseDn");
535
_roleBaseDn = (String) options.get("roleBaseDn");
537
if (options.containsKey("forceBindingLogin"))
539
_forceBindingLogin = Boolean.valueOf((String) options.get("forceBindingLogin")).booleanValue();
542
_userObjectClass = getOption(options, "userObjectClass", _userObjectClass);
543
_userRdnAttribute = getOption(options, "userRdnAttribute", _userRdnAttribute);
544
_userIdAttribute = getOption(options, "userIdAttribute", _userIdAttribute);
545
_userPasswordAttribute = getOption(options, "userPasswordAttribute", _userPasswordAttribute);
546
_roleObjectClass = getOption(options, "roleObjectClass", _roleObjectClass);
547
_roleMemberAttribute = getOption(options, "roleMemberAttribute", _roleMemberAttribute);
548
_roleNameAttribute = getOption(options, "roleNameAttribute", _roleNameAttribute);
549
_debug = Boolean.valueOf(String.valueOf(getOption(options, "debug", Boolean.toString(_debug)))).booleanValue();
553
_rootContext = new InitialDirContext(getEnvironment());
555
catch (NamingException ex)
557
throw new RuntimeException("Unable to establish root context", ex);
561
public boolean commit() throws LoginException
565
_rootContext.close();
567
catch (NamingException e)
569
throw new LoginException("error closing root context: " + e.getMessage());
572
return super.commit();
575
public boolean abort() throws LoginException
579
_rootContext.close();
581
catch (NamingException e)
583
throw new LoginException("error closing root context: " + e.getMessage());
586
return super.abort();
589
private String getOption(Map options, String key, String defaultValue)
591
Object value = options.get(key);
597
return (String) value;
601
* get the context for connection
605
public Hashtable getEnvironment()
607
Properties env = new Properties();
609
env.put(Context.INITIAL_CONTEXT_FACTORY, _contextFactory);
611
if (_hostname != null)
615
env.put(Context.PROVIDER_URL, "ldap://" + _hostname + ":" + _port + "/");
619
env.put(Context.PROVIDER_URL, "ldap://" + _hostname + "/");
623
if (_authenticationMethod != null)
625
env.put(Context.SECURITY_AUTHENTICATION, _authenticationMethod);
630
env.put(Context.SECURITY_PRINCIPAL, _bindDn);
633
if (_bindPassword != null)
635
env.put(Context.SECURITY_CREDENTIALS, _bindPassword);
641
public static String convertCredentialJettyToLdap( String encryptedPassword )
643
if ("MD5:".startsWith(encryptedPassword.toUpperCase()))
645
return "{MD5}" + encryptedPassword.substring("MD5:".length(), encryptedPassword.length());
648
if ("CRYPT:".startsWith(encryptedPassword.toUpperCase()))
650
return "{CRYPT}" + encryptedPassword.substring("CRYPT:".length(), encryptedPassword.length());
653
return encryptedPassword;
656
public static String convertCredentialLdapToJetty( String encryptedPassword )
658
if (encryptedPassword == null)
660
return encryptedPassword;
663
if ("{MD5}".startsWith(encryptedPassword.toUpperCase()))
665
return "MD5:" + encryptedPassword.substring("{MD5}".length(), encryptedPassword.length());
668
if ("{CRYPT}".startsWith(encryptedPassword.toUpperCase()))
670
return "CRYPT:" + encryptedPassword.substring("{CRYPT}".length(), encryptedPassword.length());
673
return encryptedPassword;