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