1 /* 2 * Copyright (c) 2005, 2013, 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 associated with 157 * the current {@link Subject}. </dd> 158 * 159 * <dt> <code>useSSL</code> </dt> 160 * <dd> if <code>false</code>, this module does not establish an SSL connection 161 * to the LDAP server before attempting authentication. SSL is used to 162 * protect the privacy of the user's password because it is transmitted 163 * in the clear over LDAP. 164 * By default, this module uses SSL. </dd> 165 * 166 * <dt> <code>useFirstPass</code> </dt> 167 * <dd> if <code>true</code>, this module retrieves the username and password 168 * from the module's shared state, using "javax.security.auth.login.name" 169 * and "javax.security.auth.login.password" as the respective keys. The 170 * retrieved values are used for authentication. If authentication fails, 171 * no attempt for a retry is made, and the failure is reported back to 172 * the calling application.</dd> 173 * 174 * <dt> <code>tryFirstPass</code> </dt> 175 * <dd> if <code>true</code>, this module retrieves the username and password 176 * from the module's shared state, using "javax.security.auth.login.name" 177 * and "javax.security.auth.login.password" as the respective keys. The 178 * retrieved values are used for authentication. If authentication fails, 179 * the module uses the {@link CallbackHandler} to retrieve a new username 180 * and password, and another attempt to authenticate is made. If the 181 * authentication fails, the failure is reported back to the calling 182 * application.</dd> 183 * 184 * <dt> <code>storePass</code> </dt> 185 * <dd> if <code>true</code>, this module stores the username and password 186 * obtained from the {@link CallbackHandler} in the module's shared state, 187 * using 188 * "javax.security.auth.login.name" and 189 * "javax.security.auth.login.password" as the respective keys. This is 190 * not performed if existing values already exist for the username and 191 * password in the shared state, or if authentication fails.</dd> 192 * 193 * <dt> <code>clearPass</code> </dt> 194 * <dd> if <code>true</code>, this module clears the username and password 195 * stored in the module's shared state after both phases of authentication 196 * (login and commit) have completed.</dd> 197 * 198 * <dt> <code>debug</code> </dt> 199 * <dd> if <code>true</code>, debug messages are displayed on the standard 200 * output stream. 201 * </dl> 202 * </dl> 203 * 204 * <p> 205 * Arbitrary 206 * <a href="{@docRoot}/../../../../../technotes/guides/jndi/jndi-ldap-gl.html#PROP">JNDI properties</a> 207 * may also be specified in the {@link Configuration}. 208 * They are added to the environment and passed to the LDAP provider. 209 * Note that the following four JNDI properties are set by this module directly 210 * and are ignored if also present in the configuration: 211 * <ul> 212 * <li> <code>java.naming.provider.url</code> 213 * <li> <code>java.naming.security.principal</code> 214 * <li> <code>java.naming.security.credentials</code> 215 * <li> <code>java.naming.security.protocol</code> 216 * </ul> 217 * 218 * <p> 219 * Three sample {@link Configuration}s are shown below. 220 * The first one activates search-first mode. It identifies the LDAP server 221 * and specifies that users' entries be located by their <code>uid</code> and 222 * <code>objectClass</code> attributes. It also specifies that an identity 223 * based on the user's <code>employeeNumber</code> attribute should be created. 224 * The second one activates authentication-first mode. It requests that the 225 * LDAP server be located dynamically, that authentication be performed using 226 * the supplied username directly but without the protection of SSL and that 227 * users' entries be located by one of three naming attributes and their 228 * <code>objectClass</code> attribute. 229 * The third one activates authentication-only mode. It identifies alternative 230 * LDAP servers, it specifies the distinguished name to use for 231 * authentication and a fixed identity to use for authorization. No directory 232 * search is performed. 233 * 234 * <pre> 235 * 236 * ExampleApplication { 237 * com.sun.security.auth.module.LdapLoginModule REQUIRED 238 * userProvider="ldap://ldap-svr/ou=people,dc=example,dc=com" 239 * userFilter="(&(uid={USERNAME})(objectClass=inetOrgPerson))" 240 * authzIdentity="{EMPLOYEENUMBER}" 241 * debug=true; 242 * }; 243 * 244 * ExampleApplication { 245 * com.sun.security.auth.module.LdapLoginModule REQUIRED 246 * userProvider="ldap:///cn=users,dc=example,dc=com" 247 * authIdentity="{USERNAME}" 248 * userFilter="(&(|(samAccountName={USERNAME})(userPrincipalName={USERNAME})(cn={USERNAME}))(objectClass=user))" 249 * useSSL=false 250 * debug=true; 251 * }; 252 * 253 * ExampleApplication { 254 * com.sun.security.auth.module.LdapLoginModule REQUIRED 255 * userProvider="ldap://ldap-svr1 ldap://ldap-svr2" 256 * authIdentity="cn={USERNAME},ou=people,dc=example,dc=com" 257 * authzIdentity="staff" 258 * debug=true; 259 * }; 260 * 261 * </pre> 262 * 263 * <dl> 264 * <dt><b>Note:</b> </dt> 265 * <dd>When a {@link SecurityManager} is active then an application 266 * that creates a {@link LoginContext} and uses a {@link LoginModule} 267 * must be granted certain permissions. 268 * <p> 269 * If the application creates a login context using an <em>installed</em> 270 * {@link Configuration} then the application must be granted the 271 * {@link AuthPermission} to create login contexts. 272 * For example, the following security policy allows an application in 273 * the user's current directory to instantiate <em>any</em> login context: 274 * <pre> 275 * 276 * grant codebase "file:${user.dir}/" { 277 * permission javax.security.auth.AuthPermission "createLoginContext.*"; 278 * }; 279 * </pre> 280 * 281 * Alternatively, if the application creates a login context using a 282 * <em>caller-specified</em> {@link Configuration} then the application 283 * must be granted the permissions required by the {@link LoginModule}. 284 * <em>This</em> module requires the following two permissions: 285 * <p> 286 * <ul> 287 * <li> The {@link SocketPermission} to connect to an LDAP server. 288 * <li> The {@link AuthPermission} to modify the set of {@link Principal}s 289 * associated with a {@link Subject}. 290 * </ul> 291 * <p> 292 * For example, the following security policy grants an application in the 293 * user's current directory all the permissions required by this module: 294 * <pre> 295 * 296 * grant codebase "file:${user.dir}/" { 297 * permission java.net.SocketPermission "*:389", "connect"; 298 * permission java.net.SocketPermission "*:636", "connect"; 299 * permission javax.security.auth.AuthPermission "modifyPrincipals"; 300 * }; 301 * </pre> 302 * </dd> 303 * </dl> 304 * 305 * @since 1.6 306 */ 307 @jdk.Exported 308 public class LdapLoginModule implements LoginModule { 309 310 // Use the default classloader for this class to load the prompt strings. 311 private static final ResourceBundle rb = AccessController.doPrivileged( 312 new PrivilegedAction<ResourceBundle>() { 313 public ResourceBundle run() { 314 return ResourceBundle.getBundle( 315 "sun.security.util.AuthResources"); 316 } 317 } 318 ); 319 320 // Keys to retrieve the stored username and password 321 private static final String USERNAME_KEY = "javax.security.auth.login.name"; 322 private static final String PASSWORD_KEY = 323 "javax.security.auth.login.password"; 324 325 // Option names 326 private static final String USER_PROVIDER = "userProvider"; 327 private static final String USER_FILTER = "userFilter"; 328 private static final String AUTHC_IDENTITY = "authIdentity"; 329 private static final String AUTHZ_IDENTITY = "authzIdentity"; 330 331 // Used for the username token replacement 332 private static final String USERNAME_TOKEN = "{USERNAME}"; 333 private static final Pattern USERNAME_PATTERN = 334 Pattern.compile("\\{USERNAME\\}"); 335 336 // Configurable options 337 private String userProvider; 338 private String userFilter; 339 private String authcIdentity; 340 private String authzIdentity; 341 private String authzIdentityAttr = null; 342 private boolean useSSL = true; 343 private boolean authFirst = false; 344 private boolean authOnly = false; 345 private boolean useFirstPass = false; 346 private boolean tryFirstPass = false; 347 private boolean storePass = false; 348 private boolean clearPass = false; 349 private boolean debug = false; 350 351 // Authentication status 352 private boolean succeeded = false; 353 private boolean commitSucceeded = false; 354 355 // Supplied username and password 356 private String username; 357 private char[] password; 358 359 // User's identities 360 private LdapPrincipal ldapPrincipal; 361 private UserPrincipal userPrincipal; 362 private UserPrincipal authzPrincipal; 363 364 // Initial state 365 private Subject subject; 366 private CallbackHandler callbackHandler; 367 private Map<String, Object> sharedState; 368 private Map<String, ?> options; 369 private LdapContext ctx; 370 private Matcher identityMatcher = null; 371 private Matcher filterMatcher = null; 372 private Hashtable<String, Object> ldapEnvironment; 373 private SearchControls constraints = null; 374 375 /** 376 * Initialize this <code>LoginModule</code>. 377 * 378 * @param subject the <code>Subject</code> to be authenticated. 379 * @param callbackHandler a <code>CallbackHandler</code> to acquire the 380 * username and password. 381 * @param sharedState shared <code>LoginModule</code> state. 382 * @param options options specified in the login 383 * <code>Configuration</code> for this particular 384 * <code>LoginModule</code>. 385 */ 386 // Unchecked warning from (Map<String, Object>)sharedState is safe 387 // since javax.security.auth.login.LoginContext passes a raw HashMap. 388 @SuppressWarnings("unchecked") initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options)389 public void initialize(Subject subject, CallbackHandler callbackHandler, 390 Map<String, ?> sharedState, Map<String, ?> options) { 391 392 this.subject = subject; 393 this.callbackHandler = callbackHandler; 394 this.sharedState = (Map<String, Object>)sharedState; 395 this.options = options; 396 397 ldapEnvironment = new Hashtable<String, Object>(9); 398 ldapEnvironment.put(Context.INITIAL_CONTEXT_FACTORY, 399 "com.sun.jndi.ldap.LdapCtxFactory"); 400 401 // Add any JNDI properties to the environment 402 for (String key : options.keySet()) { 403 if (key.indexOf(".") > -1) { 404 ldapEnvironment.put(key, options.get(key)); 405 } 406 } 407 408 // initialize any configured options 409 410 userProvider = (String)options.get(USER_PROVIDER); 411 if (userProvider != null) { 412 ldapEnvironment.put(Context.PROVIDER_URL, userProvider); 413 } 414 415 authcIdentity = (String)options.get(AUTHC_IDENTITY); 416 if (authcIdentity != null && 417 (authcIdentity.indexOf(USERNAME_TOKEN) != -1)) { 418 identityMatcher = USERNAME_PATTERN.matcher(authcIdentity); 419 } 420 421 userFilter = (String)options.get(USER_FILTER); 422 if (userFilter != null) { 423 if (userFilter.indexOf(USERNAME_TOKEN) != -1) { 424 filterMatcher = USERNAME_PATTERN.matcher(userFilter); 425 } 426 constraints = new SearchControls(); 427 constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); 428 constraints.setReturningAttributes(new String[0]); //return no attrs 429 } 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 */ login()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 */ commit()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 */ abort()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 */ logout()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 */ attemptAuthentication(boolean getPasswdFromSharedState)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 = 755 replaceUsernameToken(identityMatcher, authcIdentity, username); 756 757 // Prepare to bind using user's username and password 758 ldapEnvironment.put(Context.SECURITY_CREDENTIALS, password); 759 ldapEnvironment.put(Context.SECURITY_PRINCIPAL, id); 760 761 if (debug) { 762 System.out.println("\t\t[LdapLoginModule] " + 763 "attempting to authenticate user: " + username); 764 } 765 766 try { 767 // Connect to the LDAP server (using simple bind) 768 ctx = new InitialLdapContext(ldapEnvironment, null); 769 770 } catch (NamingException e) { 771 throw (LoginException) 772 new FailedLoginException("Cannot bind to LDAP server") 773 .initCause(e); 774 } 775 776 // Authentication has succeeded 777 778 // Locate the user's distinguished name 779 if (userFilter != null) { 780 dn = findUserDN(ctx); 781 } else { 782 dn = id; 783 } 784 785 } else { 786 787 try { 788 // Connect to the LDAP server (using anonymous bind) 789 ctx = new InitialLdapContext(ldapEnvironment, null); 790 791 } catch (NamingException e) { 792 throw (LoginException) 793 new FailedLoginException("Cannot connect to LDAP server") 794 .initCause(e); 795 } 796 797 // Locate the user's distinguished name 798 dn = findUserDN(ctx); 799 800 try { 801 802 // Prepare to bind using user's distinguished name and password 803 ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple"); 804 ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, dn); 805 ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password); 806 807 if (debug) { 808 System.out.println("\t\t[LdapLoginModule] " + 809 "attempting to authenticate user: " + username); 810 } 811 // Connect to the LDAP server (using simple bind) 812 ctx.reconnect(null); 813 814 // Authentication has succeeded 815 816 } catch (NamingException e) { 817 throw (LoginException) 818 new FailedLoginException("Cannot bind to LDAP server") 819 .initCause(e); 820 } 821 } 822 823 // Save input as shared state only if authentication succeeded 824 if (storePass && 825 !sharedState.containsKey(USERNAME_KEY) && 826 !sharedState.containsKey(PASSWORD_KEY)) { 827 sharedState.put(USERNAME_KEY, username); 828 sharedState.put(PASSWORD_KEY, password); 829 } 830 831 // Create the user principals 832 userPrincipal = new UserPrincipal(username); 833 if (authzIdentity != null) { 834 authzPrincipal = new UserPrincipal(authzIdentity); 835 } 836 837 try { 838 839 ldapPrincipal = new LdapPrincipal(dn); 840 841 } catch (InvalidNameException e) { 842 if (debug) { 843 System.out.println("\t\t[LdapLoginModule] " + 844 "cannot create LdapPrincipal: bad DN"); 845 } 846 throw (LoginException) 847 new FailedLoginException("Cannot create LdapPrincipal") 848 .initCause(e); 849 } 850 } 851 852 /** 853 * Search for the user's entry. 854 * Determine the distinguished name of the user's entry and optionally 855 * an authorization identity for the user. 856 * 857 * @param ctx an LDAP context to use for the search 858 * @return the user's distinguished name or an empty string if none 859 * was found. 860 * @exception LoginException if the user's entry cannot be found. 861 */ findUserDN(LdapContext ctx)862 private String findUserDN(LdapContext ctx) throws LoginException { 863 864 String userDN = ""; 865 866 // Locate the user's LDAP entry 867 if (userFilter != null) { 868 if (debug) { 869 System.out.println("\t\t[LdapLoginModule] " + 870 "searching for entry belonging to user: " + username); 871 } 872 } else { 873 if (debug) { 874 System.out.println("\t\t[LdapLoginModule] " + 875 "cannot search for entry belonging to user: " + username); 876 } 877 throw (LoginException) 878 new FailedLoginException("Cannot find user's LDAP entry"); 879 } 880 881 try { 882 // Sanitize username and substitute into LDAP filter 883 String canonicalUserFilter = 884 replaceUsernameToken(filterMatcher, userFilter, 885 escapeUsernameChars()); 886 887 NamingEnumeration<SearchResult> results = 888 ctx.search("", canonicalUserFilter, constraints); 889 890 // Extract the distinguished name of the user's entry 891 // (Use the first entry if more than one is returned) 892 if (results.hasMore()) { 893 SearchResult entry = results.next(); 894 userDN = entry.getNameInNamespace(); 895 896 if (debug) { 897 System.out.println("\t\t[LdapLoginModule] found entry: " + 898 userDN); 899 } 900 901 // Extract a value from user's authorization identity attribute 902 if (authzIdentityAttr != null) { 903 Attribute attr = 904 entry.getAttributes().get(authzIdentityAttr); 905 if (attr != null) { 906 Object val = attr.get(); 907 if (val instanceof String) { 908 authzIdentity = (String) val; 909 } 910 } 911 } 912 913 results.close(); 914 915 } else { 916 // Bad username 917 if (debug) { 918 System.out.println("\t\t[LdapLoginModule] user's entry " + 919 "not found"); 920 } 921 } 922 923 } catch (NamingException e) { 924 // ignore 925 } 926 927 if (userDN.equals("")) { 928 throw (LoginException) 929 new FailedLoginException("Cannot find user's LDAP entry"); 930 } else { 931 return userDN; 932 } 933 } 934 935 /** 936 * Modify the supplied username to encode characters that must be escaped 937 * according to RFC 4515: LDAP: String Representation of Search Filters. 938 * 939 * The following characters are encoded as a backslash "\" (ASCII 0x5c) 940 * followed by the two hexadecimal digits representing the value of the 941 * escaped character: 942 * '*' (ASCII 0x2a) 943 * '(' (ASCII 0x28) 944 * ')' (ASCII 0x29) 945 * '\' (ASCII 0x5c) 946 * '\0'(ASCII 0x00) 947 * 948 * @return the modified username with its characters escaped as needed 949 */ escapeUsernameChars()950 private String escapeUsernameChars() { 951 int len = username.length(); 952 StringBuilder escapedUsername = new StringBuilder(len + 16); 953 954 for (int i = 0; i < len; i++) { 955 char c = username.charAt(i); 956 switch (c) { 957 case '*': 958 escapedUsername.append("\\\\2A"); 959 break; 960 case '(': 961 escapedUsername.append("\\\\28"); 962 break; 963 case ')': 964 escapedUsername.append("\\\\29"); 965 break; 966 case '\\': 967 escapedUsername.append("\\\\5C"); 968 break; 969 case '\0': 970 escapedUsername.append("\\\\00"); 971 break; 972 default: 973 escapedUsername.append(c); 974 } 975 } 976 977 return escapedUsername.toString(); 978 } 979 980 981 /** 982 * Replace the username token 983 * 984 * @param matcher the replacement pattern 985 * @param string the target string 986 * @param username the supplied username 987 * @return the modified string 988 */ replaceUsernameToken(Matcher matcher, String string, String username)989 private String replaceUsernameToken(Matcher matcher, String string, 990 String username) { 991 return matcher != null ? matcher.replaceAll(username) : string; 992 } 993 994 /** 995 * Get the username and password. 996 * This method does not return any value. 997 * Instead, it sets global name and password variables. 998 * 999 * <p> Also note that this method will set the username and password 1000 * values in the shared state in case subsequent LoginModules 1001 * want to use them via use/tryFirstPass. 1002 * 1003 * @param getPasswdFromSharedState boolean that tells this method whether 1004 * to retrieve the password from the sharedState. 1005 * @exception LoginException if the username/password cannot be acquired. 1006 */ getUsernamePassword(boolean getPasswdFromSharedState)1007 private void getUsernamePassword(boolean getPasswdFromSharedState) 1008 throws LoginException { 1009 1010 if (getPasswdFromSharedState) { 1011 // use the password saved by the first module in the stack 1012 username = (String)sharedState.get(USERNAME_KEY); 1013 password = (char[])sharedState.get(PASSWORD_KEY); 1014 return; 1015 } 1016 1017 // prompt for a username and password 1018 if (callbackHandler == null) 1019 throw new LoginException("No CallbackHandler available " + 1020 "to acquire authentication information from the user"); 1021 1022 Callback[] callbacks = new Callback[2]; 1023 callbacks[0] = new NameCallback(rb.getString("username.")); 1024 callbacks[1] = new PasswordCallback(rb.getString("password."), false); 1025 1026 try { 1027 callbackHandler.handle(callbacks); 1028 username = ((NameCallback)callbacks[0]).getName(); 1029 char[] tmpPassword = ((PasswordCallback)callbacks[1]).getPassword(); 1030 password = new char[tmpPassword.length]; 1031 System.arraycopy(tmpPassword, 0, 1032 password, 0, tmpPassword.length); 1033 ((PasswordCallback)callbacks[1]).clearPassword(); 1034 1035 } catch (java.io.IOException ioe) { 1036 throw new LoginException(ioe.toString()); 1037 1038 } catch (UnsupportedCallbackException uce) { 1039 throw new LoginException("Error: " + uce.getCallback().toString() + 1040 " not available to acquire authentication information" + 1041 " from the user"); 1042 } 1043 } 1044 1045 /** 1046 * Clean out state because of a failed authentication attempt 1047 */ cleanState()1048 private void cleanState() { 1049 username = null; 1050 if (password != null) { 1051 Arrays.fill(password, ' '); 1052 password = null; 1053 } 1054 try { 1055 if (ctx != null) { 1056 ctx.close(); 1057 } 1058 } catch (NamingException e) { 1059 // ignore 1060 } 1061 ctx = null; 1062 1063 if (clearPass) { 1064 sharedState.remove(USERNAME_KEY); 1065 sharedState.remove(PASSWORD_KEY); 1066 } 1067 } 1068 } 1069