1 /*
2  * Copyright (c) 2005, 2020, 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      * Creates an {@code LdapLoginModule}.
360      */
LdapLoginModule()361     public LdapLoginModule() {}
362 
363     /**
364      * Initialize this {@code LoginModule}.
365      *
366      * @param subject the {@code Subject} to be authenticated.
367      * @param callbackHandler a {@code CallbackHandler} to acquire the
368      *                  username and password.
369      * @param sharedState shared {@code LoginModule} state.
370      * @param options options specified in the login
371      *                  {@code Configuration} for this particular
372      *                  {@code LoginModule}.
373      */
374     // Unchecked warning from (Map<String, Object>)sharedState is safe
375     // since javax.security.auth.login.LoginContext passes a raw HashMap.
376     @SuppressWarnings("unchecked")
initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options)377     public void initialize(Subject subject, CallbackHandler callbackHandler,
378                         Map<String, ?> sharedState, Map<String, ?> options) {
379 
380         this.subject = subject;
381         this.callbackHandler = callbackHandler;
382         this.sharedState = (Map<String, Object>)sharedState;
383         this.options = options;
384 
385         ldapEnvironment = new Hashtable<String, Object>(9);
386         ldapEnvironment.put(Context.INITIAL_CONTEXT_FACTORY,
387             "com.sun.jndi.ldap.LdapCtxFactory");
388 
389         // Add any JNDI properties to the environment
390         for (String key : options.keySet()) {
391             if (key.indexOf('.') > -1) {
392                 ldapEnvironment.put(key, options.get(key));
393             }
394         }
395 
396         // initialize any configured options
397 
398         userProvider = (String)options.get(USER_PROVIDER);
399         if (userProvider != null) {
400             ldapEnvironment.put(Context.PROVIDER_URL, userProvider);
401         }
402 
403         authcIdentity = (String)options.get(AUTHC_IDENTITY);
404         if (authcIdentity != null &&
405             (authcIdentity.indexOf(USERNAME_TOKEN) != -1)) {
406             identityMatcher = USERNAME_PATTERN.matcher(authcIdentity);
407         }
408 
409         userFilter = (String)options.get(USER_FILTER);
410         if (userFilter != null) {
411             if (userFilter.indexOf(USERNAME_TOKEN) != -1) {
412                 filterMatcher = USERNAME_PATTERN.matcher(userFilter);
413             }
414             constraints = new SearchControls();
415             constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
416             constraints.setReturningAttributes(new String[0]); //return no attrs
417         }
418 
419         authzIdentity = (String)options.get(AUTHZ_IDENTITY);
420         if (authzIdentity != null &&
421             authzIdentity.startsWith("{") && authzIdentity.endsWith("}")) {
422             if (constraints != null) {
423                 authzIdentityAttr =
424                     authzIdentity.substring(1, authzIdentity.length() - 1);
425                 constraints.setReturningAttributes(
426                     new String[]{authzIdentityAttr});
427             }
428             authzIdentity = null; // set later, from the specified attribute
429         }
430 
431         // determine mode
432         if (authcIdentity != null) {
433             if (userFilter != null) {
434                 authFirst = true; // authentication-first mode
435             } else {
436                 authOnly = true; // authentication-only mode
437             }
438         }
439 
440         if ("false".equalsIgnoreCase((String)options.get("useSSL"))) {
441             useSSL = false;
442             ldapEnvironment.remove(Context.SECURITY_PROTOCOL);
443         } else {
444             ldapEnvironment.put(Context.SECURITY_PROTOCOL, "ssl");
445         }
446 
447         tryFirstPass =
448                 "true".equalsIgnoreCase((String)options.get("tryFirstPass"));
449 
450         useFirstPass =
451                 "true".equalsIgnoreCase((String)options.get("useFirstPass"));
452 
453         storePass = "true".equalsIgnoreCase((String)options.get("storePass"));
454 
455         clearPass = "true".equalsIgnoreCase((String)options.get("clearPass"));
456 
457         debug = "true".equalsIgnoreCase((String)options.get("debug"));
458 
459         if (debug) {
460             if (authFirst) {
461                 System.out.println("\t\t[LdapLoginModule] " +
462                     "authentication-first mode; " +
463                     (useSSL ? "SSL enabled" : "SSL disabled"));
464             } else if (authOnly) {
465                 System.out.println("\t\t[LdapLoginModule] " +
466                     "authentication-only mode; " +
467                     (useSSL ? "SSL enabled" : "SSL disabled"));
468             } else {
469                 System.out.println("\t\t[LdapLoginModule] " +
470                     "search-first mode; " +
471                     (useSSL ? "SSL enabled" : "SSL disabled"));
472             }
473         }
474     }
475 
476     /**
477      * Begin user authentication.
478      *
479      * <p> Acquire the user's credentials and verify them against the
480      * specified LDAP directory.
481      *
482      * @return true always, since this {@code LoginModule}
483      *          should not be ignored.
484      * @exception FailedLoginException if the authentication fails.
485      * @exception LoginException if this {@code LoginModule}
486      *          is unable to perform the authentication.
487      */
login()488     public boolean login() throws LoginException {
489 
490         if (userProvider == null) {
491             throw new LoginException
492                 ("Unable to locate the LDAP directory service");
493         }
494 
495         if (debug) {
496             System.out.println("\t\t[LdapLoginModule] user provider: " +
497                 userProvider);
498         }
499 
500         // attempt the authentication
501         if (tryFirstPass) {
502 
503             try {
504                 // attempt the authentication by getting the
505                 // username and password from shared state
506                 attemptAuthentication(true);
507 
508                 // authentication succeeded
509                 succeeded = true;
510                 if (debug) {
511                     System.out.println("\t\t[LdapLoginModule] " +
512                                 "tryFirstPass succeeded");
513                 }
514                 return true;
515 
516             } catch (LoginException le) {
517                 // authentication failed -- try again below by prompting
518                 cleanState();
519                 if (debug) {
520                     System.out.println("\t\t[LdapLoginModule] " +
521                                 "tryFirstPass failed: " + le.toString());
522                 }
523             }
524 
525         } else if (useFirstPass) {
526 
527             try {
528                 // attempt the authentication by getting the
529                 // username and password from shared state
530                 attemptAuthentication(true);
531 
532                 // authentication succeeded
533                 succeeded = true;
534                 if (debug) {
535                     System.out.println("\t\t[LdapLoginModule] " +
536                                 "useFirstPass succeeded");
537                 }
538                 return true;
539 
540             } catch (LoginException le) {
541                 // authentication failed
542                 cleanState();
543                 if (debug) {
544                     System.out.println("\t\t[LdapLoginModule] " +
545                                 "useFirstPass failed");
546                 }
547                 throw le;
548             }
549         }
550 
551         // attempt the authentication by prompting for the username and pwd
552         try {
553             attemptAuthentication(false);
554 
555             // authentication succeeded
556            succeeded = true;
557             if (debug) {
558                 System.out.println("\t\t[LdapLoginModule] " +
559                                 "authentication succeeded");
560             }
561             return true;
562 
563         } catch (LoginException le) {
564             cleanState();
565             if (debug) {
566                 System.out.println("\t\t[LdapLoginModule] " +
567                                 "authentication failed");
568             }
569             throw le;
570         }
571     }
572 
573     /**
574      * Complete user authentication.
575      *
576      * <p> This method is called if the LoginContext's
577      * overall authentication succeeded
578      * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
579      * succeeded).
580      *
581      * <p> If this LoginModule's own authentication attempt
582      * succeeded (checked by retrieving the private state saved by the
583      * {@code login} method), then this method associates an
584      * {@code LdapPrincipal} and one or more {@code UserPrincipal}s
585      * with the {@code Subject} located in the
586      * {@code LoginModule}.  If this LoginModule's own
587      * authentication attempted failed, then this method removes
588      * any state that was originally saved.
589      *
590      * @exception LoginException if the commit fails
591      * @return true if this LoginModule's own login and commit
592      *          attempts succeeded, or false otherwise.
593      */
commit()594     public boolean commit() throws LoginException {
595 
596         if (succeeded == false) {
597             return false;
598         } else {
599             if (subject.isReadOnly()) {
600                 cleanState();
601                 throw new LoginException ("Subject is read-only");
602             }
603             // add Principals to the Subject
604             Set<Principal> principals = subject.getPrincipals();
605             if (! principals.contains(ldapPrincipal)) {
606                 principals.add(ldapPrincipal);
607             }
608             if (debug) {
609                 System.out.println("\t\t[LdapLoginModule] " +
610                                    "added LdapPrincipal \"" +
611                                    ldapPrincipal +
612                                    "\" to Subject");
613             }
614 
615             if (! principals.contains(userPrincipal)) {
616                 principals.add(userPrincipal);
617             }
618             if (debug) {
619                 System.out.println("\t\t[LdapLoginModule] " +
620                                    "added UserPrincipal \"" +
621                                    userPrincipal +
622                                    "\" to Subject");
623             }
624 
625             if (authzPrincipal != null &&
626                 (! principals.contains(authzPrincipal))) {
627                 principals.add(authzPrincipal);
628 
629                 if (debug) {
630                     System.out.println("\t\t[LdapLoginModule] " +
631                                    "added UserPrincipal \"" +
632                                    authzPrincipal +
633                                    "\" to Subject");
634                 }
635             }
636         }
637         // in any case, clean out state
638         cleanState();
639         commitSucceeded = true;
640         return true;
641     }
642 
643     /**
644      * Abort user authentication.
645      *
646      * <p> This method is called if the overall authentication failed.
647      * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
648      * did not succeed).
649      *
650      * <p> If this LoginModule's own authentication attempt
651      * succeeded (checked by retrieving the private state saved by the
652      * {@code login} and {@code commit} methods),
653      * then this method cleans up any state that was originally saved.
654      *
655      * @exception LoginException if the abort fails.
656      * @return false if this LoginModule's own login and/or commit attempts
657      *          failed, and true otherwise.
658      */
abort()659     public boolean abort() throws LoginException {
660         if (debug)
661             System.out.println("\t\t[LdapLoginModule] " +
662                 "aborted authentication");
663 
664         if (succeeded == false) {
665             return false;
666         } else if (succeeded == true && commitSucceeded == false) {
667 
668             // Clean out state
669             succeeded = false;
670             cleanState();
671 
672             ldapPrincipal = null;
673             userPrincipal = null;
674             authzPrincipal = null;
675         } else {
676             // overall authentication succeeded and commit succeeded,
677             // but someone else's commit failed
678             logout();
679         }
680         return true;
681     }
682 
683     /**
684      * Logout a user.
685      *
686      * <p> This method removes the Principals
687      * that were added by the {@code commit} method.
688      *
689      * @exception LoginException if the logout fails.
690      * @return true in all cases since this {@code LoginModule}
691      *          should not be ignored.
692      */
logout()693     public boolean logout() throws LoginException {
694         if (subject.isReadOnly()) {
695             cleanState();
696             throw new LoginException ("Subject is read-only");
697         }
698         Set<Principal> principals = subject.getPrincipals();
699         principals.remove(ldapPrincipal);
700         principals.remove(userPrincipal);
701         if (authzIdentity != null) {
702             principals.remove(authzPrincipal);
703         }
704 
705         // clean out state
706         cleanState();
707         succeeded = false;
708         commitSucceeded = false;
709 
710         ldapPrincipal = null;
711         userPrincipal = null;
712         authzPrincipal = null;
713 
714         if (debug) {
715             System.out.println("\t\t[LdapLoginModule] logged out Subject");
716         }
717         return true;
718     }
719 
720     /**
721      * Attempt authentication
722      *
723      * @param getPasswdFromSharedState boolean that tells this method whether
724      *          to retrieve the password from the sharedState.
725      * @exception LoginException if the authentication attempt fails.
726      */
attemptAuthentication(boolean getPasswdFromSharedState)727     private void attemptAuthentication(boolean getPasswdFromSharedState)
728         throws LoginException {
729 
730         // first get the username and password
731         getUsernamePassword(getPasswdFromSharedState);
732 
733         if (password == null || password.length == 0) {
734             throw (LoginException)
735                 new FailedLoginException("No password was supplied");
736         }
737 
738         String dn = "";
739 
740         if (authFirst || authOnly) {
741 
742             String id =
743                 replaceUsernameToken(identityMatcher, authcIdentity, username);
744 
745             // Prepare to bind using user's username and password
746             ldapEnvironment.put(Context.SECURITY_CREDENTIALS, password);
747             ldapEnvironment.put(Context.SECURITY_PRINCIPAL, id);
748 
749             if (debug) {
750                 System.out.println("\t\t[LdapLoginModule] " +
751                     "attempting to authenticate user: " + username);
752             }
753 
754             try {
755                 // Connect to the LDAP server (using simple bind)
756                 ctx = new InitialLdapContext(ldapEnvironment, null);
757 
758             } catch (NamingException e) {
759                 throw (LoginException)
760                     new FailedLoginException("Cannot bind to LDAP server")
761                         .initCause(e);
762             }
763 
764             // Authentication has succeeded
765 
766             // Locate the user's distinguished name
767             if (userFilter != null) {
768                 dn = findUserDN(ctx);
769             } else {
770                 dn = id;
771             }
772 
773         } else {
774 
775             try {
776                 // Connect to the LDAP server (using anonymous bind)
777                 ctx = new InitialLdapContext(ldapEnvironment, null);
778 
779             } catch (NamingException e) {
780                 throw (LoginException)
781                     new FailedLoginException("Cannot connect to LDAP server")
782                         .initCause(e);
783             }
784 
785             // Locate the user's distinguished name
786             dn = findUserDN(ctx);
787 
788             try {
789 
790                 // Prepare to bind using user's distinguished name and password
791                 ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple");
792                 ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, dn);
793                 ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
794 
795                 if (debug) {
796                     System.out.println("\t\t[LdapLoginModule] " +
797                         "attempting to authenticate user: " + username);
798                 }
799                 // Connect to the LDAP server (using simple bind)
800                 ctx.reconnect(null);
801 
802                 // Authentication has succeeded
803 
804             } catch (NamingException e) {
805                 throw (LoginException)
806                     new FailedLoginException("Cannot bind to LDAP server")
807                         .initCause(e);
808             }
809         }
810 
811         // Save input as shared state only if authentication succeeded
812         if (storePass &&
813             !sharedState.containsKey(USERNAME_KEY) &&
814             !sharedState.containsKey(PASSWORD_KEY)) {
815             sharedState.put(USERNAME_KEY, username);
816             sharedState.put(PASSWORD_KEY, password);
817         }
818 
819         // Create the user principals
820         userPrincipal = new UserPrincipal(username);
821         if (authzIdentity != null) {
822             authzPrincipal = new UserPrincipal(authzIdentity);
823         }
824 
825         try {
826 
827             ldapPrincipal = new LdapPrincipal(dn);
828 
829         } catch (InvalidNameException e) {
830             if (debug) {
831                 System.out.println("\t\t[LdapLoginModule] " +
832                                    "cannot create LdapPrincipal: bad DN");
833             }
834             throw (LoginException)
835                 new FailedLoginException("Cannot create LdapPrincipal")
836                     .initCause(e);
837         }
838     }
839 
840     /**
841      * Search for the user's entry.
842      * Determine the distinguished name of the user's entry and optionally
843      * an authorization identity for the user.
844      *
845      * @param ctx an LDAP context to use for the search
846      * @return the user's distinguished name or an empty string if none
847      *         was found.
848      * @exception LoginException if the user's entry cannot be found.
849      */
findUserDN(LdapContext ctx)850     private String findUserDN(LdapContext ctx) throws LoginException {
851 
852         String userDN = "";
853 
854         // Locate the user's LDAP entry
855         if (userFilter != null) {
856             if (debug) {
857                 System.out.println("\t\t[LdapLoginModule] " +
858                     "searching for entry belonging to user: " + username);
859             }
860         } else {
861             if (debug) {
862                 System.out.println("\t\t[LdapLoginModule] " +
863                     "cannot search for entry belonging to user: " + username);
864             }
865             throw (LoginException)
866                 new FailedLoginException("Cannot find user's LDAP entry");
867         }
868 
869         try {
870             // Sanitize username and substitute into LDAP filter
871             String canonicalUserFilter =
872                 replaceUsernameToken(filterMatcher, userFilter,
873                     escapeUsernameChars());
874 
875             NamingEnumeration<SearchResult> results =
876                 ctx.search("", canonicalUserFilter, constraints);
877 
878             // Extract the distinguished name of the user's entry
879             // (Use the first entry if more than one is returned)
880             if (results.hasMore()) {
881                 SearchResult entry = results.next();
882                 userDN = entry.getNameInNamespace();
883 
884                 if (debug) {
885                     System.out.println("\t\t[LdapLoginModule] found entry: " +
886                         userDN);
887                 }
888 
889                 // Extract a value from user's authorization identity attribute
890                 if (authzIdentityAttr != null) {
891                     Attribute attr =
892                         entry.getAttributes().get(authzIdentityAttr);
893                     if (attr != null) {
894                         Object val = attr.get();
895                         if (val instanceof String) {
896                             authzIdentity = (String) val;
897                         }
898                     }
899                 }
900 
901                 results.close();
902 
903             } else {
904                 // Bad username
905                 if (debug) {
906                     System.out.println("\t\t[LdapLoginModule] user's entry " +
907                         "not found");
908                 }
909             }
910 
911         } catch (NamingException e) {
912             // ignore
913         }
914 
915         if (userDN.equals("")) {
916             throw (LoginException)
917                 new FailedLoginException("Cannot find user's LDAP entry");
918         } else {
919             return userDN;
920         }
921     }
922 
923     /**
924      * Modify the supplied username to encode characters that must be escaped
925      * according to RFC 4515: LDAP: String Representation of Search Filters.
926      *
927      * The following characters are encoded as a backslash "\" (ASCII 0x5c)
928      * followed by the two hexadecimal digits representing the value of the
929      * escaped character:
930      *     '*' (ASCII 0x2a)
931      *     '(' (ASCII 0x28)
932      *     ')' (ASCII 0x29)
933      *     '\' (ASCII 0x5c)
934      *     '\0'(ASCII 0x00)
935      *
936      * @return the modified username with its characters escaped as needed
937      */
escapeUsernameChars()938     private String escapeUsernameChars() {
939         int len = username.length();
940         StringBuilder escapedUsername = new StringBuilder(len + 16);
941 
942         for (int i = 0; i < len; i++) {
943             char c = username.charAt(i);
944             switch (c) {
945             case '*':
946                 escapedUsername.append("\\\\2A");
947                 break;
948             case '(':
949                 escapedUsername.append("\\\\28");
950                 break;
951             case ')':
952                 escapedUsername.append("\\\\29");
953                 break;
954             case '\\':
955                 escapedUsername.append("\\\\5C");
956                 break;
957             case '\0':
958                 escapedUsername.append("\\\\00");
959                 break;
960             default:
961                 escapedUsername.append(c);
962             }
963         }
964 
965         return escapedUsername.toString();
966     }
967 
968 
969     /**
970      * Replace the username token
971      *
972      * @param matcher the replacement pattern
973      * @param string the target string
974      * @param username the supplied username
975      * @return the modified string
976      */
replaceUsernameToken(Matcher matcher, String string, String username)977     private String replaceUsernameToken(Matcher matcher, String string,
978         String username) {
979         return matcher != null ? matcher.replaceAll(username) : string;
980     }
981 
982     /**
983      * Get the username and password.
984      * This method does not return any value.
985      * Instead, it sets global name and password variables.
986      *
987      * <p> Also note that this method will set the username and password
988      * values in the shared state in case subsequent LoginModules
989      * want to use them via use/tryFirstPass.
990      *
991      * @param getPasswdFromSharedState boolean that tells this method whether
992      *          to retrieve the password from the sharedState.
993      * @exception LoginException if the username/password cannot be acquired.
994      */
getUsernamePassword(boolean getPasswdFromSharedState)995     private void getUsernamePassword(boolean getPasswdFromSharedState)
996         throws LoginException {
997 
998         if (getPasswdFromSharedState) {
999             // use the password saved by the first module in the stack
1000             username = (String)sharedState.get(USERNAME_KEY);
1001             password = (char[])sharedState.get(PASSWORD_KEY);
1002             return;
1003         }
1004 
1005         // prompt for a username and password
1006         if (callbackHandler == null)
1007             throw new LoginException("No CallbackHandler available " +
1008                 "to acquire authentication information from the user");
1009 
1010         Callback[] callbacks = new Callback[2];
1011         callbacks[0] = new NameCallback(getAuthResourceString("username."));
1012         callbacks[1] = new PasswordCallback(getAuthResourceString("password."), false);
1013 
1014         try {
1015             callbackHandler.handle(callbacks);
1016             username = ((NameCallback)callbacks[0]).getName();
1017             char[] tmpPassword = ((PasswordCallback)callbacks[1]).getPassword();
1018             password = new char[tmpPassword.length];
1019             System.arraycopy(tmpPassword, 0,
1020                                 password, 0, tmpPassword.length);
1021             ((PasswordCallback)callbacks[1]).clearPassword();
1022 
1023         } catch (java.io.IOException ioe) {
1024             throw new LoginException(ioe.toString());
1025 
1026         } catch (UnsupportedCallbackException uce) {
1027             throw new LoginException("Error: " + uce.getCallback().toString() +
1028                         " not available to acquire authentication information" +
1029                         " from the user");
1030         }
1031     }
1032 
1033     /**
1034      * Clean out state because of a failed authentication attempt
1035      */
cleanState()1036     private void cleanState() {
1037         username = null;
1038         if (password != null) {
1039             Arrays.fill(password, ' ');
1040             password = null;
1041         }
1042         try {
1043             if (ctx != null) {
1044                 ctx.close();
1045             }
1046         } catch (NamingException e) {
1047             // ignore
1048         }
1049         ctx = null;
1050 
1051         if (clearPass) {
1052             sharedState.remove(USERNAME_KEY);
1053             sharedState.remove(PASSWORD_KEY);
1054         }
1055     }
1056 }
1057