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