1 /*
2 * JBoss, the OpenSource WebOS
3 *
4 * Distributable under LGPL license.
5 * See terms of license at gnu.org.
6 */
7 package org.jboss.security.auth.spi;
8
9 import java.security.acl.Group;
10 import java.util.Iterator;
11 import java.util.Map.Entry;
12 import java.util.Properties;
13 import javax.naming.Context;
14 import javax.naming.NamingEnumeration;
15 import javax.naming.NamingException;
16 import javax.naming.directory.Attribute;
17 import javax.naming.directory.Attributes;
18 import javax.naming.directory.BasicAttributes;
19 import javax.naming.directory.SearchResult;
20 import javax.naming.ldap.InitialLdapContext;
21 import javax.security.auth.login.LoginException;
22
23 import org.jboss.security.SimpleGroup;
24 import org.jboss.security.SimplePrincipal;
25 import org.jboss.security.auth.spi.UsernamePasswordLoginModule;
26
27 /**
28 * An implementation of LoginModule that authenticates against an LDAP server
29 * using JNDI, based on the configuration properties.
30 * <p>
31 * The LoginModule options include whatever options your LDAP JNDI provider
32 * supports. Examples of standard property names are:
33 * <ul>
34 * <li><code>Context.INITIAL_CONTEXT_FACTORY = "java.naming.factory.initial"</code>
35 * <li><code>Context.SECURITY_PROTOCOL = "java.naming.security.protocol"</code>
36 * <li><code>Context.PROVIDER_URL = "java.naming.provider.url"</code>
37 * <li><code>Context.SECURITY_AUTHENTICATION = "java.naming.security.authentication"</code>
38 * </ul>
39 * <p>
40 * The Context.SECURITY_PRINCIPAL is set to the distinguished name of the user
41 * as obtained by the callback handler and the Context.SECURITY_CREDENTIALS
42 * property is either set to the String password or Object credential depending
43 * on the useObjectCredential option.
44 * <p>
45 * Additional module properties include:
46 * <ul>
47 * <li>principalDNPrefix, principalDNSuffix : A prefix and suffix to add to the
48 * username when forming the user distiguished name. This is useful if you
49 * prompt a user for a username and you don't want them to have to enter the
50 * fully distinguished name. Using this property and principalDNSuffix the
51 * userDN will be formed as:
52 * <pre>
53 * String userDN = principalDNPrefix + username + principalDNSuffix;
54 * </pre>
55 * <li>useObjectCredential : indicates that the credential should be obtained as
56 * an opaque Object using the <code>org.jboss.security.plugins.ObjectCallback</code> type
57 * of Callback rather than as a char[] password using a JAAS PasswordCallback.
58 * <li>rolesCtxDN : The fixed distinguished name to the context to search for user roles.
59 * <li>userRolesCtxDNAttributeName : The name of an attribute in the user
60 * object that contains the distinguished name to the context to search for
61 * user roles. This differs from rolesCtxDN in that the context to search for a
62 * user's roles can be unique for each user.
63 * <li>roleAttributeName : The name of the attribute that contains the user roles
64 * <li>uidAttributeName : The name of the attribute that in the object containing
65 * the user roles that corresponds to the userid. This is used to locate the
66 * user roles.
67 * <li>matchOnUserDN : A flag indicating if the search for user roles should match
68 * on the user's fully distinguished name. If false just the username is used
69 * as the match value. If true, the userDN is used as the match value.
70 * <li>allowEmptyPasswords : A flag indicating if empty(length==0) passwords
71 * should be passed to the ldap server. An empty password is treated as an
72 * anonymous login by some ldap servers and this may not be a desirable
73 * feature. Set this to false to reject empty passwords, true to have the ldap
74 * server validate the empty password. The default is true.
75 *
76 * <li>roleAttributeIsDN : A flag indicating whether the user's role attribute
77 * contains the fully distinguished name of a role object, or the users's role
78 * attribute contains the role name. If false, the role name is taken from the
79 * value of the user's role attribute. If true, the role attribute represents
80 * the distinguished name of a role object. The role name is taken from the
81 * value of the `roleNameAttributeId` attribute of the corresponding object. In
82 * certain directory schemas (e.g., Microsoft Active Directory), role (group)
83 * attributes in the user object are stored as DNs to role objects instead of
84 * as simple names, in which case, this property should be set to true.
85 * The default value of this property is false.
86 * <li>roleNameAttributeID : The name of the attribute of the role object which
87 * corresponds to the name of the role. If the `roleAttributeIsDN` property is
88 * set to true, this property is used to find the role object's name attribute.
89 * If the `roleAttributeIsDN` property is set to false, this property is ignored.
90 * </ul>
91 * A sample login config:
92 * <p>
93 <pre>
94 testLdap {
95 org.jboss.security.auth.spi.LdapLoginModule required
96 java.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory
97 java.naming.provider.url="ldap://ldaphost.jboss.org:1389/"
98 java.naming.security.authentication=simple
99 principalDNPrefix=uid=
100 uidAttributeID=userid
101 roleAttributeID=roleName
102 principalDNSuffix=,ou=People,o=jboss.org
103 rolesCtxDN=cn=JBossSX Tests,ou=Roles,o=jboss.org
104 };
105
106 testLdap2 {
107 org.jboss.security.auth.spi.LdapLoginModule required
108 java.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory
109 java.naming.provider.url="ldap://ldaphost.jboss.org:1389/"
110 java.naming.security.authentication=simple
111 principalDNPrefix=uid=
112 uidAttributeID=userid
113 roleAttributeID=roleName
114 principalDNSuffix=,ou=People,o=jboss.org
115 userRolesCtxDNAttributeName=ou=Roles,dc=user1,dc=com
116 };
117
118 testLdapToActiveDirectory {
119 org.jboss.security.auth.spi.LdapLoginModule required
120 java.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory
121 java.naming.provider.url="ldap://ldaphost.jboss.org:1389/"
122 java.naming.security.authentication=simple
123 rolesCtxDN=cn=Users,dc=ldaphost,dc=jboss,dc=org
124 uidAttributeID=userPrincipalName
125 roleAttributeID=memberOf
126 roleAttributeIsDN=true
127 roleNameAttributeID=name
128 };
129 </pre>
130 *
131 * @author Scott.Stark@jboss.org
132 * @version $Revision: 1.7.4.5 $
133 */
134 public class LdapLoginModule extends UsernamePasswordLoginModule
135 {
136 private static final String USE_OBJECT_CREDENTIAL_OPT = "useObjectCredential";
137 private static final String PRINCIPAL_DN_PREFIX_OPT = "principalDNPrefix";
138 private static final String PRINCIPAL_DN_SUFFIX_OPT = "principalDNSuffix";
139 private static final String ROLES_CTX_DN_OPT = "rolesCtxDN";
140 private static final String USER_ROLES_CTX_DN_ATTRIBUTE_ID_OPT =
141 "userRolesCtxDNAttributeName";
142 private static final String UID_ATTRIBUTE_ID_OPT = "uidAttributeID";
143 private static final String ROLE_ATTRIBUTE_ID_OPT = "roleAttributeID";
144 private static final String MATCH_ON_USER_DN_OPT = "matchOnUserDN";
145 private static final String ROLE_ATTRIBUTE_IS_DN_OPT = "roleAttributeIsDN";
146 private static final String ROLE_NAME_ATTRIBUTE_ID_OPT = "roleNameAttributeID";
147
148 public LdapLoginModule()
149 {
150 }
151
152 private transient SimpleGroup userRoles = new SimpleGroup("Roles");
153
154 /** Overriden to return an empty password string as typically one cannot
155 obtain a user's password. We also override the validatePassword so
156 this is ok.
157 @return and empty password String
158 */
159 protected String getUsersPassword() throws LoginException
160 {
161 return "";
162 }
163 /** Overriden by subclasses to return the Groups that correspond to the
164 to the role sets assigned to the user. Subclasses should create at
165 least a Group named "Roles" that contains the roles assigned to the user.
166 A second common group is "CallerPrincipal" that provides the application
167 identity of the user rather than the security domain identity.
168 @return Group[] containing the sets of roles
169 */
170 protected Group[] getRoleSets() throws LoginException
171 {
172 Group[] roleSets = {userRoles};
173 return roleSets;
174 }
175
176 /** Validate the inputPassword by creating a ldap InitialContext with the
177 SECURITY_CREDENTIALS set to the password.
178
179 @param inputPassword the password to validate.
180 @param expectedPassword ignored
181 */
182 protected boolean validatePassword(String inputPassword, String expectedPassword)
183 {
184 boolean isValid = false;
185 if( inputPassword != null )
186 {
187 // See if this is an empty password that should be disallowed
188 if( inputPassword.length() == 0 )
189 {
190 // Check for an allowEmptyPasswords option
191 boolean allowEmptyPasswords = true;
192 String flag = (String) options.get("allowEmptyPasswords");
193 if( flag != null )
194 allowEmptyPasswords = Boolean.valueOf(flag).booleanValue();
195 if( allowEmptyPasswords == false )
196 {
197 super.log.trace("Rejecting empty password due to allowEmptyPasswords");
198 return false;
199 }
200 }
201
202 try
203 {
204 // Validate the password by trying to create an initial context
205 String username = getUsername();
206 createLdapInitContext(username, inputPassword);
207 isValid = true;
208 }
209 catch(NamingException e)
210 {
211 super.log.debug("Failed to validate password", e);
212 }
213 }
214 return isValid;
215 }
216
217 private void createLdapInitContext(String username, Object credential) throws NamingException
218 {
219 Properties env = new Properties();
220 // Map all option into the JNDI InitialLdapContext env
221 Iterator iter = options.entrySet().iterator();
222 while( iter.hasNext() )
223 {
224 Entry entry = (Entry) iter.next();
225 env.put(entry.getKey(), entry.getValue());
226 }
227
228 // Set defaults for key values if they are missing
229 String factoryName = env.getProperty(Context.INITIAL_CONTEXT_FACTORY);
230 if( factoryName == null )
231 {
232 factoryName = "com.sun.jndi.ldap.LdapCtxFactory";
233 env.setProperty(Context.INITIAL_CONTEXT_FACTORY, factoryName);
234 }
235 String authType = env.getProperty(Context.SECURITY_AUTHENTICATION);
236 if( authType == null )
237 env.setProperty(Context.SECURITY_AUTHENTICATION, "simple");
238 String protocol = env.getProperty(Context.SECURITY_PROTOCOL);
239 String providerURL = (String) options.get(Context.PROVIDER_URL);
240 if( providerURL == null )
241 providerURL = "ldap://localhost:" + ((protocol != null && protocol.equals("ssl")) ? "636" : "389");
242
243 String principalDNPrefix = (String) options.get(PRINCIPAL_DN_PREFIX_OPT);
244 if( principalDNPrefix == null )
245 principalDNPrefix="";
246 String principalDNSuffix = (String) options.get(PRINCIPAL_DN_SUFFIX_OPT);
247 if( principalDNSuffix == null )
248 principalDNSuffix="";
249 String matchType = (String) options.get(MATCH_ON_USER_DN_OPT);
250 boolean matchOnUserDN = Boolean.valueOf(matchType).booleanValue();
251 String userDN = principalDNPrefix + username + principalDNSuffix;
252 env.setProperty(Context.PROVIDER_URL, providerURL);
253 env.setProperty(Context.SECURITY_PRINCIPAL, userDN);
254 env.put(Context.SECURITY_CREDENTIALS, credential);
255 super.log.trace("Logging into LDAP server, env="+env);
256 InitialLdapContext ctx = new InitialLdapContext(env, null);
257 super.log.trace("Logged into LDAP server, "+ctx);
258 /* If a userRolesCtxDNAttributeName was speocified, see if there is a
259 user specific roles DN. If there is not, the default rolesCtxDN will
260 be used.
261 */
262 String rolesCtxDN = (String) options.get(ROLES_CTX_DN_OPT);
263 String userRolesCtxDNAttributeName = (String) options.get(USER_ROLES_CTX_DN_ATTRIBUTE_ID_OPT);
264 if( userRolesCtxDNAttributeName != null )
265 {
266 // Query the indicated attribute for the roles ctx DN to use
267 String[] returnAttribute = {userRolesCtxDNAttributeName};
268 try
269 {
270 Attributes result = ctx.getAttributes(userDN, returnAttribute);
271 if (result.get(userRolesCtxDNAttributeName) != null)
272 {
273 rolesCtxDN = result.get(userRolesCtxDNAttributeName).get().toString();
274 super.log.trace("Found user roles context DN: " + rolesCtxDN);
275 }
276 }
277 catch(NamingException e)
278 {
279 super.log.debug("Failed to query userRolesCtxDNAttributeName", e);
280 }
281 }
282
283 // Search for any roles associated with the user
284 if( rolesCtxDN != null )
285 {
286 String uidAttrName = (String) options.get(UID_ATTRIBUTE_ID_OPT);
287 if( uidAttrName == null )
288 uidAttrName = "uid";
289 String roleAttrName = (String) options.get(ROLE_ATTRIBUTE_ID_OPT);
290 if( roleAttrName == null )
291 roleAttrName = "roles";
292 BasicAttributes matchAttrs = new BasicAttributes(true);
293 if( matchOnUserDN == true )
294 matchAttrs.put(uidAttrName, userDN);
295 else
296 matchAttrs.put(uidAttrName, username);
297 String[] roleAttr = {roleAttrName};
298 // Is user's role attribute a DN or the role name
299 String roleAttributeIsDNOption = (String) options.get(ROLE_ATTRIBUTE_IS_DN_OPT);
300 boolean roleAttributeIsDN = Boolean.valueOf(roleAttributeIsDNOption).booleanValue();
301
302 // If user's role attribute is a DN, what is the role's name attribute
303 // Default to 'name' (Group name attribute in Active Directory)
304 String roleNameAttributeID = (String) options.get(ROLE_NAME_ATTRIBUTE_ID_OPT);
305 if( roleNameAttributeID == null )
306 roleNameAttributeID = "name";
307
308 try
309 {
310 NamingEnumeration answer = ctx.search(rolesCtxDN, matchAttrs, roleAttr);
311 while( answer.hasMore() )
312 {
313 SearchResult sr = (SearchResult) answer.next();
314 Attributes attrs = sr.getAttributes();
315 Attribute roles = attrs.get(roleAttrName);
316 for(int r = 0; r < roles.size(); r ++)
317 {
318 Object value = roles.get(r);
319 String roleName = null;
320 if( roleAttributeIsDN == true )
321 {
322 // Query the roleDN location for the value of roleNameAttributeID
323 String roleDN = value.toString();
324 String[] returnAttribute = {roleNameAttributeID};
325 super.log.trace("Using roleDN: " + roleDN);
326 try
327 {
328 Attributes result = ctx.getAttributes(roleDN, returnAttribute);
329 if( result.get(roleNameAttributeID) != null )
330 {
331 roleName = result.get(roleNameAttributeID).get().toString();
332 }
333 }
334 catch(NamingException e)
335 {
336 super.log.trace("Failed to query roleNameAttrName", e);
337 }
338 }
339 else
340 {
341 // The role attribute value is the role name
342 roleName = value.toString();
343 }
344
345 if( roleName != null )
346 {
347 super.log.trace("Assign user to role " + roleName);
348 userRoles.addMember(new SimplePrincipal(roleName));
349 }
350 }
351 }
352 }
353 catch(NamingException e)
354 {
355 log.trace("Failed to locate roles", e);
356 }
357 }
358 // Close the context to release the connection
359 ctx.close();
360 }
361 }