From: markt Date: Thu, 31 Mar 2011 18:02:10 +0000 (+0000) Subject: Switch SPNEGO authenticator to use file based JAAS config as this provides greater... X-Git-Url: https://git.internetallee.de/?a=commitdiff_plain;h=1b2abe89855f9e4ed1aad927b3a3eb362ca55343;p=tomcat7.0 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. git-svn-id: https://svn.apache.org/repos/asf/tomcat/trunk@1087392 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/java/org/apache/catalina/authenticator/Constants.java b/java/org/apache/catalina/authenticator/Constants.java index ae19fb12e..2d33a6b04 100644 --- a/java/org/apache/catalina/authenticator/Constants.java +++ b/java/org/apache/catalina/authenticator/Constants.java @@ -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 = diff --git a/java/org/apache/catalina/authenticator/LocalStrings.properties b/java/org/apache/catalina/authenticator/LocalStrings.properties index b94207a26..3d084845b 100644 --- a/java/org/apache/catalina/authenticator/LocalStrings.properties +++ b/java/org/apache/catalina/authenticator/LocalStrings.properties @@ -20,6 +20,7 @@ authenticator.invalid=Invalid client certificate chain in this request 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 request violates a User Data constraint fo 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 diff --git a/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java b/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java index d4dff6e9a..b30cfd322 100644 --- a/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java +++ b/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java @@ -18,22 +18,12 @@ package org.apache.catalina.authenticator; 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; *
  • Does the SPN have to start with HTTP/...?
  • *
  • Can a port number be appended to the end of the host in the SPN?
  • *
  • Can the domain be left off the user in the ktpass command?
  • - *
  • Can -Djava.security.krb5.conf be used to change the location of krb5.ini? - *
  • *
  • What are the limitations on the account that Tomcat can run as? SPN * associated account works, domain admin works, local admin doesn't * work
  • @@ -80,12 +69,15 @@ public class SpnegoAuthenticator extends AuthenticatorBase { 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 AuthenticatorBase { } - 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; + // 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()); } - 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); + // 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 AuthenticatorBase { 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(); - private static class KerberosAuthAction - implements PrivilegedExceptionAction { + 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 byte[] inToken; - private HttpServletResponse resp; - private Context context; + authorizationBC.setOffset(authorizationBC.getOffset() + 10); + // FIXME: Add trimming + // authorizationBC.trim(); + + ByteChunk decoded = new ByteChunk(); + Base64.decode(authorizationBC, decoded); - 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); + 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)); - Principal principal = null; - - 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 options = new HashMap(); - 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; } } diff --git a/webapps/docs/config/valve.xml b/webapps/docs/config/valve.xml index 249bd30aa..b3a77aa3c 100644 --- a/webapps/docs/config/valve.xml +++ b/webapps/docs/config/valve.xml @@ -849,6 +849,12 @@ true will be used.

    + +

    The name of the JAAS login configuration to be used to login as the + service. If not specified, the default of + com.sun.security.jgss.krb5.accept is used.

    +
    +

    Controls the caching of pages that are protected by security constraints. Setting this to false may help work around @@ -885,22 +891,6 @@ specified, the platform default provider will be used.

    - -

    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 - $CATALINA_BASE. If not specified, the default value of - conf/tomcat.keytab is used.

    -
    - - -

    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 HTTP/<hostname> where - <hostname> is obtained using - InetAddress.getLocalHost().getCanonicalHostName().

    -
    - diff --git a/webapps/docs/windows-auth-howto.xml b/webapps/docs/windows-auth-howto.xml index df3d06867..a954f5279 100644 --- a/webapps/docs/windows-auth-howto.xml +++ b/webapps/docs/windows-auth-howto.xml @@ -107,9 +107,9 @@ policy had to be relaxed. This is not recommended for production environments. user. The steps to configure the Tomcat instance for Windows authentication are as follows:
  • Copy the tomcat.keytab file created on the domain controller - to $CATALINA_BASE/conf.
  • + to $CATALINA_BASE/conf/tomcat.keytab.
  • Create the kerberos configuration file - C:\Windows\krb5.ini. The file used in this how-to + $CATALINA_BASE/conf/krb5.ini. The file used in this how-to contained:[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
  • +.dev.local= DEV.LOCAL + The location of this file can be changed by setting the + java.security.krb5.conf systm property. +
  • Create the JAAS login configuration file + $CATALINA_BASE/conf/jaas.conf. The file used in this how-to + contained: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; +}; + The location of this file can be changed by setting the + java.security.auth.login.config 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 authentication + valve.
  • +
  • The system property javax.security.auth.useSubjectCredsOnly + is automatically set to the required value of false if a web application is + configured to use the SPNEGO authentication method.
  • 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.

    @@ -156,8 +186,10 @@ dev.local= DEV.LOCAL
    1. IIS and Kerberos
    2. -
    3. - Spring Security Kerberos extension
    4. +
    5. + SPNEGO project at SourceForge
    6. +
    7. + Oracle JGSS tutorial
    8. Geronimo configuration for Windows authentication
    9. @@ -194,6 +226,17 @@ dev.local= DEV.LOCAL

      + + +

      Full details of this solution can be found through the + project site. The key + features are: +

        +
      • Uses Kerberos
      • +
      • Pure Java solution
      • +
      +

      +