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 }