Author: markt
Date: Thu Mar 31 18:02:10 2011
New Revision: 1087392

URL: http://svn.apache.org/viewvc?rev=1087392&view=rev
Log:
Switch SPNEGO authenticator to use file based JAAS config as this provides 
greater flexibility including making it easier to work with non-Oracle JVMs.
Clean up the code, add debug logging and improve error handling.

Modified:
    tomcat/trunk/java/org/apache/catalina/authenticator/Constants.java
    tomcat/trunk/java/org/apache/catalina/authenticator/LocalStrings.properties
    tomcat/trunk/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java
    tomcat/trunk/webapps/docs/config/valve.xml
    tomcat/trunk/webapps/docs/windows-auth-howto.xml

Modified: tomcat/trunk/java/org/apache/catalina/authenticator/Constants.java
URL: 
http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/authenticator/Constants.java?rev=1087392&r1=1087391&r2=1087392&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/catalina/authenticator/Constants.java 
(original)
+++ tomcat/trunk/java/org/apache/catalina/authenticator/Constants.java Thu Mar 
31 18:02:10 2011
@@ -40,6 +40,15 @@ public class Constants {
     // SPNEGO authentication constants
     public static final String DEFAULT_KEYTAB = "conf/tomcat.keytab";
     public static final String DEFAULT_SPN_CLASS = "HTTP";
+    public static final String KRB5_CONF_PROPERTY = "java.security.krb5.conf";
+    public static final String DEFAULT_KRB5_CONF = "conf/krb5.ini";
+    public static final String JAAS_CONF_PROPERTY =
+            "java.security.auth.login.config";
+    public static final String DEFAULT_JAAS_CONF = "conf/jaas.conf";
+    public static final String DEFAULT_LOGIN_MODULE_NAME =
+        "com.sun.security.jgss.krb5.accept";
+    public static final String USE_SUBJECT_CREDS_ONLY_PROPERTY =
+            "javax.security.auth.useSubjectCredsOnly";
 
     // Cookie name for single sign on support
     public static final String SINGLE_SIGN_ON_COOKIE =

Modified: 
tomcat/trunk/java/org/apache/catalina/authenticator/LocalStrings.properties
URL: 
http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/authenticator/LocalStrings.properties?rev=1087392&r1=1087391&r2=1087392&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/catalina/authenticator/LocalStrings.properties 
(original)
+++ tomcat/trunk/java/org/apache/catalina/authenticator/LocalStrings.properties 
Thu Mar 31 18:02:10 2011
@@ -20,6 +20,7 @@ authenticator.invalid=Invalid client cer
 authenticator.loginFail=Login failed
 authenticator.keystore=Exception loading key store
 authenticator.manager=Exception initializing trust managers
+authenticator.noAuthHeader=No authorization header sent by client
 authenticator.notAuthenticated=Configuration error:  Cannot perform access 
control without an authenticated principal
 authenticator.notContext=Configuration error:  Must be attached to a Context
 authenticator.requestBodyTooBig=The request body was too large to be cached 
during the authentication process
@@ -30,6 +31,8 @@ authenticator.userDataConstraint=This re
 formAuthenticator.forwardErrorFail=Unexpected error forwarding to error page
 formAuthenticator.forwardLoginFail=Unexpected error forwarding to login page
 
+spnegoAuthenticator.authHeaderNoToken=The Negotiate authorization header sent 
by the client did include a token
+spnegoAuthenticator.authHeaderNotNego=The authorization header sent by the 
client did not start with Negotiate
 spnegoAuthenticator.hostnameFail=Unable to determine the host name to 
construct the default SPN. Please set the spn attribute of the authenticator.
 spnegoAuthenticator.serviceLoginFail=Unable to login as the service principal
 spnegoAuthenticator.ticketValidateFail=Failed to validate client supplied 
ticket
\ No newline at end of file

Modified: 
tomcat/trunk/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java
URL: 
http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java?rev=1087392&r1=1087391&r2=1087392&view=diff
==============================================================================
--- 
tomcat/trunk/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java 
(original)
+++ 
tomcat/trunk/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java 
Thu Mar 31 18:02:10 2011
@@ -18,22 +18,12 @@ package org.apache.catalina.authenticato
 
 import java.io.File;
 import java.io.IOException;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
 import java.security.Principal;
-import java.security.PrivilegedActionException;
-import java.security.PrivilegedExceptionAction;
-import java.util.HashMap;
-import java.util.Map;
-
-import javax.security.auth.Subject;
-import javax.security.auth.login.AppConfigurationEntry;
-import javax.security.auth.login.Configuration;
+
 import javax.security.auth.login.LoginContext;
 import javax.security.auth.login.LoginException;
 import javax.servlet.http.HttpServletResponse;
 
-import org.apache.catalina.Context;
 import org.apache.catalina.LifecycleException;
 import org.apache.catalina.connector.Request;
 import org.apache.catalina.deploy.LoginConfig;
@@ -47,6 +37,7 @@ import org.ietf.jgss.GSSContext;
 import org.ietf.jgss.GSSCredential;
 import org.ietf.jgss.GSSException;
 import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.Oid;
 
 
 /**
@@ -69,8 +60,6 @@ import org.ietf.jgss.GSSManager;
  * <li>Does the SPN have to start with HTTP/...?</li>
  * <li>Can a port number be appended to the end of the host in the SPN?</li>
  * <li>Can the domain be left off the user in the ktpass command?</li>
- * <li>Can -Djava.security.krb5.conf be used to change the location of 
krb5.ini?
- *     </li>
  * <li>What are the limitations on the account that Tomcat can run as? SPN
  *     associated account works, domain admin works, local admin doesn't
  *     work</li>
@@ -80,12 +69,15 @@ public class SpnegoAuthenticator extends
 
     private static final Log log = 
LogFactory.getLog(SpnegoAuthenticator.class);
     
-    protected String serviceKeyTab = Constants.DEFAULT_KEYTAB;
-    protected String spn = null;
+    private String loginConfigName = Constants.DEFAULT_LOGIN_MODULE_NAME;
+    public String getLoginConfigName() {
+        return loginConfigName;
+    }
+    public void setLoginConfigName(String loginConfigName) {
+        this.loginConfigName = loginConfigName;
+    }
 
-    protected Subject serviceSubject = null;
 
-    
     @Override
     protected String getAuthMethod() {
         return Constants.SPNEGO_METHOD;
@@ -98,65 +90,32 @@ public class SpnegoAuthenticator extends
     }
 
 
-    public String getServiceKeyTab() {
-        return serviceKeyTab;
-    }
-
-
-    public void setServiceKeyTab(String serviceKeyTab) {
-        this.serviceKeyTab = serviceKeyTab;
-    }
-
-
-    public String getSpn() {
-        return spn;
-    }
-
-
-    public void setSpn(String spn) {
-        this.spn = spn;
-    }
-
-
     @Override
     protected void initInternal() throws LifecycleException {
         super.initInternal();
 
-        // Service keytab needs to be an absolute file name
-        File serviceKeyTabFile = new File(serviceKeyTab);
-        if (!serviceKeyTabFile.isAbsolute()) {
-            serviceKeyTabFile =
-                new File(Bootstrap.getCatalinaBase(), serviceKeyTab);
-        }
-
-        // SPN is HTTP/hostname
-        String serviceProvideName;
-        if (spn == null || spn.length() == 0) {
-            // Construct default
-            StringBuilder name = new 
StringBuilder(Constants.DEFAULT_SPN_CLASS);
-            name.append('/');
-            try {
-                name.append(InetAddress.getLocalHost().getCanonicalHostName());
-            } catch (UnknownHostException e) {
-                throw new LifecycleException(
-                        sm.getString("spnegoAuthenticator.hostnameFail"), e);
-            }
-            serviceProvideName = name.toString();
-        } else {
-            serviceProvideName = spn;
-        }
-
-        LoginContext lc;
-        try {
-            lc = new LoginContext("", null, null,
-                    new JaasConfig(serviceKeyTabFile.getAbsolutePath(),
-                            serviceProvideName, log.isDebugEnabled()));
-            lc.login();
-            serviceSubject = lc.getSubject();
-        } catch (LoginException e) {
-            throw new LifecycleException(
-                    sm.getString("spnegoAuthenticator.serviceLoginFail"), e);
+        // Kerberos configuration file location
+        String krb5Conf = System.getProperty(Constants.KRB5_CONF_PROPERTY);
+        if (krb5Conf == null) {
+            // System property not set, use the Tomcat default
+            File krb5ConfFile = new File(Bootstrap.getCatalinaBase(),
+                    Constants.DEFAULT_KRB5_CONF);
+            System.setProperty(Constants.KRB5_CONF_PROPERTY,
+                    krb5ConfFile.getAbsolutePath());
+        }
+
+        // JAAS configuration file location
+        String jaasConf = System.getProperty(Constants.JAAS_CONF_PROPERTY);
+        if (jaasConf == null) {
+            // System property not set, use the Tomcat default
+            File jaasConfFile = new File(Bootstrap.getCatalinaBase(),
+                    Constants.DEFAULT_JAAS_CONF);
+            System.setProperty(Constants.JAAS_CONF_PROPERTY,
+                    jaasConfFile.getAbsolutePath());
         }
+        
+        // This property must be false for SPNEGO to work
+        System.setProperty(Constants.USE_SUBJECT_CREDS_ONLY_PROPERTY, "false");
     }
 
 
@@ -195,122 +154,118 @@ public class SpnegoAuthenticator extends
             request.getCoyoteRequest().getMimeHeaders()
             .getValue("authorization");
         
-        if (authorization != null) {
-            authorization.toBytes();
-            ByteChunk authorizationBC = authorization.getByteChunk();
-            if (authorizationBC.startsWithIgnoreCase("negotiate ", 0)) {
-                authorizationBC.setOffset(authorizationBC.getOffset() + 10);
-                // FIXME: Add trimming
-                // authorizationBC.trim();
-                
-                ByteChunk decoded = new ByteChunk();
-                Base64.decode(authorizationBC, decoded);
-
-                try {
-                    principal = Subject.doAs(serviceSubject,
-                            new KerberosAuthAction(decoded.getBytes(),
-                                    response, context));
-                } catch (PrivilegedActionException e) {
-                    if (log.isDebugEnabled()) {
-                        log.debug(sm.getString(
-                                "spnegoAuthenticator.ticketValidateFail"));
-                    }
-                }
-                
-                if (principal != null) {
-                    register(request, response, principal, 
Constants.SPNEGO_METHOD,
-                            principal.getName(), null);
-                    return true;
-                }
-            } else {
-                response.setHeader("WWW-Authenticate", "Negotiate");
+        if (authorization == null) {
+            if (log.isDebugEnabled()) {
+                log.debug(sm.getString("authenticator.noAuthHeader"));
             }
-        } else {
             response.setHeader("WWW-Authenticate", "Negotiate");
+            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+            return false;
         }
-        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
-        return false;
-    }
+        
+        authorization.toBytes();
+        ByteChunk authorizationBC = authorization.getByteChunk();
 
+        if (!authorizationBC.startsWithIgnoreCase("negotiate ", 0)) {
+            if (log.isDebugEnabled()) {
+                log.debug(sm.getString(
+                        "spnegoAuthenticator.authHeaderNotNego"));
+            }
+            response.setHeader("WWW-Authenticate", "Negotiate");
+            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+            return false;
+        }
 
-    private static class KerberosAuthAction
-            implements PrivilegedExceptionAction<Principal> {
+        authorizationBC.setOffset(authorizationBC.getOffset() + 10);
+        // FIXME: Add trimming
+        // authorizationBC.trim();
+                
+        ByteChunk decoded = new ByteChunk();
+        Base64.decode(authorizationBC, decoded);
 
-        private byte[] inToken;
-        private HttpServletResponse resp;
-        private Context context;
-
-        public KerberosAuthAction(byte[] inToken, HttpServletResponse resp,
-                Context context) {
-            this.inToken = inToken;
-            this.resp = resp;
-            this.context = context;
+        if (decoded.getLength() == 0) {
+            if (log.isDebugEnabled()) {
+                log.debug(sm.getString(
+                        "spnegoAuthenticator.authHeaderNoToken"));
+            }
+            response.setHeader("WWW-Authenticate", "Negotiate");
+            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+            return false;
         }
 
-        @Override
-        public Principal run() throws Exception {
-
+        LoginContext lc = null;
+        GSSContext gssContext = null;
+        byte[] outToken = null;
+        try {
+            try {
+                lc = new LoginContext(loginConfigName);
+                lc.login();
+            } catch (LoginException e) {
+                log.error(sm.getString("spnegoAuthenticator.serviceLoginFail"),
+                        e);
+                response.sendError(
+                        HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+                return false;
+            }
             // Assume the GSSContext is stateless
             // TODO: Confirm this assumption
-            GSSContext gssContext =
-                GSSManager.getInstance().createContext((GSSCredential) null);
-
-            Principal principal = null;
+            GSSManager manager = GSSManager.getInstance();
+            gssContext = manager.createContext(manager.createCredential(null,
+                    GSSCredential.DEFAULT_LIFETIME,
+                    new Oid("1.3.6.1.5.5.2"),
+                    GSSCredential.ACCEPT_ONLY));
 
-            if (inToken == null) {
-                throw new IllegalArgumentException("inToken cannot be null");
-            }
-
-            byte[] outToken =
-                gssContext.acceptSecContext(inToken, 0, inToken.length);
+            outToken = gssContext.acceptSecContext(decoded.getBytes(),
+                    decoded.getOffset(), decoded.getLength());
 
             if (outToken == null) {
-                throw new GSSException(GSSException.DEFECTIVE_TOKEN);
+                if (log.isDebugEnabled()) {
+                    log.debug(sm.getString(
+                            "spnegoAuthenticator.ticketValidateFail"));
+                }
+                // Start again
+                response.setHeader("WWW-Authenticate", "Negotiate");
+                response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+                return false;
             }
 
             principal = context.getRealm().authenticate(gssContext);
-
-            // Send response token on success and failure
-            resp.setHeader("WWW-Authenticate", "Negotiate "
-                    + Base64.encode(outToken));
-
-            gssContext.dispose();
-            return principal;
+        } catch (GSSException e) {
+            if (log.isDebugEnabled()) {
+                
log.debug(sm.getString("spnegoAuthenticator.ticketValidateFail",
+                        e));
+            }
+            response.setHeader("WWW-Authenticate", "Negotiate");
+            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+            return false;
+        } finally {
+            if (gssContext != null) {
+                try {
+                    gssContext.dispose();
+                } catch (GSSException e) {
+                    // Ignore
+                }
+            }
+            if (lc != null) {
+                try {
+                    lc.logout();
+                } catch (LoginException e) {
+                    // Ignore
+                }
+            }
         }
-    }
 
+        // Send response token on success and failure
+        response.setHeader("WWW-Authenticate", "Negotiate "
+                + Base64.encode(outToken));
 
-    /**
-     * Provides the JAAS login configuration required to create
-     */
-    private static class JaasConfig extends Configuration {
-
-        private String keytab;
-        private String spn;
-        private boolean debug;
-
-        public JaasConfig(String keytab, String spn, boolean debug) {
-            this.keytab = keytab;
-            this.spn = spn;
-            this.debug = debug;
+        if (principal != null) {
+            register(request, response, principal, Constants.SPNEGO_METHOD,
+                    principal.getName(), null);
+            return true;
         }
 
-        @Override
-        public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
-            Map<String, String> options = new HashMap<String, String>();
-            options.put("useKeyTab", "true");
-            options.put("keyTab", keytab);
-            options.put("principal", spn);
-            options.put("storeKey", "true");
-            options.put("doNotPrompt", "true");
-            options.put("isInitiator", "false");
-            options.put("debug", Boolean.toString(debug));
-
-            return new AppConfigurationEntry[] {
-                    new AppConfigurationEntry(
-                        "com.sun.security.auth.module.Krb5LoginModule",
-                        AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
-                        options) };
-        }
+        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+        return false;
     }
 }

Modified: tomcat/trunk/webapps/docs/config/valve.xml
URL: 
http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/config/valve.xml?rev=1087392&r1=1087391&r2=1087392&view=diff
==============================================================================
--- tomcat/trunk/webapps/docs/config/valve.xml (original)
+++ tomcat/trunk/webapps/docs/config/valve.xml Thu Mar 31 18:02:10 2011
@@ -849,6 +849,12 @@
         <code>true</code> will be used.</p>
       </attribute>
 
+      <attribute name="loginConfigName" required="false">
+        <p>The name of the JAAS login configuration to be used to login as the
+        service. If not specified, the default of
+        <code>com.sun.security.jgss.krb5.accept</code> is used.</p>
+      </attribute>
+
       <attribute name="securePagesWithPragma" required="false">
         <p>Controls the caching of pages that are protected by security
         constraints. Setting this to <code>false</code> may help work around
@@ -885,22 +891,6 @@
         specified, the platform default provider will be used.</p>
       </attribute>
 
-      <attribute name="serviceKeyTab" required="false">
-        <p>Name of the Kerberos keytab file that contains the private key for
-        the service principal. The name of the service principal must match the
-        spn attribute. Relative file names are relative to
-        <code>$CATALINA_BASE</code>. If not specified, the default value of
-        <code>conf/tomcat.keytab</code> is used.</p>
-      </attribute>
-
-      <attribute name="spn" required="false">
-        <p>Service Principal Name (SPN) for this server. It must match the SPN
-        associated with the key in the serviceKetTab file. If not specified, 
the
-        default value of <code>HTTP/&lt;hostname&gt;</code> where
-        <code>&lt;hostname&gt;</code> is obtained using
-        <code>InetAddress.getLocalHost().getCanonicalHostName()</code>.</p>
-      </attribute>
-
     </attributes>
 
   </subsection>

Modified: tomcat/trunk/webapps/docs/windows-auth-howto.xml
URL: 
http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/windows-auth-howto.xml?rev=1087392&r1=1087391&r2=1087392&view=diff
==============================================================================
--- tomcat/trunk/webapps/docs/windows-auth-howto.xml (original)
+++ tomcat/trunk/webapps/docs/windows-auth-howto.xml Thu Mar 31 18:02:10 2011
@@ -107,9 +107,9 @@ policy had to be relaxed. This is not re
   user. The steps to configure the Tomcat instance for Windows authentication
   are as follows:
   <li>Copy the <code>tomcat.keytab</code> file created on the domain controller
-  to <code>$CATALINA_BASE/conf</code>.</li>
+  to <code>$CATALINA_BASE/conf/tomcat.keytab</code>.</li>
   <li>Create the kerberos configuration file
-  <code>C:\Windows\krb5.ini</code>. The file used in this how-to
+  <code>$CATALINA_BASE/conf/krb5.ini</code>. The file used in this how-to
   contained:<source>[libdefaults]
 default_realm = DEV.LOCAL
 default_keytab_name = FILE:c:\apache-tomcat-7.0.x\conf\tomcat.keytab
@@ -124,7 +124,37 @@ DEV.LOCAL = {
 
 [domain_realm]
 dev.local= DEV.LOCAL
-.dev.local= DEV.LOCAL</source></li>
+.dev.local= DEV.LOCAL</source>
+  The location of this file can be changed by setting the
+  <code>java.security.krb5.conf</code> systm property.</li>
+  <li>Create the JAAS login configuration file
+  <code>$CATALINA_BASE/conf/jaas.conf</code>. The file used in this how-to
+  contained:<source>com.sun.security.jgss.krb5.initiate {
+    com.sun.security.auth.module.Krb5LoginModule required
+    doNotPrompt=true
+    principal="HTTP/win-tc01.dev.local@DEV.LOCAL"
+    useKeyTab=true
+    keyTab="c:/apache-tomcat-7.0.x/conf/tomcat.keytab"
+    storeKey=true;
+};
+
+com.sun.security.jgss.krb5.accept {
+    com.sun.security.auth.module.Krb5LoginModule required
+    doNotPrompt=true
+    principal="HTTP/win-tc01.dev.local@DEV.LOCAL"
+    useKeyTab=true
+    keyTab="c:/apache-tomcat-7.0.x/conf/tomcat.keytab"
+    storeKey=true;
+};</source>
+  The location of this file can be changed by setting the
+  <code>java.security.auth.login.config</code> system property. The LoginModule
+  used is a JVM specific one so ensure that the LoginModule specified matches
+  the JVM being used. The name of the login configuration must match the
+  value used by the <a href="config/valve.html#SPNEGO_Valve">authentication
+  valve</a>.</li>
+  <li>The system property <code>javax.security.auth.useSubjectCredsOnly</code>
+  is automatically set to the required value of false if a web application is
+  configured to use the SPNEGO authentication method.</li>
   </p>
   <p>The above steps have been tested on a Tomcat server running Windows Server
   2008 R2 64-bit Standard with an Oracle 1.6.0_24 64-bit JDK.</p>
@@ -156,8 +186,10 @@ dev.local= DEV.LOCAL
   <ol>
   <li><a 
href="http://www.adopenstatic.com/cs/blogs/ken/archive/2006/10/19/512.aspx";>
       IIS and Kerberos</a></li>
-  <li><a 
href="http://blog.springsource.com/2009/09/28/spring-security-kerberos/";>
-      Spring Security Kerberos extension</a></li>
+  <li><a href="http://spnego.sourceforge.net/index.html";>
+      SPNEGO project at SourceForge</a></li>
+  <li><a 
href="http://download.oracle.com/javase/1.5.0/docs/guide/security/jgss/tutorials/index.html";>
+      Oracle JGSS tutorial</a></li>
   <li><a 
href="https://cwiki.apache.org/GMOxDOC21/using-spengo-in-geronimo.html#UsingSpengoingeronimo-SettinguptheActiveDirectoryDomainController";>
       Geronimo configuration for Windows authentication</a></li>
   <li><a 
href="http://blogs.msdn.com/b/openspecification/archive/2010/11/17/encryption-type-selection-in-kerberos-exchanges.aspx";>
@@ -194,6 +226,17 @@ dev.local= DEV.LOCAL
   </ul>
   </p>
   </subsection>
+  
+  <subsection name="SPNEGO project at SourceForge">
+  <p>Full details of this solution can be found through the
+  <a href="http://spnego.sourceforge.net/index.html/";>project site</a>. The key
+  features are:
+  <ul>
+  <li>Uses Kerberos</li>
+  <li>Pure Java solution</li>
+  </ul>
+  </p>
+  </subsection>
 </section>
 
 <section name="Reverse proxies">



---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to