1 /* 2 * Copyright (c) 2000, 2006, 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 27 package com.sun.security.auth.module; 28 29 import java.io.*; 30 import java.text.MessageFormat; 31 import java.util.*; 32 33 import javax.security.auth.*; 34 import javax.security.auth.kerberos.*; 35 import javax.security.auth.callback.*; 36 import javax.security.auth.login.*; 37 import javax.security.auth.spi.*; 38 39 import sun.security.krb5.*; 40 import sun.security.jgss.krb5.Krb5Util; 41 import sun.security.krb5.Credentials; 42 import sun.misc.HexDumpEncoder; 43 44 /** 45 * <p> This <code>LoginModule</code> authenticates users using 46 * Kerberos protocols. 47 * 48 * <p> The configuration entry for <code>Krb5LoginModule</code> has 49 * several options that control the authentication process and 50 * additions to the <code>Subject</code>'s private credential 51 * set. Irrespective of these options, the <code>Subject</code>'s 52 * principal set and private credentials set are updated only when 53 * <code>commit</code> is called. 54 * When <code>commit</code> is called, the <code>KerberosPrincipal</code> 55 * is added to the <code>Subject</code>'s 56 * principal set and <code>KerberosTicket</code> is 57 * added to the <code>Subject</code>'s private credentials. 58 * 59 * <p> If the configuration entry for <code>KerberosLoginModule</code> 60 * has the option <code>storeKey</code> set to true, then 61 * <code>KerberosKey</code> will also be added to the 62 * subject's private credentials. <code>KerberosKey</code>, the principal's 63 * key will be either obtained from the keytab or 64 * derived from user's password. 65 * 66 * <p> This <code>LoginModule</code> recognizes the <code>doNotPrompt</code> 67 * option. If set to true the user will not be prompted for the password. 68 * 69 * <p> The user can specify the location of the ticket cache by using 70 * the option <code>ticketCache</code> in the configuration entry. 71 * 72 * <p>The user can specify the keytab location by using 73 * the option <code>keyTab</code> 74 * in the configuration entry. 75 * 76 * <p> The principal name can be specified in the configuration entry 77 * by using the option <code>principal</code>. The principal name 78 * can either be a simple user name or a service name such as 79 * <code>host/mission.eng.sun.com</code>. The principal can also 80 * be set using the system property <code>sun.security.krb5.principal</code>. 81 * This property is checked during login. If this property is not set, then 82 * the principal name from the configuration is used. In the 83 * case where the principal property is not set and the principal 84 * entry also does not exist, the user is prompted for the name. 85 * When this property of entry is set, and <code>useTicketCache</code> 86 * is set to true, only TGT belonging to this principal is used. 87 * 88 * <p> The following is a list of configuration options supported 89 * for <code>Krb5LoginModule</code>: 90 * <dl> 91 * <blockquote><dt><b><code>refreshKrb5Config</code></b>:</dt> 92 * <dd> Set this to true, if you want the configuration 93 * to be refreshed before the <code>login</code> method is called.</dd> 94 * <P> 95 * <dt><b><code>useTicketCache</code></b>:</dt> 96 * <dd>Set this to true, if you want the 97 * TGT to be obtained 98 * from the ticket cache. Set this option 99 * to false if you do not want this module to use the ticket cache. 100 * (Default is False). 101 * This module will 102 * search for the ticket 103 * cache in the following locations: 104 * On Solaris and Linux 105 * it will look for the ticket cache in /tmp/krb5cc_<code>uid</code> 106 * where the uid is numeric user 107 * identifier. If the ticket cache is 108 * not available in the above location, or if we are on a 109 * Windows platform, it will look for the cache as 110 * {user.home}{file.separator}krb5cc_{user.name}. 111 * You can override the ticket cache location by using 112 * <code>ticketCache</code>. 113 * For Windows, if a ticket cannot be retrieved from the file ticket cache, 114 * it will use Local Security Authority (LSA) API to get the TGT. 115 * <P> 116 * <dt><b><code>ticketCache</code></b>:</dt> 117 * <dd>Set this to the name of the ticket 118 * cache that contains user's TGT. 119 * If this is set, <code>useTicketCache</code> 120 * must also be set to true; Otherwise a configuration error will 121 * be returned.</dd> 122 * <P> 123 * <dt><b><code>renewTGT</code></b>:</dt> 124 * <dd>Set this to true, if you want to renew 125 * the TGT. If this is set, <code>useTicketCache</code> must also be 126 * set to true; otherwise a configuration error will be returned.</dd> 127 * <p> 128 * <dt><b><code>doNotPrompt</code></b>:</dt> 129 * <dd>Set this to true if you do not want to be 130 * prompted for the password 131 * if credentials can not be obtained from the cache, the keytab, 132 * or through shared state.(Default is false) 133 * If set to true, credential must be obtained through cache, keytab, 134 * or shared state. Otherwise, authentication will fail.</dd> 135 * <P> 136 * <dt><b><code>useKeyTab</code></b>:</dt> 137 * <dd>Set this to true if you 138 * want the module to get the principal's key from the 139 * the keytab.(default value is False) 140 * If <code>keytab</code> 141 * is not set then 142 * the module will locate the keytab from the 143 * Kerberos configuration file. 144 * If it is not specified in the Kerberos configuration file 145 * then it will look for the file 146 * <code>{user.home}{file.separator}</code>krb5.keytab.</dd> 147 * <P> 148 * <dt><b><code>keyTab</code></b>:</dt> 149 * <dd>Set this to the file name of the 150 * keytab to get principal's secret key.</dd> 151 * <P> 152 * <dt><b><code>storeKey</code></b>:</dt> 153 * <dd>Set this to true to if you want the 154 * principal's key to be stored in the Subject's private credentials. </dd> 155 * <p> 156 * <dt><b><code>principal</code></b>:</dt> 157 * <dd>The name of the principal that should 158 * be used. The principal can be a simple username such as 159 * "<code>testuser</code>" or a service name such as 160 * "<code>host/testhost.eng.sun.com</code>". You can use the 161 * <code>principal</code> option to set the principal when there are 162 * credentials for multiple principals in the 163 * <code>keyTab</code> or when you want a specific ticket cache only. 164 * The principal can also be set using the system property 165 * <code>sun.security.krb5.principal</code>. In addition, if this 166 * system property is defined, then it will be used. If this property 167 * is not set, then the principal name from the configuration will be 168 * used.</dd> 169 * <P> 170 * <dt><b><code>isInitiator</code></b>:</dt> 171 * <dd>Set this to true, if initiator. Set this to false, if acceptor only. 172 * (Default is true). 173 * Note: Do not set this value to false for initiators.</dd> 174 * </dl></blockquote> 175 * 176 * <p> This <code>LoginModule</code> also recognizes the following additional 177 * <code>Configuration</code> 178 * options that enable you to share username and passwords across different 179 * authentication modules: 180 * <pre> 181 * 182 * useFirstPass if, true, this LoginModule retrieves the 183 * username and password from the module's shared state, 184 * using "javax.security.auth.login.name" and 185 * "javax.security.auth.login.password" as the respective 186 * keys. The retrieved values are used for authentication. 187 * If authentication fails, no attempt for a retry 188 * is made, and the failure is reported back to the 189 * calling application. 190 * 191 * tryFirstPass if, true, this LoginModule retrieves the 192 * the username and password from the module's shared 193 * state using "javax.security.auth.login.name" and 194 * "javax.security.auth.login.password" as the respective 195 * keys. The retrieved values are used for 196 * authentication. 197 * If authentication fails, the module uses the 198 * CallbackHandler to retrieve a new username 199 * and password, and another attempt to authenticate 200 * is made. If the authentication fails, 201 * the failure is reported back to the calling application 202 * 203 * storePass if, true, this LoginModule stores the username and 204 * password obtained from the CallbackHandler in the 205 * modules shared state, using 206 * "javax.security.auth.login.name" and 207 * "javax.security.auth.login.password" as the respective 208 * keys. This is not performed if existing values already 209 * exist for the username and password in the shared 210 * state, or if authentication fails. 211 * 212 * clearPass if, true, this LoginModule clears the 213 * username and password stored in the module's shared 214 * state after both phases of authentication 215 * (login and commit) have completed. 216 * </pre> 217 * <p>If the principal system property or key is already provided, the value of 218 * "javax.security.auth.login.name" in the shared state is ignored. 219 * <p>When multiple mechanisms to retrieve a ticket or key is provided, the 220 * preference order looks like this: 221 * <ol> 222 * <li>ticket cache 223 * <li>keytab 224 * <li>shared state 225 * <li>user prompt 226 * </ol> 227 * <p>Note that if any step fails, it will fallback to the next step. 228 * There's only one exception, it the shared state step fails and 229 * <code>useFirstPass</code>=true, no user prompt is made. 230 * <p>Examples of some configuration values for Krb5LoginModule in 231 * JAAS config file and the results are: 232 * <ul> 233 * <p> <code>doNotPrompt</code>=true; 234 * </ul> 235 * <p> This is an illegal combination since none of <code>useTicketCache</code>, 236 * <code>useKeyTab</code>, <code>useFirstPass</code> and <code>tryFirstPass</code> 237 * is set and the user can not be prompted for the password. 238 *<ul> 239 * <p> <code>ticketCache</code> = <filename>; 240 *</ul> 241 * <p> This is an illegal combination since <code>useTicketCache</code> 242 * is not set to true and the ticketCache is set. A configuration error 243 * will occur. 244 * <ul> 245 * <p> <code>renewTGT</code>=true; 246 *</ul> 247 * <p> This is an illegal combination since <code>useTicketCache</code> is 248 * not set to true and renewTGT is set. A configuration error will occur. 249 * <ul> 250 * <p> <code>storeKey</code>=true 251 * <code>useTicketCache</code> = true 252 * <code>doNotPrompt</code>=true;; 253 *</ul> 254 * <p> This is an illegal combination since <code>storeKey</code> is set to 255 * true but the key can not be obtained either by prompting the user or from 256 * the keytab, or from the shared state. A configuration error will occur. 257 * <ul> 258 * <p> <code>keyTab</code> = <filename> <code>doNotPrompt</code>=true ; 259 * </ul> 260 * <p>This is an illegal combination since useKeyTab is not set to true and 261 * the keyTab is set. A configuration error will occur. 262 * <ul> 263 * <p> <code>debug=true </code> 264 *</ul> 265 * <p> Prompt the user for the principal name and the password. 266 * Use the authentication exchange to get TGT from the KDC and 267 * populate the <code>Subject</code> with the principal and TGT. 268 * Output debug messages. 269 * <ul> 270 * <p> <code>useTicketCache</code> = true <code>doNotPrompt</code>=true; 271 *</ul> 272 * <p>Check the default cache for TGT and populate the <code>Subject</code> 273 * with the principal and TGT. If the TGT is not available, 274 * do not prompt the user, instead fail the authentication. 275 * <ul> 276 * <p><code>principal</code>=<name><code>useTicketCache</code> = true 277 * <code>doNotPrompt</code>=true; 278 *</ul> 279 * <p> Get the TGT from the default cache for the principal and populate the 280 * Subject's principal and private creds set. If ticket cache is 281 * not available or does not contain the principal's TGT 282 * authentication will fail. 283 * <ul> 284 * <p> <code>useTicketCache</code> = true 285 * <code>ticketCache</code>=<file name><code>useKeyTab</code> = true 286 * <code> keyTab</code>=<keytab filename> 287 * <code>principal</code> = <principal name> 288 * <code>doNotPrompt</code>=true; 289 *</ul> 290 * <p> Search the cache for the principal's TGT. If it is not available 291 * use the key in the keytab to perform authentication exchange with the 292 * KDC and acquire the TGT. 293 * The Subject will be populated with the principal and the TGT. 294 * If the key is not available or valid then authentication will fail. 295 * <ul> 296 * <p><code>useTicketCache</code> = true 297 * <code>ticketCache</code>=<file name> 298 *</ul> 299 * <p> The TGT will be obtained from the cache specified. 300 * The Kerberos principal name used will be the principal name in 301 * the Ticket cache. If the TGT is not available in the 302 * ticket cache the user will be prompted for the principal name 303 * and the password. The TGT will be obtained using the authentication 304 * exchange with the KDC. 305 * The Subject will be populated with the TGT. 306 *<ul> 307 * <p> <code>useKeyTab</code> = true 308 * <code>keyTab</code>=<keytab filename> 309 * <code>principal</code>= <principal name> 310 * <code>storeKey</code>=true; 311 *</ul> 312 * <p> The key for the principal will be retrieved from the keytab. 313 * If the key is not available in the keytab the user will be prompted 314 * for the principal's password. The Subject will be populated 315 * with the principal's key either from the keytab or derived from the 316 * password entered. 317 * <ul> 318 * <p> <code>useKeyTab</code> = true 319 * <code>keyTab</code>=<keytabname> 320 * <code>storeKey</code>=true 321 * <code>doNotPrompt</code>=true; 322 *</ul> 323 * <p>The user will be prompted for the service principal name. 324 * If the principal's 325 * longterm key is available in the keytab , it will be added to the 326 * Subject's private credentials. An authentication exchange will be 327 * attempted with the principal name and the key from the Keytab. 328 * If successful the TGT will be added to the 329 * Subject's private credentials set. Otherwise the authentication will 330 * fail. 331 *<ul> 332 * <p> 333 * <code>useTicketCache</code>=true 334 * <code>ticketCache</code>=<file name>; 335 * <code>useKeyTab</code> = true 336 * <code>keyTab</code>=<file name> <code>storeKey</code>=true 337 * <code>principal</code>= <principal name> 338 *</ul> 339 * <p> 340 * The client's TGT will be retrieved from the ticket cache and added to the 341 * <code>Subject</code>'s private credentials. If the TGT is not available 342 * in the ticket cache, or the TGT's client name does not match the principal 343 * name, Java will use a secret key to obtain the TGT using the authentication 344 * exchange and added to the Subject's private credentials. 345 * This secret key will be first retrieved from the keytab. If the key 346 * is not available, the user will be prompted for the password. In either 347 * case, the key derived from the password will be added to the 348 * Subject's private credentials set. 349 * <ul> 350 * <p><code>isInitiator</code> = false 351 *</ul> 352 * <p>Configured to act as acceptor only, credentials are not acquired 353 * via AS exchange. For acceptors only, set this value to false. 354 * For initiators, do not set this value to false. 355 * <ul> 356 * <p><code>isInitiator</code> = true 357 *</ul> 358 * <p>Configured to act as initiator, credentials are acquired 359 * via AS exchange. For initiators, set this value to true, or leave this 360 * option unset, in which case default value (true) will be used. 361 * 362 * @author Ram Marti 363 */ 364 365 public class Krb5LoginModule implements LoginModule { 366 367 // initial state 368 private Subject subject; 369 private CallbackHandler callbackHandler; 370 private Map<String, Object> sharedState; 371 private Map<String, ?> options; 372 373 // configurable option 374 private boolean debug = false; 375 private boolean storeKey = false; 376 private boolean doNotPrompt = false; 377 private boolean useTicketCache = false; 378 private boolean useKeyTab = false; 379 private String ticketCacheName = null; 380 private String keyTabName = null; 381 private String princName = null; 382 383 private boolean useFirstPass = false; 384 private boolean tryFirstPass = false; 385 private boolean storePass = false; 386 private boolean clearPass = false; 387 private boolean refreshKrb5Config = false; 388 private boolean renewTGT = false; 389 390 // specify if initiator. 391 // perform authentication exchange if initiator 392 private boolean isInitiator = true; 393 394 // the authentication status 395 private boolean succeeded = false; 396 private boolean commitSucceeded = false; 397 private String username; 398 private EncryptionKey[] encKeys = null; 399 private Credentials cred = null; 400 401 private PrincipalName principal = null; 402 private KerberosPrincipal kerbClientPrinc = null; 403 private KerberosTicket kerbTicket = null; 404 private KerberosKey[] kerbKeys = null; 405 private StringBuffer krb5PrincName = null; 406 private char[] password = null; 407 408 private static final String NAME = "javax.security.auth.login.name"; 409 private static final String PWD = "javax.security.auth.login.password"; 410 static final java.util.ResourceBundle rb = 411 java.util.ResourceBundle.getBundle("sun.security.util.AuthResources"); 412 413 /** 414 * Initialize this <code>LoginModule</code>. 415 * 416 * <p> 417 * @param subject the <code>Subject</code> to be authenticated. <p> 418 * 419 * @param callbackHandler a <code>CallbackHandler</code> for 420 * communication with the end user (prompting for 421 * usernames and passwords, for example). <p> 422 * 423 * @param sharedState shared <code>LoginModule</code> state. <p> 424 * 425 * @param options options specified in the login 426 * <code>Configuration</code> for this particular 427 * <code>LoginModule</code>. 428 */ 429 // Unchecked warning from (Map<String, Object>)sharedState is safe 430 // since javax.security.auth.login.LoginContext passes a raw HashMap. 431 // Unchecked warnings from options.get(String) are safe since we are 432 // passing known keys. 433 @SuppressWarnings("unchecked") 434 public void initialize(Subject subject, 435 CallbackHandler callbackHandler, 436 Map<String, ?> sharedState, 437 Map<String, ?> options) { 438 439 this.subject = subject; 440 this.callbackHandler = callbackHandler; 441 this.sharedState = (Map<String, Object>)sharedState; 442 this.options = options; 443 444 // initialize any configured options 445 446 debug = "true".equalsIgnoreCase((String)options.get("debug")); 447 storeKey = "true".equalsIgnoreCase((String)options.get("storeKey")); 448 doNotPrompt = "true".equalsIgnoreCase((String)options.get 449 ("doNotPrompt")); 450 useTicketCache = "true".equalsIgnoreCase((String)options.get 451 ("useTicketCache")); 452 useKeyTab = "true".equalsIgnoreCase((String)options.get("useKeyTab")); 453 ticketCacheName = (String)options.get("ticketCache"); 454 keyTabName = (String)options.get("keyTab"); 455 princName = (String)options.get("principal"); 456 refreshKrb5Config = 457 "true".equalsIgnoreCase((String)options.get("refreshKrb5Config")); 458 renewTGT = 459 "true".equalsIgnoreCase((String)options.get("renewTGT")); 460 461 // check isInitiator value 462 String isInitiatorValue = ((String)options.get("isInitiator")); 463 if (isInitiatorValue == null) { 464 // use default, if value not set 465 } else { 466 isInitiator = "true".equalsIgnoreCase(isInitiatorValue); 467 } 468 469 tryFirstPass = 470 "true".equalsIgnoreCase 471 ((String)options.get("tryFirstPass")); 472 useFirstPass = 473 "true".equalsIgnoreCase 474 ((String)options.get("useFirstPass")); 475 storePass = 476 "true".equalsIgnoreCase((String)options.get("storePass")); 477 clearPass = 478 "true".equalsIgnoreCase((String)options.get("clearPass")); 479 if (debug) { 480 System.out.print("Debug is " + debug 481 + " storeKey " + storeKey 482 + " useTicketCache " + useTicketCache 483 + " useKeyTab " + useKeyTab 484 + " doNotPrompt " + doNotPrompt 485 + " ticketCache is " + ticketCacheName 486 + " isInitiator " + isInitiator 487 + " KeyTab is " + keyTabName 488 + " refreshKrb5Config is " + refreshKrb5Config 489 + " principal is " + princName 490 + " tryFirstPass is " + tryFirstPass 491 + " useFirstPass is " + useFirstPass 492 + " storePass is " + storePass 493 + " clearPass is " + clearPass + "\n"); 494 } 495 } 496 497 498 /** 499 * Authenticate the user 500 * 501 * <p> 502 * 503 * @return true in all cases since this <code>LoginModule</code> 504 * should not be ignored. 505 * 506 * @exception FailedLoginException if the authentication fails. <p> 507 * 508 * @exception LoginException if this <code>LoginModule</code> 509 * is unable to perform the authentication. 510 */ 511 public boolean login() throws LoginException { 512 513 int len; 514 validateConfiguration(); 515 if (refreshKrb5Config) { 516 try { 517 if (debug) { 518 System.out.println("Refreshing Kerberos configuration"); 519 } 520 sun.security.krb5.Config.refresh(); 521 } catch (KrbException ke) { 522 LoginException le = new LoginException(ke.getMessage()); 523 le.initCause(ke); 524 throw le; 525 } 526 } 527 String principalProperty = System.getProperty 528 ("sun.security.krb5.principal"); 529 if (principalProperty != null) { 530 krb5PrincName = new StringBuffer(principalProperty); 531 } else { 532 if (princName != null) { 533 krb5PrincName = new StringBuffer(princName); 534 } 535 } 536 537 if (tryFirstPass) { 538 try { 539 attemptAuthentication(true); 540 if (debug) 541 System.out.println("\t\t[Krb5LoginModule] " + 542 "authentication succeeded"); 543 succeeded = true; 544 cleanState(); 545 return true; 546 } catch (LoginException le) { 547 // authentication failed -- try again below by prompting 548 cleanState(); 549 if (debug) { 550 System.out.println("\t\t[Krb5LoginModule] " + 551 "tryFirstPass failed with:" + 552 le.getMessage()); 553 } 554 } 555 } else if (useFirstPass) { 556 try { 557 attemptAuthentication(true); 558 succeeded = true; 559 cleanState(); 560 return true; 561 } catch (LoginException e) { 562 // authentication failed -- clean out state 563 if (debug) { 564 System.out.println("\t\t[Krb5LoginModule] " + 565 "authentication failed \n" + 566 e.getMessage()); 567 } 568 succeeded = false; 569 cleanState(); 570 throw e; 571 } 572 } 573 574 // attempt the authentication by getting the username and pwd 575 // by prompting or configuration i.e. not from shared state 576 577 try { 578 attemptAuthentication(false); 579 succeeded = true; 580 cleanState(); 581 return true; 582 } catch (LoginException e) { 583 // authentication failed -- clean out state 584 if (debug) { 585 System.out.println("\t\t[Krb5LoginModule] " + 586 "authentication failed \n" + 587 e.getMessage()); 588 } 589 succeeded = false; 590 cleanState(); 591 throw e; 592 } 593 } 594 /** 595 * process the configuration options 596 * Get the TGT either out of 597 * cache or from the KDC using the password entered 598 * Check the permission before getting the TGT 599 */ 600 601 private void attemptAuthentication(boolean getPasswdFromSharedState) 602 throws LoginException { 603 604 /* 605 * Check the creds cache to see whether 606 * we have TGT for this client principal 607 */ 608 if (krb5PrincName != null) { 609 try { 610 principal = new PrincipalName 611 (krb5PrincName.toString(), 612 PrincipalName.KRB_NT_PRINCIPAL); 613 } catch (KrbException e) { 614 LoginException le = new LoginException(e.getMessage()); 615 le.initCause(e); 616 throw le; 617 } 618 } 619 620 try { 621 if (useTicketCache) { 622 // ticketCacheName == null implies the default cache 623 if (debug) 624 System.out.println("Acquire TGT from Cache"); 625 cred = Credentials.acquireTGTFromCache 626 (principal, ticketCacheName); 627 628 if (cred != null) { 629 // check to renew credentials 630 if (!isCurrent(cred)) { 631 if (renewTGT) { 632 cred = renewCredentials(cred); 633 } else { 634 // credentials have expired 635 cred = null; 636 if (debug) 637 System.out.println("Credentials are" + 638 " no longer valid"); 639 } 640 } 641 } 642 643 if (cred != null) { 644 // get the principal name from the ticket cache 645 if (principal == null) { 646 principal = cred.getClient(); 647 } 648 } 649 if (debug) { 650 System.out.println("Principal is " + principal); 651 if (cred == null) { 652 System.out.println 653 ("null credentials from Ticket Cache"); 654 } 655 } 656 } 657 658 // cred = null indicates that we didn't get the creds 659 // from the cache or useTicketCache was false 660 661 if (cred == null) { 662 // We need the principal name whether we use keytab 663 // or AS Exchange 664 if (principal == null) { 665 promptForName(getPasswdFromSharedState); 666 principal = new PrincipalName 667 (krb5PrincName.toString(), 668 PrincipalName.KRB_NT_PRINCIPAL); 669 } 670 if (useKeyTab) { 671 encKeys = 672 EncryptionKey.acquireSecretKeys(principal, keyTabName); 673 674 if (debug) { 675 if (encKeys != null) 676 System.out.println 677 ("principal's key obtained from the keytab"); 678 else 679 System.out.println 680 ("Key for the principal " + 681 principal + 682 " not available in " + 683 ((keyTabName == null) ? 684 "default key tab" : keyTabName)); 685 } 686 687 } 688 689 KrbAsReqBuilder builder; 690 // We can't get the key from the keytab so prompt 691 if (encKeys == null) { 692 promptForPass(getPasswdFromSharedState); 693 builder = new KrbAsReqBuilder(principal, password); 694 if (isInitiator) { 695 // XXX Even if isInitiator=false, it might be 696 // better to do an AS-REQ so that keys can be 697 // updated with PA info 698 cred = builder.action().getCreds(); 699 } 700 encKeys = builder.getKeys(); 701 } else { 702 builder = new KrbAsReqBuilder(principal, encKeys); 703 if (isInitiator) { 704 cred = builder.action().getCreds(); 705 } 706 } 707 builder.destroy(); 708 709 if (debug) { 710 System.out.println("principal is " + principal); 711 HexDumpEncoder hd = new HexDumpEncoder(); 712 for (int i = 0; i < encKeys.length; i++) { 713 System.out.println("EncryptionKey: keyType=" + 714 encKeys[i].getEType() + " keyBytes (hex dump)=" + 715 hd.encode(encKeys[i].getBytes())); 716 } 717 } 718 719 // we should hava a non-null cred 720 if (isInitiator && (cred == null)) { 721 throw new LoginException 722 ("TGT Can not be obtained from the KDC "); 723 } 724 725 } 726 } catch (KrbException e) { 727 LoginException le = new LoginException(e.getMessage()); 728 le.initCause(e); 729 throw le; 730 } catch (IOException ioe) { 731 LoginException ie = new LoginException(ioe.getMessage()); 732 ie.initCause(ioe); 733 throw ie; 734 } 735 } 736 737 private void promptForName(boolean getPasswdFromSharedState) 738 throws LoginException { 739 krb5PrincName = new StringBuffer(""); 740 if (getPasswdFromSharedState) { 741 // use the name saved by the first module in the stack 742 username = (String)sharedState.get(NAME); 743 if (debug) { 744 System.out.println 745 ("username from shared state is " + username + "\n"); 746 } 747 if (username == null) { 748 System.out.println 749 ("username from shared state is null\n"); 750 throw new LoginException 751 ("Username can not be obtained from sharedstate "); 752 } 753 if (debug) { 754 System.out.println 755 ("username from shared state is " + username + "\n"); 756 } 757 if (username != null && username.length() > 0) { 758 krb5PrincName.insert(0, username); 759 return; 760 } 761 } 762 763 if (doNotPrompt) { 764 throw new LoginException 765 ("Unable to obtain Princpal Name for authentication "); 766 } else { 767 if (callbackHandler == null) 768 throw new LoginException("No CallbackHandler " 769 + "available " 770 + "to garner authentication " 771 + "information from the user"); 772 try { 773 String defUsername = System.getProperty("user.name"); 774 775 Callback[] callbacks = new Callback[1]; 776 MessageFormat form = new MessageFormat( 777 rb.getString( 778 "Kerberos.username.defUsername.")); 779 Object[] source = {defUsername}; 780 callbacks[0] = new NameCallback(form.format(source)); 781 callbackHandler.handle(callbacks); 782 username = ((NameCallback)callbacks[0]).getName(); 783 if (username == null || username.length() == 0) 784 username = defUsername; 785 krb5PrincName.insert(0, username); 786 787 } catch (java.io.IOException ioe) { 788 throw new LoginException(ioe.getMessage()); 789 } catch (UnsupportedCallbackException uce) { 790 throw new LoginException 791 (uce.getMessage() 792 +" not available to garner " 793 +" authentication information " 794 +" from the user"); 795 } 796 } 797 } 798 799 private void promptForPass(boolean getPasswdFromSharedState) 800 throws LoginException { 801 802 if (getPasswdFromSharedState) { 803 // use the password saved by the first module in the stack 804 password = (char[])sharedState.get(PWD); 805 if (password == null) { 806 if (debug) { 807 System.out.println 808 ("Password from shared state is null"); 809 } 810 throw new LoginException 811 ("Password can not be obtained from sharedstate "); 812 } 813 if (debug) { 814 System.out.println 815 ("password is " + new String(password)); 816 } 817 return; 818 } 819 if (doNotPrompt) { 820 throw new LoginException 821 ("Unable to obtain password from user\n"); 822 } else { 823 if (callbackHandler == null) 824 throw new LoginException("No CallbackHandler " 825 + "available " 826 + "to garner authentication " 827 + "information from the user"); 828 try { 829 Callback[] callbacks = new Callback[1]; 830 String userName = krb5PrincName.toString(); 831 MessageFormat form = new MessageFormat( 832 rb.getString( 833 "Kerberos.password.for.username.")); 834 Object[] source = {userName}; 835 callbacks[0] = new PasswordCallback( 836 form.format(source), 837 false); 838 callbackHandler.handle(callbacks); 839 char[] tmpPassword = ((PasswordCallback) 840 callbacks[0]).getPassword(); 841 if (tmpPassword == null) { 842 // treat a NULL password as an empty password 843 tmpPassword = new char[0]; 844 } 845 password = new char[tmpPassword.length]; 846 System.arraycopy(tmpPassword, 0, 847 password, 0, tmpPassword.length); 848 ((PasswordCallback)callbacks[0]).clearPassword(); 849 850 851 // clear tmpPassword 852 for (int i = 0; i < tmpPassword.length; i++) 853 tmpPassword[i] = ' '; 854 tmpPassword = null; 855 if (debug) { 856 System.out.println("\t\t[Krb5LoginModule] " + 857 "user entered username: " + 858 krb5PrincName); 859 System.out.println(); 860 } 861 } catch (java.io.IOException ioe) { 862 throw new LoginException(ioe.getMessage()); 863 } catch (UnsupportedCallbackException uce) { 864 throw new LoginException(uce.getMessage() 865 +" not available to garner " 866 +" authentication information " 867 + "from the user"); 868 } 869 } 870 } 871 872 private void validateConfiguration() throws LoginException { 873 if (doNotPrompt && !useTicketCache && !useKeyTab 874 && !tryFirstPass && !useFirstPass) 875 throw new LoginException 876 ("Configuration Error" 877 + " - either doNotPrompt should be " 878 + " false or at least one of useTicketCache, " 879 + " useKeyTab, tryFirstPass and useFirstPass" 880 + " should be true"); 881 if (ticketCacheName != null && !useTicketCache) 882 throw new LoginException 883 ("Configuration Error " 884 + " - useTicketCache should be set " 885 + "to true to use the ticket cache" 886 + ticketCacheName); 887 if (keyTabName != null & !useKeyTab) 888 throw new LoginException 889 ("Configuration Error - useKeyTab should be set to true " 890 + "to use the keytab" + keyTabName); 891 if (storeKey && doNotPrompt && !useKeyTab 892 && !tryFirstPass && !useFirstPass) 893 throw new LoginException 894 ("Configuration Error - either doNotPrompt should be set to " 895 + " false or at least one of tryFirstPass, useFirstPass " 896 + "or useKeyTab must be set to true for storeKey option"); 897 if (renewTGT && !useTicketCache) 898 throw new LoginException 899 ("Configuration Error" 900 + " - either useTicketCache should be " 901 + " true or renewTGT should be false"); 902 } 903 904 private boolean isCurrent(Credentials creds) 905 { 906 Date endTime = creds.getEndTime(); 907 if (endTime != null) { 908 return (System.currentTimeMillis() <= endTime.getTime()); 909 } 910 return true; 911 } 912 913 private Credentials renewCredentials(Credentials creds) 914 { 915 Credentials lcreds; 916 try { 917 if (!creds.isRenewable()) 918 throw new RefreshFailedException("This ticket" + 919 " is not renewable"); 920 if (System.currentTimeMillis() > cred.getRenewTill().getTime()) 921 throw new RefreshFailedException("This ticket is past " 922 + "its last renewal time."); 923 lcreds = creds.renew(); 924 if (debug) 925 System.out.println("Renewed Kerberos Ticket"); 926 } catch (Exception e) { 927 lcreds = null; 928 if (debug) 929 System.out.println("Ticket could not be renewed : " 930 + e.getMessage()); 931 } 932 return lcreds; 933 } 934 935 /** 936 * <p> This method is called if the LoginContext's 937 * overall authentication succeeded 938 * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL 939 * LoginModules succeeded). 940 * 941 * <p> If this LoginModule's own authentication attempt 942 * succeeded (checked by retrieving the private state saved by the 943 * <code>login</code> method), then this method associates a 944 * <code>Krb5Principal</code> 945 * with the <code>Subject</code> located in the 946 * <code>LoginModule</code>. It adds Kerberos Credentials to the 947 * the Subject's private credentials set. If this LoginModule's own 948 * authentication attempted failed, then this method removes 949 * any state that was originally saved. 950 * 951 * <p> 952 * 953 * @exception LoginException if the commit fails. 954 * 955 * @return true if this LoginModule's own login and commit 956 * attempts succeeded, or false otherwise. 957 */ 958 959 public boolean commit() throws LoginException { 960 961 /* 962 * Let us add the Krb5 Creds to the Subject's 963 * private credentials. The credentials are of type 964 * KerberosKey or KerberosTicket 965 */ 966 if (succeeded == false) { 967 return false; 968 } else { 969 970 if (isInitiator && (cred == null)) { 971 succeeded = false; 972 throw new LoginException("Null Client Credential"); 973 } 974 975 if (subject.isReadOnly()) { 976 cleanKerberosCred(); 977 throw new LoginException("Subject is Readonly"); 978 } 979 980 /* 981 * Add the Principal (authenticated identity) 982 * to the Subject's principal set and 983 * add the credentials (TGT or Service key) to the 984 * Subject's private credentials 985 */ 986 987 Set<Object> privCredSet = subject.getPrivateCredentials(); 988 Set<java.security.Principal> princSet = subject.getPrincipals(); 989 kerbClientPrinc = new KerberosPrincipal(principal.getName()); 990 991 // create Kerberos Ticket 992 if (isInitiator) { 993 kerbTicket = Krb5Util.credsToTicket(cred); 994 } 995 996 if (storeKey) { 997 if (encKeys == null || encKeys.length <= 0) { 998 succeeded = false; 999 throw new LoginException("Null Server Key "); 1000 } 1001 1002 kerbKeys = new KerberosKey[encKeys.length]; 1003 for (int i = 0; i < encKeys.length; i ++) { 1004 Integer temp = encKeys[i].getKeyVersionNumber(); 1005 kerbKeys[i] = new KerberosKey(kerbClientPrinc, 1006 encKeys[i].getBytes(), 1007 encKeys[i].getEType(), 1008 (temp == null? 1009 0: temp.intValue())); 1010 } 1011 1012 } 1013 // Let us add the kerbClientPrinc,kerbTicket and kerbKey (if 1014 // storeKey is true) 1015 if (!princSet.contains(kerbClientPrinc)) 1016 princSet.add(kerbClientPrinc); 1017 1018 // add the TGT 1019 if (kerbTicket != null) { 1020 if (!privCredSet.contains(kerbTicket)) 1021 privCredSet.add(kerbTicket); 1022 } 1023 1024 if (storeKey) { 1025 for (int i = 0; i < kerbKeys.length; i++) { 1026 if (!privCredSet.contains(kerbKeys[i])) { 1027 privCredSet.add(kerbKeys[i]); 1028 } 1029 encKeys[i].destroy(); 1030 encKeys[i] = null; 1031 if (debug) { 1032 System.out.println("Added server's key" 1033 + kerbKeys[i]); 1034 System.out.println("\t\t[Krb5LoginModule] " + 1035 "added Krb5Principal " + 1036 kerbClientPrinc.toString() 1037 + " to Subject"); 1038 } 1039 } 1040 } 1041 } 1042 commitSucceeded = true; 1043 if (debug) 1044 System.out.println("Commit Succeeded \n"); 1045 return true; 1046 } 1047 1048 /** 1049 * <p> This method is called if the LoginContext's 1050 * overall authentication failed. 1051 * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL 1052 * LoginModules did not succeed). 1053 * 1054 * <p> If this LoginModule's own authentication attempt 1055 * succeeded (checked by retrieving the private state saved by the 1056 * <code>login</code> and <code>commit</code> methods), 1057 * then this method cleans up any state that was originally saved. 1058 * 1059 * <p> 1060 * 1061 * @exception LoginException if the abort fails. 1062 * 1063 * @return false if this LoginModule's own login and/or commit attempts 1064 * failed, and true otherwise. 1065 */ 1066 1067 public boolean abort() throws LoginException { 1068 if (succeeded == false) { 1069 return false; 1070 } else if (succeeded == true && commitSucceeded == false) { 1071 // login succeeded but overall authentication failed 1072 succeeded = false; 1073 cleanKerberosCred(); 1074 } else { 1075 // overall authentication succeeded and commit succeeded, 1076 // but someone else's commit failed 1077 logout(); 1078 } 1079 return true; 1080 } 1081 1082 /** 1083 * Logout the user. 1084 * 1085 * <p> This method removes the <code>Krb5Principal</code> 1086 * that was added by the <code>commit</code> method. 1087 * 1088 * <p> 1089 * 1090 * @exception LoginException if the logout fails. 1091 * 1092 * @return true in all cases since this <code>LoginModule</code> 1093 * should not be ignored. 1094 */ 1095 public boolean logout() throws LoginException { 1096 1097 if (debug) { 1098 System.out.println("\t\t[Krb5LoginModule]: " + 1099 "Entering logout"); 1100 } 1101 1102 if (subject.isReadOnly()) { 1103 cleanKerberosCred(); 1104 throw new LoginException("Subject is Readonly"); 1105 } 1106 1107 subject.getPrincipals().remove(kerbClientPrinc); 1108 // Let us remove all Kerberos credentials stored in the Subject 1109 Iterator<Object> it = subject.getPrivateCredentials().iterator(); 1110 while (it.hasNext()) { 1111 Object o = it.next(); 1112 if (o instanceof KerberosTicket || 1113 o instanceof KerberosKey) { 1114 it.remove(); 1115 } 1116 } 1117 // clean the kerberos ticket and keys 1118 cleanKerberosCred(); 1119 1120 succeeded = false; 1121 commitSucceeded = false; 1122 if (debug) { 1123 System.out.println("\t\t[Krb5LoginModule]: " + 1124 "logged out Subject"); 1125 } 1126 return true; 1127 } 1128 1129 /** 1130 * Clean Kerberos credentials 1131 */ 1132 private void cleanKerberosCred() throws LoginException { 1133 // Clean the ticket and server key 1134 try { 1135 if (kerbTicket != null) 1136 kerbTicket.destroy(); 1137 if (kerbKeys != null) { 1138 for (int i = 0; i < kerbKeys.length; i++) { 1139 kerbKeys[i].destroy(); 1140 } 1141 } 1142 } catch (DestroyFailedException e) { 1143 throw new LoginException 1144 ("Destroy Failed on Kerberos Private Credentials"); 1145 } 1146 kerbTicket = null; 1147 kerbKeys = null; 1148 kerbClientPrinc = null; 1149 } 1150 1151 /** 1152 * Clean out the state 1153 */ 1154 private void cleanState() { 1155 1156 // save input as shared state only if 1157 // authentication succeeded 1158 if (succeeded) { 1159 if (storePass && 1160 !sharedState.containsKey(NAME) && 1161 !sharedState.containsKey(PWD)) { 1162 sharedState.put(NAME, username); 1163 sharedState.put(PWD, password); 1164 } 1165 } 1166 username = null; 1167 password = null; 1168 if (krb5PrincName != null && krb5PrincName.length() != 0) 1169 krb5PrincName.delete(0, krb5PrincName.length()); 1170 krb5PrincName = null; 1171 if (clearPass) { 1172 sharedState.remove(NAME); 1173 sharedState.remove(PWD); 1174 } 1175 } 1176 }