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