1 /*
2  * Copyright (c) 2005, 2013, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package com.sun.security.auth.module;
27 
28 import java.security.AccessController;
29 import java.net.SocketPermission;
30 import java.security.Principal;
31 import java.security.PrivilegedAction;
32 import java.util.Arrays;
33 import java.util.Hashtable;
34 import java.util.Map;
35 import java.util.ResourceBundle;
36 import java.util.regex.Matcher;
37 import java.util.regex.Pattern;
38 import java.util.Set;
39 
40 import javax.naming.*;
41 import javax.naming.directory.*;
42 import javax.naming.ldap.*;
43 import javax.security.auth.*;
44 import javax.security.auth.callback.*;
45 import javax.security.auth.login.*;
46 import javax.security.auth.spi.*;
47 
48 import com.sun.security.auth.LdapPrincipal;
49 import com.sun.security.auth.UserPrincipal;
50 
51 
52 /**
53  * This {@link LoginModule} performs LDAP-based authentication.
54  * A username and password is verified against the corresponding user
55  * credentials stored in an LDAP directory.
56  * This module requires the supplied {@link CallbackHandler} to support a
57  * {@link NameCallback} and a {@link PasswordCallback}.
58  * If authentication is successful then a new {@link LdapPrincipal} is created
59  * using the user's distinguished name and a new {@link UserPrincipal} is
60  * created using the user's username and both are associated
61  * with the current {@link Subject}.
62  *
63  * <p> This module operates in one of three modes: <i>search-first</i>,
64  * <i>authentication-first</i> or <i>authentication-only</i>.
65  * A mode is selected by specifying a particular set of options.
66  *
67  * <p> In search-first mode, the LDAP directory is searched to determine the
68  * user's distinguished name and then authentication is attempted.
69  * An (anonymous) search is performed using the supplied username in
70  * conjunction with a specified search filter.
71  * If successful then authentication is attempted using the user's
72  * distinguished name and the supplied password.
73  * To enable this mode, set the <code>userFilter</code> option and omit the
74  * <code>authIdentity</code> option.
75  * Use search-first mode when the user's distinguished name is not
76  * known in advance.
77  *
78  * <p> In authentication-first mode, authentication is attempted using the
79  * supplied username and password and then the LDAP directory is searched.
80  * If authentication is successful then a search is performed using the
81  * supplied username in conjunction with a specified search filter.
82  * To enable this mode, set the <code>authIdentity</code> and the
83  * <code>userFilter</code> options.
84  * Use authentication-first mode when accessing an LDAP directory
85  * that has been configured to disallow anonymous searches.
86  *
87  * <p> In authentication-only mode, authentication is attempted using the
88  * supplied username and password. The LDAP directory is not searched because
89  * the user's distinguished name is already known.
90  * To enable this mode, set the <code>authIdentity</code> option to a valid
91  * distinguished name and omit the <code>userFilter</code> option.
92  * Use authentication-only mode when the user's distinguished name is
93  * known in advance.
94  *
95  * <p> The following option is mandatory and must be specified in this
96  * module's login {@link Configuration}:
97  * <dl><dt></dt><dd>
98  * <dl>
99  * <dt> <code>userProvider=<b>ldap_urls</b></code>
100  * </dt>
101  * <dd> This option identifies the LDAP directory that stores user entries.
102  *      <b>ldap_urls</b> is a list of space-separated LDAP URLs
103  *      (<a href="http://www.ietf.org/rfc/rfc2255.txt">RFC 2255</a>)
104  *      that identifies the LDAP server to use and the position in
105  *      its directory tree where user entries are located.
106  *      When several LDAP URLs are specified then each is attempted,
107  *      in turn, until the first successful connection is established.
108  *      Spaces in the distinguished name component of the URL must be escaped
109  *      using the standard mechanism of percent character ('<code>%</code>')
110  *      followed by two hexadecimal digits (see {@link java.net.URI}).
111  *      Query components must also be omitted from the URL.
112  *
113  *      <p>
114  *      Automatic discovery of the LDAP server via DNS
115  *      (<a href="http://www.ietf.org/rfc/rfc2782.txt">RFC 2782</a>)
116  *      is supported (once DNS has been configured to support such a service).
117  *      It is enabled by omitting the hostname and port number components from
118  *      the LDAP URL. </dd>
119  * </dl></dl>
120  *
121  * <p> This module also recognizes the following optional {@link Configuration}
122  *     options:
123  * <dl><dt></dt><dd>
124  * <dl>
125  * <dt> <code>userFilter=<b>ldap_filter</b></code> </dt>
126  * <dd> This option specifies the search filter to use to locate a user's
127  *      entry in the LDAP directory. It is used to determine a user's
128  *      distinguished name.
129  *      <code><b>ldap_filter</b></code> is an LDAP filter string
130  *      (<a href="http://www.ietf.org/rfc/rfc2254.txt">RFC 2254</a>).
131  *      If it contains the special token "<code><b>{USERNAME}</b></code>"
132  *      then that token will be replaced with the supplied username value
133  *      before the filter is used to search the directory. </dd>
134  *
135  * <dt> <code>authIdentity=<b>auth_id</b></code> </dt>
136  * <dd> This option specifies the identity to use when authenticating a user
137  *      to the LDAP directory.
138  *      <code><b>auth_id</b></code> may be an LDAP distinguished name string
139  *      (<a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>) or some
140  *      other string name.
141  *      It must contain the special token "<code><b>{USERNAME}</b></code>"
142  *      which will be replaced with the supplied username value before the
143  *      name is used for authentication.
144  *      Note that if this option does not contain a distinguished name then
145  *      the <code>userFilter</code> option must also be specified. </dd>
146  *
147  * <dt> <code>authzIdentity=<b>authz_id</b></code> </dt>
148  * <dd> This option specifies an authorization identity for the user.
149  *      <code><b>authz_id</b></code> is any string name.
150  *      If it comprises a single special token with curly braces then
151  *      that token is treated as a attribute name and will be replaced with a
152  *      single value of that attribute from the user's LDAP entry.
153  *      If the attribute cannot be found then the option is ignored.
154  *      When this option is supplied and the user has been successfully
155  *      authenticated then an additional {@link UserPrincipal}
156  *      is created using the authorization identity and it is associated with
157  *      the current {@link Subject}. </dd>
158  *
159  * <dt> <code>useSSL</code> </dt>
160  * <dd> if <code>false</code>, this module does not establish an SSL connection
161  *      to the LDAP server before attempting authentication. SSL is used to
162  *      protect the privacy of the user's password because it is transmitted
163  *      in the clear over LDAP.
164  *      By default, this module uses SSL. </dd>
165  *
166  * <dt> <code>useFirstPass</code> </dt>
167  * <dd> if <code>true</code>, this module retrieves the username and password
168  *      from the module's shared state, using "javax.security.auth.login.name"
169  *      and "javax.security.auth.login.password" as the respective keys. The
170  *      retrieved values are used for authentication. If authentication fails,
171  *      no attempt for a retry is made, and the failure is reported back to
172  *      the calling application.</dd>
173  *
174  * <dt> <code>tryFirstPass</code> </dt>
175  * <dd> if <code>true</code>, this module retrieves the username and password
176  *      from the module's shared state, using "javax.security.auth.login.name"
177  *       and "javax.security.auth.login.password" as the respective keys.  The
178  *      retrieved values are used for authentication. If authentication fails,
179  *      the module uses the {@link CallbackHandler} to retrieve a new username
180  *      and password, and another attempt to authenticate is made. If the
181  *      authentication fails, the failure is reported back to the calling
182  *      application.</dd>
183  *
184  * <dt> <code>storePass</code> </dt>
185  * <dd> if <code>true</code>, this module stores the username and password
186  *      obtained from the {@link CallbackHandler} in the module's shared state,
187  *      using
188  *      "javax.security.auth.login.name" and
189  *      "javax.security.auth.login.password" as the respective keys.  This is
190  *      not performed if existing values already exist for the username and
191  *      password in the shared state, or if authentication fails.</dd>
192  *
193  * <dt> <code>clearPass</code> </dt>
194  * <dd> if <code>true</code>, this module clears the username and password
195  *      stored in the module's shared state after both phases of authentication
196  *      (login and commit) have completed.</dd>
197  *
198  * <dt> <code>debug</code> </dt>
199  * <dd> if <code>true</code>, debug messages are displayed on the standard
200  *      output stream.
201  * </dl>
202  * </dl>
203  *
204  * <p>
205  * Arbitrary
206  * <a href="{@docRoot}/../../../../../technotes/guides/jndi/jndi-ldap-gl.html#PROP">JNDI properties</a>
207  * may also be specified in the {@link Configuration}.
208  * They are added to the environment and passed to the LDAP provider.
209  * Note that the following four JNDI properties are set by this module directly
210  * and are ignored if also present in the configuration:
211  * <ul>
212  * <li> <code>java.naming.provider.url</code>
213  * <li> <code>java.naming.security.principal</code>
214  * <li> <code>java.naming.security.credentials</code>
215  * <li> <code>java.naming.security.protocol</code>
216  * </ul>
217  *
218  * <p>
219  * Three sample {@link Configuration}s are shown below.
220  * The first one activates search-first mode. It identifies the LDAP server
221  * and specifies that users' entries be located by their <code>uid</code> and
222  * <code>objectClass</code> attributes. It also specifies that an identity
223  * based on the user's <code>employeeNumber</code> attribute should be created.
224  * The second one activates authentication-first mode. It requests that the
225  * LDAP server be located dynamically, that authentication be performed using
226  * the supplied username directly but without the protection of SSL and that
227  * users' entries be located by one of three naming attributes and their
228  * <code>objectClass</code> attribute.
229  * The third one activates authentication-only mode. It identifies alternative
230  * LDAP servers, it specifies the distinguished name to use for
231  * authentication and a fixed identity to use for authorization. No directory
232  * search is performed.
233  *
234  * <pre>
235  *
236  *     ExampleApplication {
237  *         com.sun.security.auth.module.LdapLoginModule REQUIRED
238  *             userProvider="ldap://ldap-svr/ou=people,dc=example,dc=com"
239  *             userFilter="(&(uid={USERNAME})(objectClass=inetOrgPerson))"
240  *             authzIdentity="{EMPLOYEENUMBER}"
241  *             debug=true;
242  *     };
243  *
244  *     ExampleApplication {
245  *         com.sun.security.auth.module.LdapLoginModule REQUIRED
246  *             userProvider="ldap:///cn=users,dc=example,dc=com"
247  *             authIdentity="{USERNAME}"
248  *             userFilter="(&(|(samAccountName={USERNAME})(userPrincipalName={USERNAME})(cn={USERNAME}))(objectClass=user))"
249  *             useSSL=false
250  *             debug=true;
251  *     };
252  *
253  *     ExampleApplication {
254  *         com.sun.security.auth.module.LdapLoginModule REQUIRED
255  *             userProvider="ldap://ldap-svr1 ldap://ldap-svr2"
256  *             authIdentity="cn={USERNAME},ou=people,dc=example,dc=com"
257  *             authzIdentity="staff"
258  *             debug=true;
259  *     };
260  *
261  * </pre>
262  *
263  * <dl>
264  * <dt><b>Note:</b> </dt>
265  * <dd>When a {@link SecurityManager} is active then an application
266  *     that creates a {@link LoginContext} and uses a {@link LoginModule}
267  *     must be granted certain permissions.
268  *     <p>
269  *     If the application creates a login context using an <em>installed</em>
270  *     {@link Configuration} then the application must be granted the
271  *     {@link AuthPermission} to create login contexts.
272  *     For example, the following security policy allows an application in
273  *     the user's current directory to instantiate <em>any</em> login context:
274  *     <pre>
275  *
276  *     grant codebase "file:${user.dir}/" {
277  *         permission javax.security.auth.AuthPermission "createLoginContext.*";
278  *     };
279  *     </pre>
280  *
281  *     Alternatively, if the application creates a login context using a
282  *     <em>caller-specified</em> {@link Configuration} then the application
283  *     must be granted the permissions required by the {@link LoginModule}.
284  *     <em>This</em> module requires the following two permissions:
285  *     <p>
286  *     <ul>
287  *     <li> The {@link SocketPermission} to connect to an LDAP server.
288  *     <li> The {@link AuthPermission} to modify the set of {@link Principal}s
289  *          associated with a {@link Subject}.
290  *     </ul>
291  *     <p>
292  *     For example, the following security policy grants an application in the
293  *     user's current directory all the permissions required by this module:
294  *     <pre>
295  *
296  *     grant codebase "file:${user.dir}/" {
297  *         permission java.net.SocketPermission "*:389", "connect";
298  *         permission java.net.SocketPermission "*:636", "connect";
299  *         permission javax.security.auth.AuthPermission "modifyPrincipals";
300  *     };
301  *     </pre>
302  * </dd>
303  * </dl>
304  *
305  * @since 1.6
306  */
307 @jdk.Exported
308 public class LdapLoginModule implements LoginModule {
309 
310     // Use the default classloader for this class to load the prompt strings.
311     private static final ResourceBundle rb = AccessController.doPrivileged(
312             new PrivilegedAction<ResourceBundle>() {
313                 public ResourceBundle run() {
314                     return ResourceBundle.getBundle(
315                         "sun.security.util.AuthResources");
316                 }
317             }
318         );
319 
320     // Keys to retrieve the stored username and password
321     private static final String USERNAME_KEY = "javax.security.auth.login.name";
322     private static final String PASSWORD_KEY =
323         "javax.security.auth.login.password";
324 
325     // Option names
326     private static final String USER_PROVIDER = "userProvider";
327     private static final String USER_FILTER = "userFilter";
328     private static final String AUTHC_IDENTITY = "authIdentity";
329     private static final String AUTHZ_IDENTITY = "authzIdentity";
330 
331     // Used for the username token replacement
332     private static final String USERNAME_TOKEN = "{USERNAME}";
333     private static final Pattern USERNAME_PATTERN =
334         Pattern.compile("\\{USERNAME\\}");
335 
336     // Configurable options
337     private String userProvider;
338     private String userFilter;
339     private String authcIdentity;
340     private String authzIdentity;
341     private String authzIdentityAttr = null;
342     private boolean useSSL = true;
343     private boolean authFirst = false;
344     private boolean authOnly = false;
345     private boolean useFirstPass = false;
346     private boolean tryFirstPass = false;
347     private boolean storePass = false;
348     private boolean clearPass = false;
349     private boolean debug = false;
350 
351     // Authentication status
352     private boolean succeeded = false;
353     private boolean commitSucceeded = false;
354 
355     // Supplied username and password
356     private String username;
357     private char[] password;
358 
359     // User's identities
360     private LdapPrincipal ldapPrincipal;
361     private UserPrincipal userPrincipal;
362     private UserPrincipal authzPrincipal;
363 
364     // Initial state
365     private Subject subject;
366     private CallbackHandler callbackHandler;
367     private Map<String, Object> sharedState;
368     private Map<String, ?> options;
369     private LdapContext ctx;
370     private Matcher identityMatcher = null;
371     private Matcher filterMatcher = null;
372     private Hashtable<String, Object> ldapEnvironment;
373     private SearchControls constraints = null;
374 
375     /**
376      * Initialize this <code>LoginModule</code>.
377      *
378      * @param subject the <code>Subject</code> to be authenticated.
379      * @param callbackHandler a <code>CallbackHandler</code> to acquire the
380      *                  username and password.
381      * @param sharedState shared <code>LoginModule</code> state.
382      * @param options options specified in the login
383      *                  <code>Configuration</code> for this particular
384      *                  <code>LoginModule</code>.
385      */
386     // Unchecked warning from (Map<String, Object>)sharedState is safe
387     // since javax.security.auth.login.LoginContext passes a raw HashMap.
388     @SuppressWarnings("unchecked")
initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options)389     public void initialize(Subject subject, CallbackHandler callbackHandler,
390                         Map<String, ?> sharedState, Map<String, ?> options) {
391 
392         this.subject = subject;
393         this.callbackHandler = callbackHandler;
394         this.sharedState = (Map<String, Object>)sharedState;
395         this.options = options;
396 
397         ldapEnvironment = new Hashtable<String, Object>(9);
398         ldapEnvironment.put(Context.INITIAL_CONTEXT_FACTORY,
399             "com.sun.jndi.ldap.LdapCtxFactory");
400 
401         // Add any JNDI properties to the environment
402         for (String key : options.keySet()) {
403             if (key.indexOf(".") > -1) {
404                 ldapEnvironment.put(key, options.get(key));
405             }
406         }
407 
408         // initialize any configured options
409 
410         userProvider = (String)options.get(USER_PROVIDER);
411         if (userProvider != null) {
412             ldapEnvironment.put(Context.PROVIDER_URL, userProvider);
413         }
414 
415         authcIdentity = (String)options.get(AUTHC_IDENTITY);
416         if (authcIdentity != null &&
417             (authcIdentity.indexOf(USERNAME_TOKEN) != -1)) {
418             identityMatcher = USERNAME_PATTERN.matcher(authcIdentity);
419         }
420 
421         userFilter = (String)options.get(USER_FILTER);
422         if (userFilter != null) {
423             if (userFilter.indexOf(USERNAME_TOKEN) != -1) {
424                 filterMatcher = USERNAME_PATTERN.matcher(userFilter);
425             }
426             constraints = new SearchControls();
427             constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
428             constraints.setReturningAttributes(new String[0]); //return no attrs
429         }
430 
431         authzIdentity = (String)options.get(AUTHZ_IDENTITY);
432         if (authzIdentity != null &&
433             authzIdentity.startsWith("{") && authzIdentity.endsWith("}")) {
434             if (constraints != null) {
435                 authzIdentityAttr =
436                     authzIdentity.substring(1, authzIdentity.length() - 1);
437                 constraints.setReturningAttributes(
438                     new String[]{authzIdentityAttr});
439             }
440             authzIdentity = null; // set later, from the specified attribute
441         }
442 
443         // determine mode
444         if (authcIdentity != null) {
445             if (userFilter != null) {
446                 authFirst = true; // authentication-first mode
447             } else {
448                 authOnly = true; // authentication-only mode
449             }
450         }
451 
452         if ("false".equalsIgnoreCase((String)options.get("useSSL"))) {
453             useSSL = false;
454             ldapEnvironment.remove(Context.SECURITY_PROTOCOL);
455         } else {
456             ldapEnvironment.put(Context.SECURITY_PROTOCOL, "ssl");
457         }
458 
459         tryFirstPass =
460                 "true".equalsIgnoreCase((String)options.get("tryFirstPass"));
461 
462         useFirstPass =
463                 "true".equalsIgnoreCase((String)options.get("useFirstPass"));
464 
465         storePass = "true".equalsIgnoreCase((String)options.get("storePass"));
466 
467         clearPass = "true".equalsIgnoreCase((String)options.get("clearPass"));
468 
469         debug = "true".equalsIgnoreCase((String)options.get("debug"));
470 
471         if (debug) {
472             if (authFirst) {
473                 System.out.println("\t\t[LdapLoginModule] " +
474                     "authentication-first mode; " +
475                     (useSSL ? "SSL enabled" : "SSL disabled"));
476             } else if (authOnly) {
477                 System.out.println("\t\t[LdapLoginModule] " +
478                     "authentication-only mode; " +
479                     (useSSL ? "SSL enabled" : "SSL disabled"));
480             } else {
481                 System.out.println("\t\t[LdapLoginModule] " +
482                     "search-first mode; " +
483                     (useSSL ? "SSL enabled" : "SSL disabled"));
484             }
485         }
486     }
487 
488     /**
489      * Begin user authentication.
490      *
491      * <p> Acquire the user's credentials and verify them against the
492      * specified LDAP directory.
493      *
494      * @return true always, since this <code>LoginModule</code>
495      *          should not be ignored.
496      * @exception FailedLoginException if the authentication fails.
497      * @exception LoginException if this <code>LoginModule</code>
498      *          is unable to perform the authentication.
499      */
login()500     public boolean login() throws LoginException {
501 
502         if (userProvider == null) {
503             throw new LoginException
504                 ("Unable to locate the LDAP directory service");
505         }
506 
507         if (debug) {
508             System.out.println("\t\t[LdapLoginModule] user provider: " +
509                 userProvider);
510         }
511 
512         // attempt the authentication
513         if (tryFirstPass) {
514 
515             try {
516                 // attempt the authentication by getting the
517                 // username and password from shared state
518                 attemptAuthentication(true);
519 
520                 // authentication succeeded
521                 succeeded = true;
522                 if (debug) {
523                     System.out.println("\t\t[LdapLoginModule] " +
524                                 "tryFirstPass succeeded");
525                 }
526                 return true;
527 
528             } catch (LoginException le) {
529                 // authentication failed -- try again below by prompting
530                 cleanState();
531                 if (debug) {
532                     System.out.println("\t\t[LdapLoginModule] " +
533                                 "tryFirstPass failed: " + le.toString());
534                 }
535             }
536 
537         } else if (useFirstPass) {
538 
539             try {
540                 // attempt the authentication by getting the
541                 // username and password from shared state
542                 attemptAuthentication(true);
543 
544                 // authentication succeeded
545                 succeeded = true;
546                 if (debug) {
547                     System.out.println("\t\t[LdapLoginModule] " +
548                                 "useFirstPass succeeded");
549                 }
550                 return true;
551 
552             } catch (LoginException le) {
553                 // authentication failed
554                 cleanState();
555                 if (debug) {
556                     System.out.println("\t\t[LdapLoginModule] " +
557                                 "useFirstPass failed");
558                 }
559                 throw le;
560             }
561         }
562 
563         // attempt the authentication by prompting for the username and pwd
564         try {
565             attemptAuthentication(false);
566 
567             // authentication succeeded
568            succeeded = true;
569             if (debug) {
570                 System.out.println("\t\t[LdapLoginModule] " +
571                                 "authentication succeeded");
572             }
573             return true;
574 
575         } catch (LoginException le) {
576             cleanState();
577             if (debug) {
578                 System.out.println("\t\t[LdapLoginModule] " +
579                                 "authentication failed");
580             }
581             throw le;
582         }
583     }
584 
585     /**
586      * Complete user authentication.
587      *
588      * <p> This method is called if the LoginContext's
589      * overall authentication succeeded
590      * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
591      * succeeded).
592      *
593      * <p> If this LoginModule's own authentication attempt
594      * succeeded (checked by retrieving the private state saved by the
595      * <code>login</code> method), then this method associates an
596      * <code>LdapPrincipal</code> and one or more <code>UserPrincipal</code>s
597      * with the <code>Subject</code> located in the
598      * <code>LoginModule</code>.  If this LoginModule's own
599      * authentication attempted failed, then this method removes
600      * any state that was originally saved.
601      *
602      * @exception LoginException if the commit fails
603      * @return true if this LoginModule's own login and commit
604      *          attempts succeeded, or false otherwise.
605      */
commit()606     public boolean commit() throws LoginException {
607 
608         if (succeeded == false) {
609             return false;
610         } else {
611             if (subject.isReadOnly()) {
612                 cleanState();
613                 throw new LoginException ("Subject is read-only");
614             }
615             // add Principals to the Subject
616             Set<Principal> principals = subject.getPrincipals();
617             if (! principals.contains(ldapPrincipal)) {
618                 principals.add(ldapPrincipal);
619             }
620             if (debug) {
621                 System.out.println("\t\t[LdapLoginModule] " +
622                                    "added LdapPrincipal \"" +
623                                    ldapPrincipal +
624                                    "\" to Subject");
625             }
626 
627             if (! principals.contains(userPrincipal)) {
628                 principals.add(userPrincipal);
629             }
630             if (debug) {
631                 System.out.println("\t\t[LdapLoginModule] " +
632                                    "added UserPrincipal \"" +
633                                    userPrincipal +
634                                    "\" to Subject");
635             }
636 
637             if (authzPrincipal != null &&
638                 (! principals.contains(authzPrincipal))) {
639                 principals.add(authzPrincipal);
640 
641                 if (debug) {
642                     System.out.println("\t\t[LdapLoginModule] " +
643                                    "added UserPrincipal \"" +
644                                    authzPrincipal +
645                                    "\" to Subject");
646                 }
647             }
648         }
649         // in any case, clean out state
650         cleanState();
651         commitSucceeded = true;
652         return true;
653     }
654 
655     /**
656      * Abort user authentication.
657      *
658      * <p> This method is called if the overall authentication failed.
659      * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
660      * did not succeed).
661      *
662      * <p> If this LoginModule's own authentication attempt
663      * succeeded (checked by retrieving the private state saved by the
664      * <code>login</code> and <code>commit</code> methods),
665      * then this method cleans up any state that was originally saved.
666      *
667      * @exception LoginException if the abort fails.
668      * @return false if this LoginModule's own login and/or commit attempts
669      *          failed, and true otherwise.
670      */
abort()671     public boolean abort() throws LoginException {
672         if (debug)
673             System.out.println("\t\t[LdapLoginModule] " +
674                 "aborted authentication");
675 
676         if (succeeded == false) {
677             return false;
678         } else if (succeeded == true && commitSucceeded == false) {
679 
680             // Clean out state
681             succeeded = false;
682             cleanState();
683 
684             ldapPrincipal = null;
685             userPrincipal = null;
686             authzPrincipal = null;
687         } else {
688             // overall authentication succeeded and commit succeeded,
689             // but someone else's commit failed
690             logout();
691         }
692         return true;
693     }
694 
695     /**
696      * Logout a user.
697      *
698      * <p> This method removes the Principals
699      * that were added by the <code>commit</code> method.
700      *
701      * @exception LoginException if the logout fails.
702      * @return true in all cases since this <code>LoginModule</code>
703      *          should not be ignored.
704      */
logout()705     public boolean logout() throws LoginException {
706         if (subject.isReadOnly()) {
707             cleanState();
708             throw new LoginException ("Subject is read-only");
709         }
710         Set<Principal> principals = subject.getPrincipals();
711         principals.remove(ldapPrincipal);
712         principals.remove(userPrincipal);
713         if (authzIdentity != null) {
714             principals.remove(authzPrincipal);
715         }
716 
717         // clean out state
718         cleanState();
719         succeeded = false;
720         commitSucceeded = false;
721 
722         ldapPrincipal = null;
723         userPrincipal = null;
724         authzPrincipal = null;
725 
726         if (debug) {
727             System.out.println("\t\t[LdapLoginModule] logged out Subject");
728         }
729         return true;
730     }
731 
732     /**
733      * Attempt authentication
734      *
735      * @param getPasswdFromSharedState boolean that tells this method whether
736      *          to retrieve the password from the sharedState.
737      * @exception LoginException if the authentication attempt fails.
738      */
attemptAuthentication(boolean getPasswdFromSharedState)739     private void attemptAuthentication(boolean getPasswdFromSharedState)
740         throws LoginException {
741 
742         // first get the username and password
743         getUsernamePassword(getPasswdFromSharedState);
744 
745         if (password == null || password.length == 0) {
746             throw (LoginException)
747                 new FailedLoginException("No password was supplied");
748         }
749 
750         String dn = "";
751 
752         if (authFirst || authOnly) {
753 
754             String id =
755                 replaceUsernameToken(identityMatcher, authcIdentity, username);
756 
757             // Prepare to bind using user's username and password
758             ldapEnvironment.put(Context.SECURITY_CREDENTIALS, password);
759             ldapEnvironment.put(Context.SECURITY_PRINCIPAL, id);
760 
761             if (debug) {
762                 System.out.println("\t\t[LdapLoginModule] " +
763                     "attempting to authenticate user: " + username);
764             }
765 
766             try {
767                 // Connect to the LDAP server (using simple bind)
768                 ctx = new InitialLdapContext(ldapEnvironment, null);
769 
770             } catch (NamingException e) {
771                 throw (LoginException)
772                     new FailedLoginException("Cannot bind to LDAP server")
773                         .initCause(e);
774             }
775 
776             // Authentication has succeeded
777 
778             // Locate the user's distinguished name
779             if (userFilter != null) {
780                 dn = findUserDN(ctx);
781             } else {
782                 dn = id;
783             }
784 
785         } else {
786 
787             try {
788                 // Connect to the LDAP server (using anonymous bind)
789                 ctx = new InitialLdapContext(ldapEnvironment, null);
790 
791             } catch (NamingException e) {
792                 throw (LoginException)
793                     new FailedLoginException("Cannot connect to LDAP server")
794                         .initCause(e);
795             }
796 
797             // Locate the user's distinguished name
798             dn = findUserDN(ctx);
799 
800             try {
801 
802                 // Prepare to bind using user's distinguished name and password
803                 ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple");
804                 ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, dn);
805                 ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
806 
807                 if (debug) {
808                     System.out.println("\t\t[LdapLoginModule] " +
809                         "attempting to authenticate user: " + username);
810                 }
811                 // Connect to the LDAP server (using simple bind)
812                 ctx.reconnect(null);
813 
814                 // Authentication has succeeded
815 
816             } catch (NamingException e) {
817                 throw (LoginException)
818                     new FailedLoginException("Cannot bind to LDAP server")
819                         .initCause(e);
820             }
821         }
822 
823         // Save input as shared state only if authentication succeeded
824         if (storePass &&
825             !sharedState.containsKey(USERNAME_KEY) &&
826             !sharedState.containsKey(PASSWORD_KEY)) {
827             sharedState.put(USERNAME_KEY, username);
828             sharedState.put(PASSWORD_KEY, password);
829         }
830 
831         // Create the user principals
832         userPrincipal = new UserPrincipal(username);
833         if (authzIdentity != null) {
834             authzPrincipal = new UserPrincipal(authzIdentity);
835         }
836 
837         try {
838 
839             ldapPrincipal = new LdapPrincipal(dn);
840 
841         } catch (InvalidNameException e) {
842             if (debug) {
843                 System.out.println("\t\t[LdapLoginModule] " +
844                                    "cannot create LdapPrincipal: bad DN");
845             }
846             throw (LoginException)
847                 new FailedLoginException("Cannot create LdapPrincipal")
848                     .initCause(e);
849         }
850     }
851 
852     /**
853      * Search for the user's entry.
854      * Determine the distinguished name of the user's entry and optionally
855      * an authorization identity for the user.
856      *
857      * @param ctx an LDAP context to use for the search
858      * @return the user's distinguished name or an empty string if none
859      *         was found.
860      * @exception LoginException if the user's entry cannot be found.
861      */
findUserDN(LdapContext ctx)862     private String findUserDN(LdapContext ctx) throws LoginException {
863 
864         String userDN = "";
865 
866         // Locate the user's LDAP entry
867         if (userFilter != null) {
868             if (debug) {
869                 System.out.println("\t\t[LdapLoginModule] " +
870                     "searching for entry belonging to user: " + username);
871             }
872         } else {
873             if (debug) {
874                 System.out.println("\t\t[LdapLoginModule] " +
875                     "cannot search for entry belonging to user: " + username);
876             }
877             throw (LoginException)
878                 new FailedLoginException("Cannot find user's LDAP entry");
879         }
880 
881         try {
882             // Sanitize username and substitute into LDAP filter
883             String canonicalUserFilter =
884                 replaceUsernameToken(filterMatcher, userFilter,
885                     escapeUsernameChars());
886 
887             NamingEnumeration<SearchResult> results =
888                 ctx.search("", canonicalUserFilter, constraints);
889 
890             // Extract the distinguished name of the user's entry
891             // (Use the first entry if more than one is returned)
892             if (results.hasMore()) {
893                 SearchResult entry = results.next();
894                 userDN = entry.getNameInNamespace();
895 
896                 if (debug) {
897                     System.out.println("\t\t[LdapLoginModule] found entry: " +
898                         userDN);
899                 }
900 
901                 // Extract a value from user's authorization identity attribute
902                 if (authzIdentityAttr != null) {
903                     Attribute attr =
904                         entry.getAttributes().get(authzIdentityAttr);
905                     if (attr != null) {
906                         Object val = attr.get();
907                         if (val instanceof String) {
908                             authzIdentity = (String) val;
909                         }
910                     }
911                 }
912 
913                 results.close();
914 
915             } else {
916                 // Bad username
917                 if (debug) {
918                     System.out.println("\t\t[LdapLoginModule] user's entry " +
919                         "not found");
920                 }
921             }
922 
923         } catch (NamingException e) {
924             // ignore
925         }
926 
927         if (userDN.equals("")) {
928             throw (LoginException)
929                 new FailedLoginException("Cannot find user's LDAP entry");
930         } else {
931             return userDN;
932         }
933     }
934 
935     /**
936      * Modify the supplied username to encode characters that must be escaped
937      * according to RFC 4515: LDAP: String Representation of Search Filters.
938      *
939      * The following characters are encoded as a backslash "\" (ASCII 0x5c)
940      * followed by the two hexadecimal digits representing the value of the
941      * escaped character:
942      *     '*' (ASCII 0x2a)
943      *     '(' (ASCII 0x28)
944      *     ')' (ASCII 0x29)
945      *     '\' (ASCII 0x5c)
946      *     '\0'(ASCII 0x00)
947      *
948      * @return the modified username with its characters escaped as needed
949      */
escapeUsernameChars()950     private String escapeUsernameChars() {
951         int len = username.length();
952         StringBuilder escapedUsername = new StringBuilder(len + 16);
953 
954         for (int i = 0; i < len; i++) {
955             char c = username.charAt(i);
956             switch (c) {
957             case '*':
958                 escapedUsername.append("\\\\2A");
959                 break;
960             case '(':
961                 escapedUsername.append("\\\\28");
962                 break;
963             case ')':
964                 escapedUsername.append("\\\\29");
965                 break;
966             case '\\':
967                 escapedUsername.append("\\\\5C");
968                 break;
969             case '\0':
970                 escapedUsername.append("\\\\00");
971                 break;
972             default:
973                 escapedUsername.append(c);
974             }
975         }
976 
977         return escapedUsername.toString();
978     }
979 
980 
981     /**
982      * Replace the username token
983      *
984      * @param matcher the replacement pattern
985      * @param string the target string
986      * @param username the supplied username
987      * @return the modified string
988      */
replaceUsernameToken(Matcher matcher, String string, String username)989     private String replaceUsernameToken(Matcher matcher, String string,
990         String username) {
991         return matcher != null ? matcher.replaceAll(username) : string;
992     }
993 
994     /**
995      * Get the username and password.
996      * This method does not return any value.
997      * Instead, it sets global name and password variables.
998      *
999      * <p> Also note that this method will set the username and password
1000      * values in the shared state in case subsequent LoginModules
1001      * want to use them via use/tryFirstPass.
1002      *
1003      * @param getPasswdFromSharedState boolean that tells this method whether
1004      *          to retrieve the password from the sharedState.
1005      * @exception LoginException if the username/password cannot be acquired.
1006      */
getUsernamePassword(boolean getPasswdFromSharedState)1007     private void getUsernamePassword(boolean getPasswdFromSharedState)
1008         throws LoginException {
1009 
1010         if (getPasswdFromSharedState) {
1011             // use the password saved by the first module in the stack
1012             username = (String)sharedState.get(USERNAME_KEY);
1013             password = (char[])sharedState.get(PASSWORD_KEY);
1014             return;
1015         }
1016 
1017         // prompt for a username and password
1018         if (callbackHandler == null)
1019             throw new LoginException("No CallbackHandler available " +
1020                 "to acquire authentication information from the user");
1021 
1022         Callback[] callbacks = new Callback[2];
1023         callbacks[0] = new NameCallback(rb.getString("username."));
1024         callbacks[1] = new PasswordCallback(rb.getString("password."), false);
1025 
1026         try {
1027             callbackHandler.handle(callbacks);
1028             username = ((NameCallback)callbacks[0]).getName();
1029             char[] tmpPassword = ((PasswordCallback)callbacks[1]).getPassword();
1030             password = new char[tmpPassword.length];
1031             System.arraycopy(tmpPassword, 0,
1032                                 password, 0, tmpPassword.length);
1033             ((PasswordCallback)callbacks[1]).clearPassword();
1034 
1035         } catch (java.io.IOException ioe) {
1036             throw new LoginException(ioe.toString());
1037 
1038         } catch (UnsupportedCallbackException uce) {
1039             throw new LoginException("Error: " + uce.getCallback().toString() +
1040                         " not available to acquire authentication information" +
1041                         " from the user");
1042         }
1043     }
1044 
1045     /**
1046      * Clean out state because of a failed authentication attempt
1047      */
cleanState()1048     private void cleanState() {
1049         username = null;
1050         if (password != null) {
1051             Arrays.fill(password, ' ');
1052             password = null;
1053         }
1054         try {
1055             if (ctx != null) {
1056                 ctx.close();
1057             }
1058         } catch (NamingException e) {
1059             // ignore
1060         }
1061         ctx = null;
1062 
1063         if (clearPass) {
1064             sharedState.remove(USERNAME_KEY);
1065             sharedState.remove(PASSWORD_KEY);
1066         }
1067     }
1068 }
1069