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 }