changeset 812:76edd0698e0a

6765491: Krb5LoginModule a little too restrictive, and the doc is not clear. Reviewed-by: valeriep
author weijun
date Wed, 12 Nov 2008 16:01:06 +0800
parents d2f96992b77b
children 84bd7fd5fb65
files src/share/classes/com/sun/security/auth/module/Krb5LoginModule.java test/sun/security/krb5/auto/Context.java test/sun/security/krb5/auto/KDC.java test/sun/security/krb5/auto/LoginModuleOptions.java
diffstat 4 files changed, 290 insertions(+), 52 deletions(-) [+]
line wrap: on
line diff
--- a/src/share/classes/com/sun/security/auth/module/Krb5LoginModule.java	Wed Nov 12 16:00:22 2008 +0800
+++ b/src/share/classes/com/sun/security/auth/module/Krb5LoginModule.java	Wed Nov 12 16:01:06 2008 +0800
@@ -86,6 +86,8 @@
  * the principal name from the configuration is used. In the
  * case where the principal property is not set and the principal
  * entry also does not exist, the user is prompted for the name.
+ * When this property of entry is set, and <code>useTicketCache</code>
+ * is set to true, only TGT belonging to this principal is used.
  *
  * <p> The following is a list of configuration options supported
  * for <code>Krb5LoginModule</code>:
@@ -101,18 +103,19 @@
  * to false if you do not want this module to use the ticket cache.
  * (Default is False).
  * This module will
- * search for the tickect
+ * search for the ticket
  * cache in the following locations:
- * For Windows 2000, it will use Local Security Authority (LSA) API
- * to get the TGT. On Solaris and Linux
+ * On Solaris and Linux
  * it will look for the ticket cache in /tmp/krb5cc_<code>uid</code>
  * where the uid is numeric user
  * identifier. If the ticket cache is
- * not available in either of the above locations, or if we are on a
- * different Windows platform,  it will look for the cache as
+ * not available in the above location, or if we are on a
+ * Windows platform, it will look for the cache as
  * {user.home}{file.separator}krb5cc_{user.name}.
  * You can override the ticket cache location by using
- * <code>ticketCache</code>
+ * <code>ticketCache</code>.
+ * For Windows, if a ticket cannot be retrieved from the file ticket cache,
+ * it will use Local Security Authority (LSA) API to get the TGT.
  * <P>
  * <dt><b><code>ticketCache</code></b>:</dt>
  * <dd>Set this to the name of the ticket
@@ -129,20 +132,20 @@
  * <dt><b><code>doNotPrompt</code></b>:</dt>
  * <dd>Set this to true if you do not want to be
  * prompted for the password
- * if credentials can
- * not be obtained from the cache or keytab.(Default is false)
- * If set to true authentication will fail if credentials can
- * not be obtained from the cache or keytab.</dd>
+ * if credentials can not be obtained from the cache, the keytab,
+ * or through shared state.(Default is false)
+ * If set to true, credential must be obtained through cache, keytab,
+ * or shared state. Otherwise, authentication will fail.</dd>
  * <P>
  * <dt><b><code>useKeyTab</code></b>:</dt>
  * <dd>Set this to true if you
  * want the module to get the principal's key from the
  * the keytab.(default value is False)
- * If <code>keyatb</code>
+ * If <code>keytab</code>
  * is not set then
  * the module will locate the keytab from the
- * Kerberos configuration file.</dd>
- * If it is not specifed in the Kerberos configuration file
+ * Kerberos configuration file.
+ * If it is not specified in the Kerberos configuration file
  * then it will look for the file
  * <code>{user.home}{file.separator}</code>krb5.keytab.</dd>
  * <P>
@@ -210,20 +213,34 @@
  *                   exist for the username and password in the shared
  *                   state, or if authentication fails.
  *
- *    clearPass     if, true, this <code>LoginModule</code> clears the
- *                  username and password stored in the module's shared
- *                  state  after both phases of authentication
- *                  (login and commit)  have completed.
+ *    clearPass      if, true, this LoginModule clears the
+ *                   username and password stored in the module's shared
+ *                   state  after both phases of authentication
+ *                   (login and commit) have completed.
  * </pre>
+ * <p>If the principal system property or key is already provided, the value of
+ * "javax.security.auth.login.name" in the shared state is ignored.
+ * <p>When multiple mechanisms to retrieve a ticket or key is provided, the
+ * preference order looks like this:
+ * <ol>
+ * <li>ticket cache
+ * <li>keytab
+ * <li>shared state
+ * <li>user prompt
+ * </ol>
+ * <p>Note that if any step fails, it will fallback to the next step.
+ * There's only one exception, it the shared state step fails and
+ * <code>useFirstPass</code>=true, no user prompt is made.
  * <p>Examples of some configuration values for Krb5LoginModule in
  * JAAS config file and the results are:
  * <ul>
  * <p> <code>doNotPrompt</code>=true;
  * </ul>
- * <p> This is an illegal combination since <code>useTicketCache</code>
- * is not set and the user can not be prompted for the password.
+ * <p> This is an illegal combination since none of <code>useTicketCache</code>,
+ * <code>useKeyTab</code>, <code>useFirstPass</code> and <code>tryFirstPass</code>
+ * is set and the user can not be prompted for the password.
  *<ul>
- * <p> <code>ticketCache</code> = < filename >;
+ * <p> <code>ticketCache</code> = &lt;filename&gt;;
  *</ul>
  * <p> This is an illegal combination since <code>useTicketCache</code>
  * is not set to true and the ticketCache is set. A configuration error
@@ -240,9 +257,9 @@
  *</ul>
  * <p> This is an illegal combination since  <code>storeKey</code> is set to
  * true but the key can not be obtained either by prompting the user or from
- * the keytab.A configuration error will occur.
+ * the keytab, or from the shared state. A configuration error will occur.
  * <ul>
- * <p>  <code>keyTab</code> = < filename > <code>doNotPrompt</code>=true ;
+ * <p>  <code>keyTab</code> = &lt;filename&gt; <code>doNotPrompt</code>=true ;
  * </ul>
  * <p>This is an illegal combination since useKeyTab is not set to true and
  * the keyTab is set. A configuration error will occur.
@@ -260,7 +277,7 @@
  * with the principal and TGT. If the TGT is not available,
  * do not prompt the user, instead fail the authentication.
  * <ul>
- * <p><code>principal</code>=< name ><code>useTicketCache</code> = true
+ * <p><code>principal</code>=&lt;name&gt;<code>useTicketCache</code> = true
  * <code>doNotPrompt</code>=true;
  *</ul>
  * <p> Get the TGT from the default cache for the principal and populate the
@@ -269,9 +286,9 @@
  * authentication will fail.
  * <ul>
  * <p> <code>useTicketCache</code> = true
- * <code>ticketCache</code>=< file name ><code>useKeyTab</code> = true
- * <code> keyTab</code>=< keytab filename >
- * <code>principal</code> = < principal name >
+ * <code>ticketCache</code>=&lt;file name&gt;<code>useKeyTab</code> = true
+ * <code> keyTab</code>=&lt;keytab filename&gt;
+ * <code>principal</code> = &lt;principal name&gt;
  * <code>doNotPrompt</code>=true;
  *</ul>
  * <p>  Search the cache for the principal's TGT. If it is not available
@@ -281,7 +298,7 @@
  * If the key is not available or valid then authentication will fail.
  * <ul>
  * <p><code>useTicketCache</code> = true
- * <code>ticketCache</code>=< file name >
+ * <code>ticketCache</code>=&lt;file name&gt;
  *</ul>
  * <p> The TGT will be obtained from the cache specified.
  * The Kerberos principal name used will be the principal name in
@@ -292,8 +309,8 @@
  * The Subject will be populated with the TGT.
  *<ul>
  * <p> <code>useKeyTab</code> = true
- * <code>keyTab</code>=< keytab filename >
- * <code>principal</code>= < principal name >
+ * <code>keyTab</code>=&lt;keytab filename&gt;
+ * <code>principal</code>= &lt;principal name&gt;
  * <code>storeKey</code>=true;
  *</ul>
  * <p>  The key for the principal will be retrieved from the keytab.
@@ -303,7 +320,7 @@
  * password entered.
  * <ul>
  * <p> <code>useKeyTab</code> = true
- * <code>keyTab</code>=< keytabname >
+ * <code>keyTab</code>=&lt;keytabname&gt;
  * <code>storeKey</code>=true
  * <code>doNotPrompt</code>=true;
  *</ul>
@@ -316,21 +333,23 @@
  * Subject's private credentials set. Otherwise the authentication will
  * fail.
  *<ul>
- * <p><code>useKeyTab</code> = true
- * <code>keyTab</code>=< file name > <code>storeKey</code>=true
- * <code>principal</code>= < principal name >
+ * <p>
  * <code>useTicketCache</code>=true
- * <code>ticketCache</code>=< file name >;
+ * <code>ticketCache</code>=&lt;file name&gt;;
+ * <code>useKeyTab</code> = true
+ * <code>keyTab</code>=&lt;file name&gt; <code>storeKey</code>=true
+ * <code>principal</code>= &lt;principal name&gt;
  *</ul>
- * <p>The principal's key will be retrieved from the keytab and added
- * to the <code>Subject</code>'s private credentials. If the key
- * is not available, the
- * user will be prompted for the password; the key derived from the password
- * will be added to the Subject's private credentials set. The
- * client's TGT will be retrieved from the ticket cache and added to the
+ * <p>
+ * The client's TGT will be retrieved from the ticket cache and added to the
  * <code>Subject</code>'s private credentials. If the TGT is not available
- * in the ticket cache, it will be obtained using the authentication
+ * in the ticket cache, or the TGT's client name does not match the principal
+ * name, Java will use a secret key to obtain the TGT using the authentication
  * exchange and added to the Subject's private credentials.
+ * This secret key will be first retrieved from the keytab. If the key
+ * is not available, the user will be prompted for the password. In either
+ * case, the key derived from the password will be added to the
+ * Subject's private credentials set.
  * <ul>
  * <p><code>isInitiator</code> = false
  *</ul>
@@ -856,11 +875,13 @@
     }
 
     private void validateConfiguration() throws LoginException {
-        if (doNotPrompt && !useTicketCache && !useKeyTab)
+        if (doNotPrompt && !useTicketCache && !useKeyTab
+                && !tryFirstPass && !useFirstPass)
             throw new LoginException
                 ("Configuration Error"
                  + " - either doNotPrompt should be "
-                 + " false or useTicketCache/useKeyTab "
+                 + " false or at least one of useTicketCache, "
+                 + " useKeyTab, tryFirstPass and useFirstPass"
                  + " should be true");
         if (ticketCacheName != null && !useTicketCache)
             throw new LoginException
@@ -872,11 +893,12 @@
             throw new LoginException
                 ("Configuration Error - useKeyTab should be set to true "
                  + "to use the keytab" + keyTabName);
-        if (storeKey && doNotPrompt && !useKeyTab)
+        if (storeKey && doNotPrompt && !useKeyTab
+                && !tryFirstPass && !useFirstPass)
             throw new LoginException
-                ("Configuration Error - either doNotPrompt "
-                 + "should be set to false or "
-                 + "useKeyTab must be set to true for storeKey option");
+                ("Configuration Error - either doNotPrompt should be set to "
+                 + " false or at least one of tryFirstPass, useFirstPass "
+                 + "or useKeyTab must be set to true for storeKey option");
         if (renewTGT && !useTicketCache)
             throw new LoginException
                 ("Configuration Error"
--- a/test/sun/security/krb5/auto/Context.java	Wed Nov 12 16:00:22 2008 +0800
+++ b/test/sun/security/krb5/auto/Context.java	Wed Nov 12 16:01:06 2008 +0800
@@ -109,13 +109,22 @@
         out.s = new Subject();
         Krb5LoginModule krb5 = new Krb5LoginModule();
         Map<String, String> map = new HashMap<String, String>();
-        map.put("tryFirstPass", "true");
+        Map<String, Object> shared = new HashMap<String, Object>();
+
+        if (pass != null) {
+            map.put("useFirstPass", "true");
+            shared.put("javax.security.auth.login.name", user);
+            shared.put("javax.security.auth.login.password", pass);
+        } else {
+            map.put("doNotPrompt", "true");
+            map.put("useTicketCache", "true");
+            if (user != null) {
+                map.put("principal", user);
+            }
+        }
         if (storeKey) {
             map.put("storeKey", "true");
         }
-        Map<String, Object> shared = new HashMap<String, Object>();
-        shared.put("javax.security.auth.login.name", user);
-        shared.put("javax.security.auth.login.password", pass);
 
         krb5.initialize(out.s, null, shared, map);
         krb5.login();
--- a/test/sun/security/krb5/auto/KDC.java	Wed Nov 12 16:00:22 2008 +0800
+++ b/test/sun/security/krb5/auto/KDC.java	Wed Nov 12 16:01:06 2008 +0800
@@ -32,6 +32,7 @@
 import java.util.concurrent.*;
 import sun.security.krb5.*;
 import sun.security.krb5.internal.*;
+import sun.security.krb5.internal.ccache.CredentialsCache;
 import sun.security.krb5.internal.crypto.KeyUsage;
 import sun.security.krb5.internal.ktab.KeyTab;
 import sun.security.util.DerInputStream;
@@ -765,7 +766,29 @@
             DerOutputStream out = new DerOutputStream();
             out.write(DerValue.createTag(DerValue.TAG_APPLICATION,
                     true, (byte)Krb5.KRB_AS_REP), asRep.asn1Encode());
-            return out.toByteArray();
+            byte[] result = out.toByteArray();
+
+            // Added feature:
+            // Write the current issuing TGT into a ccache file specified
+            // by the system property below.
+            String ccache = System.getProperty("test.kdc.save.ccache");
+            if (ccache != null) {
+                asRep.encKDCRepPart = enc_part;
+                sun.security.krb5.internal.ccache.Credentials credentials =
+                    new sun.security.krb5.internal.ccache.Credentials(asRep);
+                asReq.reqBody.cname.setRealm(getRealm());
+                CredentialsCache cache =
+                    CredentialsCache.create(asReq.reqBody.cname, ccache);
+                if (cache == null) {
+                   throw new IOException("Unable to create the cache file " +
+                                         ccache);
+                }
+                cache.update(credentials);
+                cache.save();
+                new File(ccache).deleteOnExit();
+            }
+
+            return result;
         } catch (KrbException ke) {
             ke.printStackTrace(System.out);
             KRBError kerr = ke.getError();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/sun/security/krb5/auto/LoginModuleOptions.java	Wed Nov 12 16:01:06 2008 +0800
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2008 Sun Microsystems, Inc.  All Rights Reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
+ * CA 95054 USA or visit www.sun.com if you need additional information or
+ * have any questions.
+ */
+
+/*
+ * @test
+ * @bug 6765491
+ * @summary Krb5LoginModule a little too restrictive, and the doc is not clear.
+ */
+import com.sun.security.auth.module.Krb5LoginModule;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+
+public class LoginModuleOptions {
+
+    private static final String NAME = "javax.security.auth.login.name";
+    private static final String PWD = "javax.security.auth.login.password";
+
+    public static void main(String[] args) throws Exception {
+        OneKDC kdc = new OneKDC(null);
+        kdc.addPrincipal("foo", "bar".toCharArray());
+        kdc.writeKtab(OneKDC.KTAB); // rewrite to add foo
+
+        // All 4 works: keytab, shared state, callback, cache
+        login(null, "useKeyTab", "true", "principal", "dummy");
+        login(null, "tryFirstPass", "true", NAME, OneKDC.USER,
+                PWD, OneKDC.PASS);
+        System.setProperty("test.kdc.save.ccache", "krbcc");
+        login(new MyCallback(OneKDC.USER, OneKDC.PASS));    // save the cache
+        System.clearProperty("test.kdc.save.ccache");
+        login(null, "useTicketCache", "true", "ticketCache", "krbcc");
+
+        // Fallbacks
+        // 1. ccache -> keytab
+        login(null, "useTicketCache", "true", "ticketCache", "krbcc_non_exists",
+                "useKeyTab", "true", "principal", "dummy");
+        // 2. keytab -> shared
+        login(null, "useKeyTab", "true", "principal", "dummy",
+                "keyTab", "ktab_non_exist",
+                "tryFirstPass", "true", NAME, OneKDC.USER, PWD, OneKDC.PASS);
+        // 3. shared -> callback
+        // 3.1. useFirstPass, no callback
+        boolean failed = false;
+        try {
+            login(new MyCallback(OneKDC.USER, OneKDC.PASS),
+                    "useFirstPass", "true",
+                    NAME, OneKDC.USER, PWD, "haha".toCharArray());
+        } catch (Exception e) {
+            failed = true;
+        }
+        if (!failed) {
+            throw new Exception("useFirstPass should not fallback to callback");
+        }
+        // 3.2. tryFirstPass, has callback
+        login(new MyCallback(OneKDC.USER, OneKDC.PASS),
+                "tryFirstPass", "true",
+                NAME, OneKDC.USER, PWD, "haha".toCharArray());
+
+        // Preferences of type
+        // 1. ccache preferred to keytab
+        login(new MyCallback("foo", null),
+                "useTicketCache", "true", "ticketCache", "krbcc",
+                "useKeyTab", "true");
+        // 2. keytab preferred to shared. This test case is not exactly correct,
+        // because principal=dummy would shadow the PWD setting in the shared
+        // state. So by only looking at the final authentication user name
+        // (which is how this program does), there's no way to tell if keyTab
+        // is picked first, or shared is tried first but fallback to keytab.
+        login(null, "useKeyTab", "true", "principal", "dummy",
+                "tryFirstPass", "true", NAME, "foo", PWD, "bar".toCharArray());
+        // 3. shared preferred to callback
+        login(new MyCallback("foo", "bar".toCharArray()),
+                "tryFirstPass", "true", NAME, OneKDC.USER, PWD, OneKDC.PASS);
+
+        // Preferences of username
+        // 1. principal preferred to NAME (NAME can be wrong or missing)
+        login(null, "principal", OneKDC.USER,
+                "tryFirstPass", "true", NAME, "someone_else", PWD, OneKDC.PASS);
+        login(null, "principal", OneKDC.USER,
+                "tryFirstPass", "true", PWD, OneKDC.PASS);
+        // 2. NAME preferred to callback
+        login(new MyCallback("someone_else", OneKDC.PASS),
+                "principal", OneKDC.USER);
+        // 3. With tryFirstPass, NAME preferred to callback
+        login(new MyCallback("someone_else", null),
+                "tryFirstPass", "true", NAME, OneKDC.USER, PWD, OneKDC.PASS);
+        // 3.1. you must provide a NAME (when there's no principal)
+        failed = false;
+        try {
+            login(new MyCallback(OneKDC.USER, null),
+                    "tryFirstPass", "true", PWD, OneKDC.PASS);
+        } catch (Exception e) {
+            failed = true;
+        }
+        if (!failed) {
+            throw new Exception("useFirstPass must provide a NAME");
+        }
+        // 3.2 Hybrid, you can use NAME as "", and provide it using callback.
+        // I don't think this is designed.
+        login(new MyCallback(OneKDC.USER, null),
+                "tryFirstPass", "true", NAME, "", PWD, OneKDC.PASS);
+
+        // Test for the bug fix: doNotPrompt can be true if tryFirstPass=true
+        login(null, "doNotPrompt", "true", "storeKey", "true",
+                "tryFirstPass", "true", NAME, OneKDC.USER, PWD, OneKDC.PASS);
+    }
+
+    static void login(CallbackHandler callback, Object... options)
+            throws Exception {
+        Krb5LoginModule krb5 = new Krb5LoginModule();
+        Subject subject = new Subject();
+        Map<String, String> map = new HashMap<String, String>();
+        Map<String, Object> shared = new HashMap<String, Object>();
+
+        int count = options.length / 2;
+        for (int i = 0; i < count; i++) {
+            String key = (String) options[2 * i];
+            Object value = options[2 * i + 1];
+            if (key.startsWith("javax")) {
+                shared.put(key, value);
+            } else {
+                map.put(key, (String) value);
+            }
+        }
+        krb5.initialize(subject, callback, shared, map);
+        krb5.login();
+        krb5.commit();
+        if (!subject.getPrincipals().iterator().next()
+                .getName().startsWith(OneKDC.USER)) {
+            throw new Exception("The authenticated is not " + OneKDC.USER);
+        }
+    }
+
+    static class MyCallback implements CallbackHandler {
+
+        private String name;
+        private char[] password;
+
+        public MyCallback(String name, char[] password) {
+            this.name = name;
+            this.password = password;
+        }
+
+        public void handle(Callback[] callbacks) {
+            for (Callback callback : callbacks) {
+                System.err.println(callback);
+                if (callback instanceof NameCallback) {
+                    System.err.println("name is " + name);
+                    ((NameCallback) callback).setName(name);
+                }
+                if (callback instanceof PasswordCallback) {
+                    System.err.println("pass is " + new String(password));
+                    ((PasswordCallback) callback).setPassword(password);
+                }
+            }
+        }
+    }
+}