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