1 /*
   2  * Copyright (c) 2005, 2011, 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 assocated 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.Supported
 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")
 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             constraints.setReturningObjFlag(true); // to get the full DN
 430         }
 431 
 432         authzIdentity = (String)options.get(AUTHZ_IDENTITY);
 433         if (authzIdentity != null &&
 434             authzIdentity.startsWith("{") && authzIdentity.endsWith("}")) {
 435             if (constraints != null) {
 436                 authzIdentityAttr =
 437                     authzIdentity.substring(1, authzIdentity.length() - 1);
 438                 constraints.setReturningAttributes(
 439                     new String[]{authzIdentityAttr});
 440             }
 441             authzIdentity = null; // set later, from the specified attribute
 442         }
 443 
 444         // determine mode
 445         if (authcIdentity != null) {
 446             if (userFilter != null) {
 447                 authFirst = true; // authentication-first mode
 448             } else {
 449                 authOnly = true; // authentication-only mode
 450             }
 451         }
 452 
 453         if ("false".equalsIgnoreCase((String)options.get("useSSL"))) {
 454             useSSL = false;
 455             ldapEnvironment.remove(Context.SECURITY_PROTOCOL);
 456         } else {
 457             ldapEnvironment.put(Context.SECURITY_PROTOCOL, "ssl");
 458         }
 459 
 460         tryFirstPass =
 461                 "true".equalsIgnoreCase((String)options.get("tryFirstPass"));
 462 
 463         useFirstPass =
 464                 "true".equalsIgnoreCase((String)options.get("useFirstPass"));
 465 
 466         storePass = "true".equalsIgnoreCase((String)options.get("storePass"));
 467 
 468         clearPass = "true".equalsIgnoreCase((String)options.get("clearPass"));
 469 
 470         debug = "true".equalsIgnoreCase((String)options.get("debug"));
 471 
 472         if (debug) {
 473             if (authFirst) {
 474                 System.out.println("\t\t[LdapLoginModule] " +
 475                     "authentication-first mode; " +
 476                     (useSSL ? "SSL enabled" : "SSL disabled"));
 477             } else if (authOnly) {
 478                 System.out.println("\t\t[LdapLoginModule] " +
 479                     "authentication-only mode; " +
 480                     (useSSL ? "SSL enabled" : "SSL disabled"));
 481             } else {
 482                 System.out.println("\t\t[LdapLoginModule] " +
 483                     "search-first mode; " +
 484                     (useSSL ? "SSL enabled" : "SSL disabled"));
 485             }
 486         }
 487     }
 488 
 489     /**
 490      * Begin user authentication.
 491      *
 492      * <p> Acquire the user's credentials and verify them against the
 493      * specified LDAP directory.
 494      *
 495      * @return true always, since this <code>LoginModule</code>
 496      *          should not be ignored.
 497      * @exception FailedLoginException if the authentication fails.
 498      * @exception LoginException if this <code>LoginModule</code>
 499      *          is unable to perform the authentication.
 500      */
 501     public boolean login() throws LoginException {
 502 
 503         if (userProvider == null) {
 504             throw new LoginException
 505                 ("Unable to locate the LDAP directory service");
 506         }
 507 
 508         if (debug) {
 509             System.out.println("\t\t[LdapLoginModule] user provider: " +
 510                 userProvider);
 511         }
 512 
 513         // attempt the authentication
 514         if (tryFirstPass) {
 515 
 516             try {
 517                 // attempt the authentication by getting the
 518                 // username and password from shared state
 519                 attemptAuthentication(true);
 520 
 521                 // authentication succeeded
 522                 succeeded = true;
 523                 if (debug) {
 524                     System.out.println("\t\t[LdapLoginModule] " +
 525                                 "tryFirstPass succeeded");
 526                 }
 527                 return true;
 528 
 529             } catch (LoginException le) {
 530                 // authentication failed -- try again below by prompting
 531                 cleanState();
 532                 if (debug) {
 533                     System.out.println("\t\t[LdapLoginModule] " +
 534                                 "tryFirstPass failed: " + le.toString());
 535                 }
 536             }
 537 
 538         } else if (useFirstPass) {
 539 
 540             try {
 541                 // attempt the authentication by getting the
 542                 // username and password from shared state
 543                 attemptAuthentication(true);
 544 
 545                 // authentication succeeded
 546                 succeeded = true;
 547                 if (debug) {
 548                     System.out.println("\t\t[LdapLoginModule] " +
 549                                 "useFirstPass succeeded");
 550                 }
 551                 return true;
 552 
 553             } catch (LoginException le) {
 554                 // authentication failed
 555                 cleanState();
 556                 if (debug) {
 557                     System.out.println("\t\t[LdapLoginModule] " +
 558                                 "useFirstPass failed");
 559                 }
 560                 throw le;
 561             }
 562         }
 563 
 564         // attempt the authentication by prompting for the username and pwd
 565         try {
 566             attemptAuthentication(false);
 567 
 568             // authentication succeeded
 569            succeeded = true;
 570             if (debug) {
 571                 System.out.println("\t\t[LdapLoginModule] " +
 572                                 "authentication succeeded");
 573             }
 574             return true;
 575 
 576         } catch (LoginException le) {
 577             cleanState();
 578             if (debug) {
 579                 System.out.println("\t\t[LdapLoginModule] " +
 580                                 "authentication failed");
 581             }
 582             throw le;
 583         }
 584     }
 585 
 586     /**
 587      * Complete user authentication.
 588      *
 589      * <p> This method is called if the LoginContext's
 590      * overall authentication succeeded
 591      * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
 592      * succeeded).
 593      *
 594      * <p> If this LoginModule's own authentication attempt
 595      * succeeded (checked by retrieving the private state saved by the
 596      * <code>login</code> method), then this method associates an
 597      * <code>LdapPrincipal</code> and one or more <code>UserPrincipal</code>s
 598      * with the <code>Subject</code> located in the
 599      * <code>LoginModule</code>.  If this LoginModule's own
 600      * authentication attempted failed, then this method removes
 601      * any state that was originally saved.
 602      *
 603      * @exception LoginException if the commit fails
 604      * @return true if this LoginModule's own login and commit
 605      *          attempts succeeded, or false otherwise.
 606      */
 607     public boolean commit() throws LoginException {
 608 
 609         if (succeeded == false) {
 610             return false;
 611         } else {
 612             if (subject.isReadOnly()) {
 613                 cleanState();
 614                 throw new LoginException ("Subject is read-only");
 615             }
 616             // add Principals to the Subject
 617             Set<Principal> principals = subject.getPrincipals();
 618             if (! principals.contains(ldapPrincipal)) {
 619                 principals.add(ldapPrincipal);
 620             }
 621             if (debug) {
 622                 System.out.println("\t\t[LdapLoginModule] " +
 623                                    "added LdapPrincipal \"" +
 624                                    ldapPrincipal +
 625                                    "\" to Subject");
 626             }
 627 
 628             if (! principals.contains(userPrincipal)) {
 629                 principals.add(userPrincipal);
 630             }
 631             if (debug) {
 632                 System.out.println("\t\t[LdapLoginModule] " +
 633                                    "added UserPrincipal \"" +
 634                                    userPrincipal +
 635                                    "\" to Subject");
 636             }
 637 
 638             if (authzPrincipal != null &&
 639                 (! principals.contains(authzPrincipal))) {
 640                 principals.add(authzPrincipal);
 641 
 642                 if (debug) {
 643                     System.out.println("\t\t[LdapLoginModule] " +
 644                                    "added UserPrincipal \"" +
 645                                    authzPrincipal +
 646                                    "\" to Subject");
 647                 }
 648             }
 649         }
 650         // in any case, clean out state
 651         cleanState();
 652         commitSucceeded = true;
 653         return true;
 654     }
 655 
 656     /**
 657      * Abort user authentication.
 658      *
 659      * <p> This method is called if the overall authentication failed.
 660      * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
 661      * did not succeed).
 662      *
 663      * <p> If this LoginModule's own authentication attempt
 664      * succeeded (checked by retrieving the private state saved by the
 665      * <code>login</code> and <code>commit</code> methods),
 666      * then this method cleans up any state that was originally saved.
 667      *
 668      * @exception LoginException if the abort fails.
 669      * @return false if this LoginModule's own login and/or commit attempts
 670      *          failed, and true otherwise.
 671      */
 672     public boolean abort() throws LoginException {
 673         if (debug)
 674             System.out.println("\t\t[LdapLoginModule] " +
 675                 "aborted authentication");
 676 
 677         if (succeeded == false) {
 678             return false;
 679         } else if (succeeded == true && commitSucceeded == false) {
 680 
 681             // Clean out state
 682             succeeded = false;
 683             cleanState();
 684 
 685             ldapPrincipal = null;
 686             userPrincipal = null;
 687             authzPrincipal = null;
 688         } else {
 689             // overall authentication succeeded and commit succeeded,
 690             // but someone else's commit failed
 691             logout();
 692         }
 693         return true;
 694     }
 695 
 696     /**
 697      * Logout a user.
 698      *
 699      * <p> This method removes the Principals
 700      * that were added by the <code>commit</code> method.
 701      *
 702      * @exception LoginException if the logout fails.
 703      * @return true in all cases since this <code>LoginModule</code>
 704      *          should not be ignored.
 705      */
 706     public boolean logout() throws LoginException {
 707         if (subject.isReadOnly()) {
 708             cleanState();
 709             throw new LoginException ("Subject is read-only");
 710         }
 711         Set<Principal> principals = subject.getPrincipals();
 712         principals.remove(ldapPrincipal);
 713         principals.remove(userPrincipal);
 714         if (authzIdentity != null) {
 715             principals.remove(authzPrincipal);
 716         }
 717 
 718         // clean out state
 719         cleanState();
 720         succeeded = false;
 721         commitSucceeded = false;
 722 
 723         ldapPrincipal = null;
 724         userPrincipal = null;
 725         authzPrincipal = null;
 726 
 727         if (debug) {
 728             System.out.println("\t\t[LdapLoginModule] logged out Subject");
 729         }
 730         return true;
 731     }
 732 
 733     /**
 734      * Attempt authentication
 735      *
 736      * @param getPasswdFromSharedState boolean that tells this method whether
 737      *          to retrieve the password from the sharedState.
 738      * @exception LoginException if the authentication attempt fails.
 739      */
 740     private void attemptAuthentication(boolean getPasswdFromSharedState)
 741         throws LoginException {
 742 
 743         // first get the username and password
 744         getUsernamePassword(getPasswdFromSharedState);
 745 
 746         if (password == null || password.length == 0) {
 747             throw (LoginException)
 748                 new FailedLoginException("No password was supplied");
 749         }
 750 
 751         String dn = "";
 752 
 753         if (authFirst || authOnly) {
 754 
 755             String id = replaceUsernameToken(identityMatcher, authcIdentity);
 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      */
 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             NamingEnumeration<SearchResult> results = ctx.search("",
 883                 replaceUsernameToken(filterMatcher, userFilter), constraints);
 884 
 885             // Extract the distinguished name of the user's entry
 886             // (Use the first entry if more than one is returned)
 887             if (results.hasMore()) {
 888                 SearchResult entry = results.next();
 889 
 890                 // %%% - use the SearchResult.getNameInNamespace method
 891                 //        available in JDK 1.5 and later.
 892                 //        (can remove call to constraints.setReturningObjFlag)
 893                 userDN = ((Context)entry.getObject()).getNameInNamespace();
 894 
 895                 if (debug) {
 896                     System.out.println("\t\t[LdapLoginModule] found entry: " +
 897                         userDN);
 898                 }
 899 
 900                 // Extract a value from user's authorization identity attribute
 901                 if (authzIdentityAttr != null) {
 902                     Attribute attr =
 903                         entry.getAttributes().get(authzIdentityAttr);
 904                     if (attr != null) {
 905                         Object val = attr.get();
 906                         if (val instanceof String) {
 907                             authzIdentity = (String) val;
 908                         }
 909                     }
 910                 }
 911 
 912                 results.close();
 913 
 914             } else {
 915                 // Bad username
 916                 if (debug) {
 917                     System.out.println("\t\t[LdapLoginModule] user's entry " +
 918                         "not found");
 919                 }
 920             }
 921 
 922         } catch (NamingException e) {
 923             // ignore
 924         }
 925 
 926         if (userDN.equals("")) {
 927             throw (LoginException)
 928                 new FailedLoginException("Cannot find user's LDAP entry");
 929         } else {
 930             return userDN;
 931         }
 932     }
 933 
 934     /**
 935      * Replace the username token
 936      *
 937      * @param string the target string
 938      * @return the modified string
 939      */
 940     private String replaceUsernameToken(Matcher matcher, String string) {
 941         return matcher != null ? matcher.replaceAll(username) : string;
 942     }
 943 
 944     /**
 945      * Get the username and password.
 946      * This method does not return any value.
 947      * Instead, it sets global name and password variables.
 948      *
 949      * <p> Also note that this method will set the username and password
 950      * values in the shared state in case subsequent LoginModules
 951      * want to use them via use/tryFirstPass.
 952      *
 953      * @param getPasswdFromSharedState boolean that tells this method whether
 954      *          to retrieve the password from the sharedState.
 955      * @exception LoginException if the username/password cannot be acquired.
 956      */
 957     private void getUsernamePassword(boolean getPasswdFromSharedState)
 958         throws LoginException {
 959 
 960         if (getPasswdFromSharedState) {
 961             // use the password saved by the first module in the stack
 962             username = (String)sharedState.get(USERNAME_KEY);
 963             password = (char[])sharedState.get(PASSWORD_KEY);
 964             return;
 965         }
 966 
 967         // prompt for a username and password
 968         if (callbackHandler == null)
 969             throw new LoginException("No CallbackHandler available " +
 970                 "to acquire authentication information from the user");
 971 
 972         Callback[] callbacks = new Callback[2];
 973         callbacks[0] = new NameCallback(rb.getString("username."));
 974         callbacks[1] = new PasswordCallback(rb.getString("password."), false);
 975 
 976         try {
 977             callbackHandler.handle(callbacks);
 978             username = ((NameCallback)callbacks[0]).getName();
 979             char[] tmpPassword = ((PasswordCallback)callbacks[1]).getPassword();
 980             password = new char[tmpPassword.length];
 981             System.arraycopy(tmpPassword, 0,
 982                                 password, 0, tmpPassword.length);
 983             ((PasswordCallback)callbacks[1]).clearPassword();
 984 
 985         } catch (java.io.IOException ioe) {
 986             throw new LoginException(ioe.toString());
 987 
 988         } catch (UnsupportedCallbackException uce) {
 989             throw new LoginException("Error: " + uce.getCallback().toString() +
 990                         " not available to acquire authentication information" +
 991                         " from the user");
 992         }
 993     }
 994 
 995     /**
 996      * Clean out state because of a failed authentication attempt
 997      */
 998     private void cleanState() {
 999         username = null;
1000         if (password != null) {
1001             Arrays.fill(password, ' ');
1002             password = null;
1003         }
1004         try {
1005             if (ctx != null) {
1006                 ctx.close();
1007             }
1008         } catch (NamingException e) {
1009             // ignore
1010         }
1011         ctx = null;
1012 
1013         if (clearPass) {
1014             sharedState.remove(USERNAME_KEY);
1015             sharedState.remove(PASSWORD_KEY);
1016         }
1017     }
1018 }