1 /* 2 * Copyright (c) 2005, 2017, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.sun.security.auth.module; 27 28 import java.net.SocketPermission; 29 import java.security.Principal; 30 import java.util.Arrays; 31 import java.util.Hashtable; 32 import java.util.Map; 33 import java.util.regex.Matcher; 34 import java.util.regex.Pattern; 35 import java.util.Set; 36 37 import javax.naming.*; 38 import javax.naming.directory.*; 39 import javax.naming.ldap.*; 40 import javax.security.auth.*; 41 import javax.security.auth.callback.*; 42 import javax.security.auth.login.*; 43 import javax.security.auth.spi.*; 44 45 import com.sun.security.auth.LdapPrincipal; 46 import com.sun.security.auth.UserPrincipal; 47 import static sun.security.util.ResourcesMgr.getAuthResourceString; 48 49 50 /** 51 * This {@link LoginModule} performs LDAP-based authentication. 52 * A username and password is verified against the corresponding user 53 * credentials stored in an LDAP directory. 54 * This module requires the supplied {@link CallbackHandler} to support a 55 * {@link NameCallback} and a {@link PasswordCallback}. 56 * If authentication is successful then a new {@link LdapPrincipal} is created 57 * using the user's distinguished name and a new {@link UserPrincipal} is 58 * created using the user's username and both are associated 59 * with the current {@link Subject}. 60 * 61 * <p> This module operates in one of three modes: <i>search-first</i>, 62 * <i>authentication-first</i> or <i>authentication-only</i>. 63 * A mode is selected by specifying a particular set of options. 64 * 65 * <p> In search-first mode, the LDAP directory is searched to determine the 66 * user's distinguished name and then authentication is attempted. 67 * An (anonymous) search is performed using the supplied username in 68 * conjunction with a specified search filter. 69 * If successful then authentication is attempted using the user's 70 * distinguished name and the supplied password. 71 * To enable this mode, set the {@code userFilter} option and omit the 72 * {@code authIdentity} option. 73 * Use search-first mode when the user's distinguished name is not 74 * known in advance. 75 * 76 * <p> In authentication-first mode, authentication is attempted using the 77 * supplied username and password and then the LDAP directory is searched. 78 * If authentication is successful then a search is performed using the 79 * supplied username in conjunction with a specified search filter. 80 * To enable this mode, set the {@code authIdentity} and the 81 * {@code userFilter} options. 82 * Use authentication-first mode when accessing an LDAP directory 83 * that has been configured to disallow anonymous searches. 84 * 85 * <p> In authentication-only mode, authentication is attempted using the 86 * supplied username and password. The LDAP directory is not searched because 87 * the user's distinguished name is already known. 88 * To enable this mode, set the {@code authIdentity} option to a valid 89 * distinguished name and omit the {@code userFilter} option. 90 * Use authentication-only mode when the user's distinguished name is 91 * known in advance. 92 * 93 * <p> The following option is mandatory and must be specified in this 94 * module's login {@link Configuration}: 95 * <dl> 96 * <dt> <code>userProvider=<b>ldap_urls</b></code> 97 * </dt> 98 * <dd> This option identifies the LDAP directory that stores user entries. 99 * <b>ldap_urls</b> is a list of space-separated LDAP URLs 100 * (<a href="http://www.ietf.org/rfc/rfc2255.txt">RFC 2255</a>) 101 * that identifies the LDAP server to use and the position in 102 * its directory tree where user entries are located. 103 * When several LDAP URLs are specified then each is attempted, 104 * in turn, until the first successful connection is established. 105 * Spaces in the distinguished name component of the URL must be escaped 106 * using the standard mechanism of percent character ('{@code %}') 107 * followed by two hexadecimal digits (see {@link java.net.URI}). 108 * Query components must also be omitted from the URL. 109 * 110 * <p> 111 * Automatic discovery of the LDAP server via DNS 112 * (<a href="http://www.ietf.org/rfc/rfc2782.txt">RFC 2782</a>) 113 * is supported (once DNS has been configured to support such a service). 114 * It is enabled by omitting the hostname and port number components from 115 * the LDAP URL. </dd> 116 * </dl> 117 * 118 * <p> This module also recognizes the following optional {@link Configuration} 119 * options: 120 * <dl> 121 * <dt> <code>userFilter=<b>ldap_filter</b></code> </dt> 122 * <dd> This option specifies the search filter to use to locate a user's 123 * entry in the LDAP directory. It is used to determine a user's 124 * distinguished name. 125 * <b>{@code ldap_filter}</b> is an LDAP filter string 126 * (<a href="http://www.ietf.org/rfc/rfc2254.txt">RFC 2254</a>). 127 * If it contains the special token "<b>{@code {USERNAME}}</b>" 128 * then that token will be replaced with the supplied username value 129 * before the filter is used to search the directory. </dd> 130 * 131 * <dt> <code>authIdentity=<b>auth_id</b></code> </dt> 132 * <dd> This option specifies the identity to use when authenticating a user 133 * to the LDAP directory. 134 * <b>{@code auth_id}</b> may be an LDAP distinguished name string 135 * (<a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>) or some 136 * other string name. 137 * It must contain the special token "<b>{@code {USERNAME}}</b>" 138 * which will be replaced with the supplied username value before the 139 * name is used for authentication. 140 * Note that if this option does not contain a distinguished name then 141 * the {@code userFilter} option must also be specified. </dd> 142 * 143 * <dt> <code>authzIdentity=<b>authz_id</b></code> </dt> 144 * <dd> This option specifies an authorization identity for the user. 145 * <b>{@code authz_id}</b> is any string name. 146 * If it comprises a single special token with curly braces then 147 * that token is treated as a attribute name and will be replaced with a 148 * single value of that attribute from the user's LDAP entry. 149 * If the attribute cannot be found then the option is ignored. 150 * When this option is supplied and the user has been successfully 151 * authenticated then an additional {@link UserPrincipal} 152 * is created using the authorization identity and it is associated with 153 * the current {@link Subject}. </dd> 154 * 155 * <dt> {@code useSSL} </dt> 156 * <dd> if {@code false}, this module does not establish an SSL connection 157 * to the LDAP server before attempting authentication. SSL is used to 158 * protect the privacy of the user's password because it is transmitted 159 * in the clear over LDAP. 160 * By default, this module uses SSL. </dd> 161 * 162 * <dt> {@code useFirstPass} </dt> 163 * <dd> if {@code true}, this module retrieves the username and password 164 * from the module's shared state, using "javax.security.auth.login.name" 165 * and "javax.security.auth.login.password" as the respective keys. The 166 * retrieved values are used for authentication. If authentication fails, 167 * no attempt for a retry is made, and the failure is reported back to 168 * the calling application.</dd> 169 * 170 * <dt> {@code tryFirstPass} </dt> 171 * <dd> if {@code true}, this module retrieves the username and password 172 * from the module's shared state, using "javax.security.auth.login.name" 173 * and "javax.security.auth.login.password" as the respective keys. The 174 * retrieved values are used for authentication. If authentication fails, 175 * the module uses the {@link CallbackHandler} to retrieve a new username 176 * and password, and another attempt to authenticate is made. If the 177 * authentication fails, the failure is reported back to the calling 178 * application.</dd> 179 * 180 * <dt> {@code storePass} </dt> 181 * <dd> if {@code true}, this module stores the username and password 182 * obtained from the {@link CallbackHandler} in the module's shared state, 183 * using 184 * "javax.security.auth.login.name" and 185 * "javax.security.auth.login.password" as the respective keys. This is 186 * not performed if existing values already exist for the username and 187 * password in the shared state, or if authentication fails.</dd> 188 * 189 * <dt> {@code clearPass} </dt> 190 * <dd> if {@code true}, this module clears the username and password 191 * stored in the module's shared state after both phases of authentication 192 * (login and commit) have completed.</dd> 193 * 194 * <dt> {@code debug} </dt> 195 * <dd> if {@code true}, debug messages are displayed on the standard 196 * output stream.</dd> 197 * </dl> 198 * 199 * <p> 200 * Arbitrary 201 * {@extLink jndi_ldap_gl_prop "JNDI properties"} 202 * may also be specified in the {@link Configuration}. 203 * They are added to the environment and passed to the LDAP provider. 204 * Note that the following four JNDI properties are set by this module directly 205 * and are ignored if also present in the configuration: 206 * <ul> 207 * <li> {@code java.naming.provider.url} 208 * <li> {@code java.naming.security.principal} 209 * <li> {@code java.naming.security.credentials} 210 * <li> {@code java.naming.security.protocol} 211 * </ul> 212 * 213 * <p> 214 * Three sample {@link Configuration}s are shown below. 215 * The first one activates search-first mode. It identifies the LDAP server 216 * and specifies that users' entries be located by their {@code uid} and 217 * {@code objectClass} attributes. It also specifies that an identity 218 * based on the user's {@code employeeNumber} attribute should be created. 219 * The second one activates authentication-first mode. It requests that the 220 * LDAP server be located dynamically, that authentication be performed using 221 * the supplied username directly but without the protection of SSL and that 222 * users' entries be located by one of three naming attributes and their 223 * {@code objectClass} attribute. 224 * The third one activates authentication-only mode. It identifies alternative 225 * LDAP servers, it specifies the distinguished name to use for 226 * authentication and a fixed identity to use for authorization. No directory 227 * search is performed. 228 * 229 * <pre>{@literal 230 * 231 * ExampleApplication { 232 * com.sun.security.auth.module.LdapLoginModule REQUIRED 233 * userProvider="ldap://ldap-svr/ou=people,dc=example,dc=com" 234 * userFilter="(&(uid={USERNAME})(objectClass=inetOrgPerson))" 235 * authzIdentity="{EMPLOYEENUMBER}" 236 * debug=true; 237 * }; 238 * 239 * ExampleApplication { 240 * com.sun.security.auth.module.LdapLoginModule REQUIRED 241 * userProvider="ldap:///cn=users,dc=example,dc=com" 242 * authIdentity="{USERNAME}" 243 * userFilter="(&(|(samAccountName={USERNAME})(userPrincipalName={USERNAME})(cn={USERNAME}))(objectClass=user))" 244 * useSSL=false 245 * debug=true; 246 * }; 247 * 248 * ExampleApplication { 249 * com.sun.security.auth.module.LdapLoginModule REQUIRED 250 * userProvider="ldap://ldap-svr1 ldap://ldap-svr2" 251 * authIdentity="cn={USERNAME},ou=people,dc=example,dc=com" 252 * authzIdentity="staff" 253 * debug=true; 254 * }; 255 * 256 * }</pre> 257 * 258 * <dl> 259 * <dt><b>Note:</b> </dt> 260 * <dd>When a {@link SecurityManager} is active then an application 261 * that creates a {@link LoginContext} and uses a {@link LoginModule} 262 * must be granted certain permissions. 263 * <p> 264 * If the application creates a login context using an <em>installed</em> 265 * {@link Configuration} then the application must be granted the 266 * {@link AuthPermission} to create login contexts. 267 * For example, the following security policy allows an application in 268 * the user's current directory to instantiate <em>any</em> login context: 269 * <pre> 270 * 271 * grant codebase "file:${user.dir}/" { 272 * permission javax.security.auth.AuthPermission "createLoginContext.*"; 273 * }; 274 * </pre> 275 * 276 * Alternatively, if the application creates a login context using a 277 * <em>caller-specified</em> {@link Configuration} then the application 278 * must be granted the permissions required by the {@link LoginModule}. 279 * <em>This</em> module requires the following two permissions: 280 * <ul> 281 * <li> The {@link SocketPermission} to connect to an LDAP server. 282 * <li> The {@link AuthPermission} to modify the set of {@link Principal}s 283 * associated with a {@link Subject}. 284 * </ul> 285 * <p> 286 * For example, the following security policy grants an application in the 287 * user's current directory all the permissions required by this module: 288 * <pre> 289 * 290 * grant codebase "file:${user.dir}/" { 291 * permission java.net.SocketPermission "*:389", "connect"; 292 * permission java.net.SocketPermission "*:636", "connect"; 293 * permission javax.security.auth.AuthPermission "modifyPrincipals"; 294 * }; 295 * </pre> 296 * </dd> 297 * </dl> 298 * 299 * @since 1.6 300 */ 301 public class LdapLoginModule implements LoginModule { 302 303 // Keys to retrieve the stored username and password 304 private static final String USERNAME_KEY = "javax.security.auth.login.name"; 305 private static final String PASSWORD_KEY = 306 "javax.security.auth.login.password"; 307 308 // Option names 309 private static final String USER_PROVIDER = "userProvider"; 310 private static final String USER_FILTER = "userFilter"; 311 private static final String AUTHC_IDENTITY = "authIdentity"; 312 private static final String AUTHZ_IDENTITY = "authzIdentity"; 313 314 // Used for the username token replacement 315 private static final String USERNAME_TOKEN = "{USERNAME}"; 316 private static final Pattern USERNAME_PATTERN = 317 Pattern.compile("\\{USERNAME\\}"); 318 319 // Configurable options 320 private String userProvider; 321 private String userFilter; 322 private String authcIdentity; 323 private String authzIdentity; 324 private String authzIdentityAttr = null; 325 private boolean useSSL = true; 326 private boolean authFirst = false; 327 private boolean authOnly = false; 328 private boolean useFirstPass = false; 329 private boolean tryFirstPass = false; 330 private boolean storePass = false; 331 private boolean clearPass = false; 332 private boolean debug = false; 333 334 // Authentication status 335 private boolean succeeded = false; 336 private boolean commitSucceeded = false; 337 338 // Supplied username and password 339 private String username; 340 private char[] password; 341 342 // User's identities 343 private LdapPrincipal ldapPrincipal; 344 private UserPrincipal userPrincipal; 345 private UserPrincipal authzPrincipal; 346 347 // Initial state 348 private Subject subject; 349 private CallbackHandler callbackHandler; 350 private Map<String, Object> sharedState; 351 private Map<String, ?> options; 352 private LdapContext ctx; 353 private Matcher identityMatcher = null; 354 private Matcher filterMatcher = null; 355 private Hashtable<String, Object> ldapEnvironment; 356 private SearchControls constraints = null; 357 358 /** 359 * Initialize this {@code LoginModule}. 360 * 361 * @param subject the {@code Subject} to be authenticated. 362 * @param callbackHandler a {@code CallbackHandler} to acquire the 363 * username and password. 364 * @param sharedState shared {@code LoginModule} state. 365 * @param options options specified in the login 366 * {@code Configuration} for this particular 367 * {@code LoginModule}. 368 */ 369 // Unchecked warning from (Map<String, Object>)sharedState is safe 370 // since javax.security.auth.login.LoginContext passes a raw HashMap. 371 @SuppressWarnings("unchecked") initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options)372 public void initialize(Subject subject, CallbackHandler callbackHandler, 373 Map<String, ?> sharedState, Map<String, ?> options) { 374 375 this.subject = subject; 376 this.callbackHandler = callbackHandler; 377 this.sharedState = (Map<String, Object>)sharedState; 378 this.options = options; 379 380 ldapEnvironment = new Hashtable<String, Object>(9); 381 ldapEnvironment.put(Context.INITIAL_CONTEXT_FACTORY, 382 "com.sun.jndi.ldap.LdapCtxFactory"); 383 384 // Add any JNDI properties to the environment 385 for (String key : options.keySet()) { 386 if (key.indexOf('.') > -1) { 387 ldapEnvironment.put(key, options.get(key)); 388 } 389 } 390 391 // initialize any configured options 392 393 userProvider = (String)options.get(USER_PROVIDER); 394 if (userProvider != null) { 395 ldapEnvironment.put(Context.PROVIDER_URL, userProvider); 396 } 397 398 authcIdentity = (String)options.get(AUTHC_IDENTITY); 399 if (authcIdentity != null && 400 (authcIdentity.indexOf(USERNAME_TOKEN) != -1)) { 401 identityMatcher = USERNAME_PATTERN.matcher(authcIdentity); 402 } 403 404 userFilter = (String)options.get(USER_FILTER); 405 if (userFilter != null) { 406 if (userFilter.indexOf(USERNAME_TOKEN) != -1) { 407 filterMatcher = USERNAME_PATTERN.matcher(userFilter); 408 } 409 constraints = new SearchControls(); 410 constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); 411 constraints.setReturningAttributes(new String[0]); //return no attrs 412 } 413 414 authzIdentity = (String)options.get(AUTHZ_IDENTITY); 415 if (authzIdentity != null && 416 authzIdentity.startsWith("{") && authzIdentity.endsWith("}")) { 417 if (constraints != null) { 418 authzIdentityAttr = 419 authzIdentity.substring(1, authzIdentity.length() - 1); 420 constraints.setReturningAttributes( 421 new String[]{authzIdentityAttr}); 422 } 423 authzIdentity = null; // set later, from the specified attribute 424 } 425 426 // determine mode 427 if (authcIdentity != null) { 428 if (userFilter != null) { 429 authFirst = true; // authentication-first mode 430 } else { 431 authOnly = true; // authentication-only mode 432 } 433 } 434 435 if ("false".equalsIgnoreCase((String)options.get("useSSL"))) { 436 useSSL = false; 437 ldapEnvironment.remove(Context.SECURITY_PROTOCOL); 438 } else { 439 ldapEnvironment.put(Context.SECURITY_PROTOCOL, "ssl"); 440 } 441 442 tryFirstPass = 443 "true".equalsIgnoreCase((String)options.get("tryFirstPass")); 444 445 useFirstPass = 446 "true".equalsIgnoreCase((String)options.get("useFirstPass")); 447 448 storePass = "true".equalsIgnoreCase((String)options.get("storePass")); 449 450 clearPass = "true".equalsIgnoreCase((String)options.get("clearPass")); 451 452 debug = "true".equalsIgnoreCase((String)options.get("debug")); 453 454 if (debug) { 455 if (authFirst) { 456 System.out.println("\t\t[LdapLoginModule] " + 457 "authentication-first mode; " + 458 (useSSL ? "SSL enabled" : "SSL disabled")); 459 } else if (authOnly) { 460 System.out.println("\t\t[LdapLoginModule] " + 461 "authentication-only mode; " + 462 (useSSL ? "SSL enabled" : "SSL disabled")); 463 } else { 464 System.out.println("\t\t[LdapLoginModule] " + 465 "search-first mode; " + 466 (useSSL ? "SSL enabled" : "SSL disabled")); 467 } 468 } 469 } 470 471 /** 472 * Begin user authentication. 473 * 474 * <p> Acquire the user's credentials and verify them against the 475 * specified LDAP directory. 476 * 477 * @return true always, since this {@code LoginModule} 478 * should not be ignored. 479 * @exception FailedLoginException if the authentication fails. 480 * @exception LoginException if this {@code LoginModule} 481 * is unable to perform the authentication. 482 */ login()483 public boolean login() throws LoginException { 484 485 if (userProvider == null) { 486 throw new LoginException 487 ("Unable to locate the LDAP directory service"); 488 } 489 490 if (debug) { 491 System.out.println("\t\t[LdapLoginModule] user provider: " + 492 userProvider); 493 } 494 495 // attempt the authentication 496 if (tryFirstPass) { 497 498 try { 499 // attempt the authentication by getting the 500 // username and password from shared state 501 attemptAuthentication(true); 502 503 // authentication succeeded 504 succeeded = true; 505 if (debug) { 506 System.out.println("\t\t[LdapLoginModule] " + 507 "tryFirstPass succeeded"); 508 } 509 return true; 510 511 } catch (LoginException le) { 512 // authentication failed -- try again below by prompting 513 cleanState(); 514 if (debug) { 515 System.out.println("\t\t[LdapLoginModule] " + 516 "tryFirstPass failed: " + le.toString()); 517 } 518 } 519 520 } else if (useFirstPass) { 521 522 try { 523 // attempt the authentication by getting the 524 // username and password from shared state 525 attemptAuthentication(true); 526 527 // authentication succeeded 528 succeeded = true; 529 if (debug) { 530 System.out.println("\t\t[LdapLoginModule] " + 531 "useFirstPass succeeded"); 532 } 533 return true; 534 535 } catch (LoginException le) { 536 // authentication failed 537 cleanState(); 538 if (debug) { 539 System.out.println("\t\t[LdapLoginModule] " + 540 "useFirstPass failed"); 541 } 542 throw le; 543 } 544 } 545 546 // attempt the authentication by prompting for the username and pwd 547 try { 548 attemptAuthentication(false); 549 550 // authentication succeeded 551 succeeded = true; 552 if (debug) { 553 System.out.println("\t\t[LdapLoginModule] " + 554 "authentication succeeded"); 555 } 556 return true; 557 558 } catch (LoginException le) { 559 cleanState(); 560 if (debug) { 561 System.out.println("\t\t[LdapLoginModule] " + 562 "authentication failed"); 563 } 564 throw le; 565 } 566 } 567 568 /** 569 * Complete user authentication. 570 * 571 * <p> This method is called if the LoginContext's 572 * overall authentication succeeded 573 * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules 574 * succeeded). 575 * 576 * <p> If this LoginModule's own authentication attempt 577 * succeeded (checked by retrieving the private state saved by the 578 * {@code login} method), then this method associates an 579 * {@code LdapPrincipal} and one or more {@code UserPrincipal}s 580 * with the {@code Subject} located in the 581 * {@code LoginModule}. If this LoginModule's own 582 * authentication attempted failed, then this method removes 583 * any state that was originally saved. 584 * 585 * @exception LoginException if the commit fails 586 * @return true if this LoginModule's own login and commit 587 * attempts succeeded, or false otherwise. 588 */ commit()589 public boolean commit() throws LoginException { 590 591 if (succeeded == false) { 592 return false; 593 } else { 594 if (subject.isReadOnly()) { 595 cleanState(); 596 throw new LoginException ("Subject is read-only"); 597 } 598 // add Principals to the Subject 599 Set<Principal> principals = subject.getPrincipals(); 600 if (! principals.contains(ldapPrincipal)) { 601 principals.add(ldapPrincipal); 602 } 603 if (debug) { 604 System.out.println("\t\t[LdapLoginModule] " + 605 "added LdapPrincipal \"" + 606 ldapPrincipal + 607 "\" to Subject"); 608 } 609 610 if (! principals.contains(userPrincipal)) { 611 principals.add(userPrincipal); 612 } 613 if (debug) { 614 System.out.println("\t\t[LdapLoginModule] " + 615 "added UserPrincipal \"" + 616 userPrincipal + 617 "\" to Subject"); 618 } 619 620 if (authzPrincipal != null && 621 (! principals.contains(authzPrincipal))) { 622 principals.add(authzPrincipal); 623 624 if (debug) { 625 System.out.println("\t\t[LdapLoginModule] " + 626 "added UserPrincipal \"" + 627 authzPrincipal + 628 "\" to Subject"); 629 } 630 } 631 } 632 // in any case, clean out state 633 cleanState(); 634 commitSucceeded = true; 635 return true; 636 } 637 638 /** 639 * Abort user authentication. 640 * 641 * <p> This method is called if the overall authentication failed. 642 * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules 643 * did not succeed). 644 * 645 * <p> If this LoginModule's own authentication attempt 646 * succeeded (checked by retrieving the private state saved by the 647 * {@code login} and {@code commit} methods), 648 * then this method cleans up any state that was originally saved. 649 * 650 * @exception LoginException if the abort fails. 651 * @return false if this LoginModule's own login and/or commit attempts 652 * failed, and true otherwise. 653 */ abort()654 public boolean abort() throws LoginException { 655 if (debug) 656 System.out.println("\t\t[LdapLoginModule] " + 657 "aborted authentication"); 658 659 if (succeeded == false) { 660 return false; 661 } else if (succeeded == true && commitSucceeded == false) { 662 663 // Clean out state 664 succeeded = false; 665 cleanState(); 666 667 ldapPrincipal = null; 668 userPrincipal = null; 669 authzPrincipal = null; 670 } else { 671 // overall authentication succeeded and commit succeeded, 672 // but someone else's commit failed 673 logout(); 674 } 675 return true; 676 } 677 678 /** 679 * Logout a user. 680 * 681 * <p> This method removes the Principals 682 * that were added by the {@code commit} method. 683 * 684 * @exception LoginException if the logout fails. 685 * @return true in all cases since this {@code LoginModule} 686 * should not be ignored. 687 */ logout()688 public boolean logout() throws LoginException { 689 if (subject.isReadOnly()) { 690 cleanState(); 691 throw new LoginException ("Subject is read-only"); 692 } 693 Set<Principal> principals = subject.getPrincipals(); 694 principals.remove(ldapPrincipal); 695 principals.remove(userPrincipal); 696 if (authzIdentity != null) { 697 principals.remove(authzPrincipal); 698 } 699 700 // clean out state 701 cleanState(); 702 succeeded = false; 703 commitSucceeded = false; 704 705 ldapPrincipal = null; 706 userPrincipal = null; 707 authzPrincipal = null; 708 709 if (debug) { 710 System.out.println("\t\t[LdapLoginModule] logged out Subject"); 711 } 712 return true; 713 } 714 715 /** 716 * Attempt authentication 717 * 718 * @param getPasswdFromSharedState boolean that tells this method whether 719 * to retrieve the password from the sharedState. 720 * @exception LoginException if the authentication attempt fails. 721 */ attemptAuthentication(boolean getPasswdFromSharedState)722 private void attemptAuthentication(boolean getPasswdFromSharedState) 723 throws LoginException { 724 725 // first get the username and password 726 getUsernamePassword(getPasswdFromSharedState); 727 728 if (password == null || password.length == 0) { 729 throw (LoginException) 730 new FailedLoginException("No password was supplied"); 731 } 732 733 String dn = ""; 734 735 if (authFirst || authOnly) { 736 737 String id = 738 replaceUsernameToken(identityMatcher, authcIdentity, username); 739 740 // Prepare to bind using user's username and password 741 ldapEnvironment.put(Context.SECURITY_CREDENTIALS, password); 742 ldapEnvironment.put(Context.SECURITY_PRINCIPAL, id); 743 744 if (debug) { 745 System.out.println("\t\t[LdapLoginModule] " + 746 "attempting to authenticate user: " + username); 747 } 748 749 try { 750 // Connect to the LDAP server (using simple bind) 751 ctx = new InitialLdapContext(ldapEnvironment, null); 752 753 } catch (NamingException e) { 754 throw (LoginException) 755 new FailedLoginException("Cannot bind to LDAP server") 756 .initCause(e); 757 } 758 759 // Authentication has succeeded 760 761 // Locate the user's distinguished name 762 if (userFilter != null) { 763 dn = findUserDN(ctx); 764 } else { 765 dn = id; 766 } 767 768 } else { 769 770 try { 771 // Connect to the LDAP server (using anonymous bind) 772 ctx = new InitialLdapContext(ldapEnvironment, null); 773 774 } catch (NamingException e) { 775 throw (LoginException) 776 new FailedLoginException("Cannot connect to LDAP server") 777 .initCause(e); 778 } 779 780 // Locate the user's distinguished name 781 dn = findUserDN(ctx); 782 783 try { 784 785 // Prepare to bind using user's distinguished name and password 786 ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple"); 787 ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, dn); 788 ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password); 789 790 if (debug) { 791 System.out.println("\t\t[LdapLoginModule] " + 792 "attempting to authenticate user: " + username); 793 } 794 // Connect to the LDAP server (using simple bind) 795 ctx.reconnect(null); 796 797 // Authentication has succeeded 798 799 } catch (NamingException e) { 800 throw (LoginException) 801 new FailedLoginException("Cannot bind to LDAP server") 802 .initCause(e); 803 } 804 } 805 806 // Save input as shared state only if authentication succeeded 807 if (storePass && 808 !sharedState.containsKey(USERNAME_KEY) && 809 !sharedState.containsKey(PASSWORD_KEY)) { 810 sharedState.put(USERNAME_KEY, username); 811 sharedState.put(PASSWORD_KEY, password); 812 } 813 814 // Create the user principals 815 userPrincipal = new UserPrincipal(username); 816 if (authzIdentity != null) { 817 authzPrincipal = new UserPrincipal(authzIdentity); 818 } 819 820 try { 821 822 ldapPrincipal = new LdapPrincipal(dn); 823 824 } catch (InvalidNameException e) { 825 if (debug) { 826 System.out.println("\t\t[LdapLoginModule] " + 827 "cannot create LdapPrincipal: bad DN"); 828 } 829 throw (LoginException) 830 new FailedLoginException("Cannot create LdapPrincipal") 831 .initCause(e); 832 } 833 } 834 835 /** 836 * Search for the user's entry. 837 * Determine the distinguished name of the user's entry and optionally 838 * an authorization identity for the user. 839 * 840 * @param ctx an LDAP context to use for the search 841 * @return the user's distinguished name or an empty string if none 842 * was found. 843 * @exception LoginException if the user's entry cannot be found. 844 */ findUserDN(LdapContext ctx)845 private String findUserDN(LdapContext ctx) throws LoginException { 846 847 String userDN = ""; 848 849 // Locate the user's LDAP entry 850 if (userFilter != null) { 851 if (debug) { 852 System.out.println("\t\t[LdapLoginModule] " + 853 "searching for entry belonging to user: " + username); 854 } 855 } else { 856 if (debug) { 857 System.out.println("\t\t[LdapLoginModule] " + 858 "cannot search for entry belonging to user: " + username); 859 } 860 throw (LoginException) 861 new FailedLoginException("Cannot find user's LDAP entry"); 862 } 863 864 try { 865 // Sanitize username and substitute into LDAP filter 866 String canonicalUserFilter = 867 replaceUsernameToken(filterMatcher, userFilter, 868 escapeUsernameChars()); 869 870 NamingEnumeration<SearchResult> results = 871 ctx.search("", canonicalUserFilter, constraints); 872 873 // Extract the distinguished name of the user's entry 874 // (Use the first entry if more than one is returned) 875 if (results.hasMore()) { 876 SearchResult entry = results.next(); 877 userDN = entry.getNameInNamespace(); 878 879 if (debug) { 880 System.out.println("\t\t[LdapLoginModule] found entry: " + 881 userDN); 882 } 883 884 // Extract a value from user's authorization identity attribute 885 if (authzIdentityAttr != null) { 886 Attribute attr = 887 entry.getAttributes().get(authzIdentityAttr); 888 if (attr != null) { 889 Object val = attr.get(); 890 if (val instanceof String) { 891 authzIdentity = (String) val; 892 } 893 } 894 } 895 896 results.close(); 897 898 } else { 899 // Bad username 900 if (debug) { 901 System.out.println("\t\t[LdapLoginModule] user's entry " + 902 "not found"); 903 } 904 } 905 906 } catch (NamingException e) { 907 // ignore 908 } 909 910 if (userDN.equals("")) { 911 throw (LoginException) 912 new FailedLoginException("Cannot find user's LDAP entry"); 913 } else { 914 return userDN; 915 } 916 } 917 918 /** 919 * Modify the supplied username to encode characters that must be escaped 920 * according to RFC 4515: LDAP: String Representation of Search Filters. 921 * 922 * The following characters are encoded as a backslash "\" (ASCII 0x5c) 923 * followed by the two hexadecimal digits representing the value of the 924 * escaped character: 925 * '*' (ASCII 0x2a) 926 * '(' (ASCII 0x28) 927 * ')' (ASCII 0x29) 928 * '\' (ASCII 0x5c) 929 * '\0'(ASCII 0x00) 930 * 931 * @return the modified username with its characters escaped as needed 932 */ escapeUsernameChars()933 private String escapeUsernameChars() { 934 int len = username.length(); 935 StringBuilder escapedUsername = new StringBuilder(len + 16); 936 937 for (int i = 0; i < len; i++) { 938 char c = username.charAt(i); 939 switch (c) { 940 case '*': 941 escapedUsername.append("\\\\2A"); 942 break; 943 case '(': 944 escapedUsername.append("\\\\28"); 945 break; 946 case ')': 947 escapedUsername.append("\\\\29"); 948 break; 949 case '\\': 950 escapedUsername.append("\\\\5C"); 951 break; 952 case '\0': 953 escapedUsername.append("\\\\00"); 954 break; 955 default: 956 escapedUsername.append(c); 957 } 958 } 959 960 return escapedUsername.toString(); 961 } 962 963 964 /** 965 * Replace the username token 966 * 967 * @param matcher the replacement pattern 968 * @param string the target string 969 * @param username the supplied username 970 * @return the modified string 971 */ replaceUsernameToken(Matcher matcher, String string, String username)972 private String replaceUsernameToken(Matcher matcher, String string, 973 String username) { 974 return matcher != null ? matcher.replaceAll(username) : string; 975 } 976 977 /** 978 * Get the username and password. 979 * This method does not return any value. 980 * Instead, it sets global name and password variables. 981 * 982 * <p> Also note that this method will set the username and password 983 * values in the shared state in case subsequent LoginModules 984 * want to use them via use/tryFirstPass. 985 * 986 * @param getPasswdFromSharedState boolean that tells this method whether 987 * to retrieve the password from the sharedState. 988 * @exception LoginException if the username/password cannot be acquired. 989 */ getUsernamePassword(boolean getPasswdFromSharedState)990 private void getUsernamePassword(boolean getPasswdFromSharedState) 991 throws LoginException { 992 993 if (getPasswdFromSharedState) { 994 // use the password saved by the first module in the stack 995 username = (String)sharedState.get(USERNAME_KEY); 996 password = (char[])sharedState.get(PASSWORD_KEY); 997 return; 998 } 999 1000 // prompt for a username and password 1001 if (callbackHandler == null) 1002 throw new LoginException("No CallbackHandler available " + 1003 "to acquire authentication information from the user"); 1004 1005 Callback[] callbacks = new Callback[2]; 1006 callbacks[0] = new NameCallback(getAuthResourceString("username.")); 1007 callbacks[1] = new PasswordCallback(getAuthResourceString("password."), false); 1008 1009 try { 1010 callbackHandler.handle(callbacks); 1011 username = ((NameCallback)callbacks[0]).getName(); 1012 char[] tmpPassword = ((PasswordCallback)callbacks[1]).getPassword(); 1013 password = new char[tmpPassword.length]; 1014 System.arraycopy(tmpPassword, 0, 1015 password, 0, tmpPassword.length); 1016 ((PasswordCallback)callbacks[1]).clearPassword(); 1017 1018 } catch (java.io.IOException ioe) { 1019 throw new LoginException(ioe.toString()); 1020 1021 } catch (UnsupportedCallbackException uce) { 1022 throw new LoginException("Error: " + uce.getCallback().toString() + 1023 " not available to acquire authentication information" + 1024 " from the user"); 1025 } 1026 } 1027 1028 /** 1029 * Clean out state because of a failed authentication attempt 1030 */ cleanState()1031 private void cleanState() { 1032 username = null; 1033 if (password != null) { 1034 Arrays.fill(password, ' '); 1035 password = null; 1036 } 1037 try { 1038 if (ctx != null) { 1039 ctx.close(); 1040 } 1041 } catch (NamingException e) { 1042 // ignore 1043 } 1044 ctx = null; 1045 1046 if (clearPass) { 1047 sharedState.remove(USERNAME_KEY); 1048 sharedState.remove(PASSWORD_KEY); 1049 } 1050 } 1051 } 1052