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/<hostname></code> where - <code><hostname></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