--- AuthenticatorBase.java 2007-09-20 17:14:22.551592203 +0200 +++ /var/local/apache-tomcat-5.5.23-src/container/catalina/src/share/org/apache/catalina/authenticator/AuthenticatorBase.java 2007-09-14 16:48:01.591807635 +0200 @@ -1,887 +1,905 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - -package org.apache.catalina.authenticator; - - -import java.io.IOException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.Principal; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; -import java.util.Random; - -import javax.servlet.ServletException; -import javax.servlet.http.Cookie; - -import org.apache.catalina.Authenticator; -import org.apache.catalina.Container; -import org.apache.catalina.Context; -import org.apache.catalina.Lifecycle; -import org.apache.catalina.LifecycleException; -import org.apache.catalina.LifecycleListener; -import org.apache.catalina.Pipeline; -import org.apache.catalina.Realm; -import org.apache.catalina.Session; -import org.apache.catalina.Valve; -import org.apache.catalina.connector.Request; -import org.apache.catalina.connector.Response; -import org.apache.catalina.deploy.LoginConfig; -import org.apache.catalina.deploy.SecurityConstraint; -import org.apache.catalina.util.DateTool; -import org.apache.catalina.util.LifecycleSupport; -import org.apache.catalina.util.StringManager; -import org.apache.catalina.valves.ValveBase; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - - -/** - * Basic implementation of the Valve interface that enforces the - * <security-constraint> elements in the web application - * deployment descriptor. This functionality is implemented as a Valve - * so that it can be ommitted in environments that do not require these - * features. Individual implementations of each supported authentication - * method can subclass this base class as required. - *

- * USAGE CONSTRAINT: When this class is utilized, the Context to - * which it is attached (or a parent Container in a hierarchy) must have an - * associated Realm that can be used for authenticating users and enumerating - * the roles to which they have been assigned. - *

- * USAGE CONSTRAINT: This Valve is only useful when processing HTTP - * requests. Requests of any other type will simply be passed through. - * - * @author Craig R. McClanahan - * @version $Revision: 496025 $ $Date: 2007-01-13 20:18:06 -0700 (Sat, 13 Jan 2007) $ - */ - - -public abstract class AuthenticatorBase - extends ValveBase - implements Authenticator, Lifecycle { - private static Log log = LogFactory.getLog(AuthenticatorBase.class); - - - // ----------------------------------------------------- Instance Variables - - - /** - * The default message digest algorithm to use if we cannot use - * the requested one. - */ - protected static final String DEFAULT_ALGORITHM = "MD5"; - - - /** - * The number of random bytes to include when generating a - * session identifier. - */ - protected static final int SESSION_ID_BYTES = 16; - - - /** - * The message digest algorithm to be used when generating session - * identifiers. This must be an algorithm supported by the - * java.security.MessageDigest class on your platform. - */ - protected String algorithm = DEFAULT_ALGORITHM; - - - /** - * Should we cache authenticated Principals if the request is part of - * an HTTP session? - */ - protected boolean cache = true; - - - /** - * The Context to which this Valve is attached. - */ - protected Context context = null; - - - /** - * Return the MessageDigest implementation to be used when - * creating session identifiers. - */ - protected MessageDigest digest = null; - - - /** - * A String initialization parameter used to increase the entropy of - * the initialization of our random number generator. - */ - protected String entropy = null; - - - /** - * Descriptive information about this implementation. - */ - protected static final String info = - "org.apache.catalina.authenticator.AuthenticatorBase/1.0"; - - /** - * Flag to determine if we disable proxy caching, or leave the issue - * up to the webapp developer. - */ - protected boolean disableProxyCaching = true; - - /** - * Flag to determine if we disable proxy caching with headers incompatible - * with IE - */ - protected boolean securePagesWithPragma = true; - - /** - * The lifecycle event support for this component. - */ - protected LifecycleSupport lifecycle = new LifecycleSupport(this); - - - /** - * A random number generator to use when generating session identifiers. - */ - protected Random random = null; - - - /** - * The Java class name of the random number generator class to be used - * when generating session identifiers. - */ - protected String randomClass = "java.security.SecureRandom"; - - - /** - * The string manager for this package. - */ - protected static final StringManager sm = - StringManager.getManager(Constants.Package); - - - /** - * The SingleSignOn implementation in our request processing chain, - * if there is one. - */ - protected SingleSignOn sso = null; - - - /** - * Has this component been started? - */ - protected boolean started = false; - - - /** - * "Expires" header always set to Date(1), so generate once only - */ - private static final String DATE_ONE = - (new SimpleDateFormat(DateTool.HTTP_RESPONSE_DATE_HEADER, - Locale.US)).format(new Date(1)); - - - // ------------------------------------------------------------- Properties - - - /** - * Return the message digest algorithm for this Manager. - */ - public String getAlgorithm() { - - return (this.algorithm); - - } - - - /** - * Set the message digest algorithm for this Manager. - * - * @param algorithm The new message digest algorithm - */ - public void setAlgorithm(String algorithm) { - - this.algorithm = algorithm; - - } - - - /** - * Return the cache authenticated Principals flag. - */ - public boolean getCache() { - - return (this.cache); - - } - - - /** - * Set the cache authenticated Principals flag. - * - * @param cache The new cache flag - */ - public void setCache(boolean cache) { - - this.cache = cache; - - } - - - /** - * Return the Container to which this Valve is attached. - */ - public Container getContainer() { - - return (this.context); - - } - - - /** - * Set the Container to which this Valve is attached. - * - * @param container The container to which we are attached - */ - public void setContainer(Container container) { - - if (!(container instanceof Context)) - throw new IllegalArgumentException - (sm.getString("authenticator.notContext")); - - super.setContainer(container); - this.context = (Context) container; - - } - - - /** - * Return the entropy increaser value, or compute a semi-useful value - * if this String has not yet been set. - */ - public String getEntropy() { - - // Calculate a semi-useful value if this has not been set - if (this.entropy == null) - setEntropy(this.toString()); - - return (this.entropy); - - } - - - /** - * Set the entropy increaser value. - * - * @param entropy The new entropy increaser value - */ - public void setEntropy(String entropy) { - - this.entropy = entropy; - - } - - - /** - * Return descriptive information about this Valve implementation. - */ - public String getInfo() { - - return (info); - - } - - - /** - * Return the random number generator class name. - */ - public String getRandomClass() { - - return (this.randomClass); - - } - - - /** - * Set the random number generator class name. - * - * @param randomClass The new random number generator class name - */ - public void setRandomClass(String randomClass) { - - this.randomClass = randomClass; - - } - - /** - * Return the flag that states if we add headers to disable caching by - * proxies. - */ - public boolean getDisableProxyCaching() { - return disableProxyCaching; - } - - /** - * Set the value of the flag that states if we add headers to disable - * caching by proxies. - * @param nocache true if we add headers to disable proxy - * caching, false if we leave the headers alone. - */ - public void setDisableProxyCaching(boolean nocache) { - disableProxyCaching = nocache; - } - - /** - * Return the flag that states, if proxy caching is disabled, what headers - * we add to disable the caching. - */ - public boolean getSecurePagesWithPragma() { - return securePagesWithPragma; - } - - /** - * Set the value of the flag that states what headers we add to disable - * proxy caching. - * @param securePagesWithPragma true if we add headers which - * are incompatible with downloading office documents in IE under SSL but - * which fix a caching problem in Mozilla. - */ - public void setSecurePagesWithPragma(boolean securePagesWithPragma) { - this.securePagesWithPragma = securePagesWithPragma; - } - - // --------------------------------------------------------- Public Methods - - - /** - * Enforce the security restrictions in the web application deployment - * descriptor of our associated Context. - * - * @param request Request to be processed - * @param response Response to be processed - * - * @exception IOException if an input/output error occurs - * @exception ServletException if thrown by a processing element - */ - public void invoke(Request request, Response response) - throws IOException, ServletException { - - if (log.isDebugEnabled()) - log.debug("Security checking request " + - request.getMethod() + " " + request.getRequestURI()); - LoginConfig config = this.context.getLoginConfig(); - - // Have we got a cached authenticated Principal to record? - if (cache) { - Principal principal = request.getUserPrincipal(); - if (principal == null) { - Session session = request.getSessionInternal(false); - if (session != null) { - principal = session.getPrincipal(); - if (principal != null) { - if (log.isDebugEnabled()) - log.debug("We have cached auth type " + - session.getAuthType() + - " for principal " + - session.getPrincipal()); - request.setAuthType(session.getAuthType()); - request.setUserPrincipal(principal); - } - } - } - } - - // Special handling for form-based logins to deal with the case - // where the login form (and therefore the "j_security_check" URI - // to which it submits) might be outside the secured area - String contextPath = this.context.getPath(); - String requestURI = request.getDecodedRequestURI(); - if (requestURI.startsWith(contextPath) && - requestURI.endsWith(Constants.FORM_ACTION)) { - if (!authenticate(request, response, config)) { - if (log.isDebugEnabled()) - log.debug(" Failed authenticate() test ??" + requestURI ); - return; - } - } - - Realm realm = this.context.getRealm(); - // Is this request URI subject to a security constraint? - SecurityConstraint [] constraints - = realm.findSecurityConstraints(request, this.context); - - if ((constraints == null) /* && - (!Constants.FORM_METHOD.equals(config.getAuthMethod())) */ ) { - if (log.isDebugEnabled()) - log.debug(" Not subject to any constraint"); - getNext().invoke(request, response); - return; - } - - // Make sure that constrained resources are not cached by web proxies - // or browsers as caching can provide a security hole - if (disableProxyCaching && - // FIXME: Disabled for Mozilla FORM support over SSL - // (improper caching issue) - //!request.isSecure() && - !"POST".equalsIgnoreCase(request.getMethod())) { - if (securePagesWithPragma) { - // FIXME: These cause problems with downloading office docs - // from IE under SSL and may not be needed for newer Mozilla - // clients. - response.setHeader("Pragma", "No-cache"); - response.setHeader("Cache-Control", "no-cache"); - } else { - response.setHeader("Cache-Control", "private"); - } - response.setHeader("Expires", DATE_ONE); - } - - int i; - // Enforce any user data constraint for this security constraint - if (log.isDebugEnabled()) { - log.debug(" Calling hasUserDataPermission()"); - } - if (!realm.hasUserDataPermission(request, response, - constraints)) { - if (log.isDebugEnabled()) { - log.debug(" Failed hasUserDataPermission() test"); - } - /* - * ASSERT: Authenticator already set the appropriate - * HTTP status code, so we do not have to do anything special - */ - return; - } - - // Since authenticate modifies the response on failure, - // we have to check for allow-from-all first. - boolean authRequired = true; - for(i=0; i < constraints.length && authRequired; i++) { - if(!constraints[i].getAuthConstraint()) { - authRequired = false; - } else if(!constraints[i].getAllRoles()) { - String [] roles = constraints[i].findAuthRoles(); - if(roles == null || roles.length == 0) { - authRequired = false; - } - } - } - - if(authRequired) { - if (log.isDebugEnabled()) { - log.debug(" Calling authenticate()"); - } - if (!authenticate(request, response, config)) { - if (log.isDebugEnabled()) { - log.debug(" Failed authenticate() test"); - } - /* - * ASSERT: Authenticator already set the appropriate - * HTTP status code, so we do not have to do anything - * special - */ - return; - } - } - - if (log.isDebugEnabled()) { - log.debug(" Calling accessControl()"); - } - if (!realm.hasResourcePermission(request, response, - constraints, - this.context)) { - if (log.isDebugEnabled()) { - log.debug(" Failed accessControl() test"); - } - /* - * ASSERT: AccessControl method has already set the - * appropriate HTTP status code, so we do not have to do - * anything special - */ - return; - } - - // Any and all specified constraints have been satisfied - if (log.isDebugEnabled()) { - log.debug(" Successfully passed all security constraints"); - } - getNext().invoke(request, response); - - } - - - // ------------------------------------------------------ Protected Methods - - - - - /** - * Associate the specified single sign on identifier with the - * specified Session. - * - * @param ssoId Single sign on identifier - * @param session Session to be associated - */ - protected void associate(String ssoId, Session session) { - - if (sso == null) - return; - sso.associate(ssoId, session); - - } - - - /** - * Authenticate the user making this request, based on the specified - * login configuration. Return true if any specified - * constraint has been satisfied, or false if we have - * created a response challenge already. - * - * @param request Request we are processing - * @param response Response we are creating - * @param config Login configuration describing how authentication - * should be performed - * - * @exception IOException if an input/output error occurs - */ - protected abstract boolean authenticate(Request request, - Response response, - LoginConfig config) - throws IOException; - - - /** - * Generate and return a new session identifier for the cookie that - * identifies an SSO principal. - */ - protected synchronized String generateSessionId() { - - // Generate a byte array containing a session identifier - byte bytes[] = new byte[SESSION_ID_BYTES]; - getRandom().nextBytes(bytes); - bytes = getDigest().digest(bytes); - - // Render the result as a String of hexadecimal digits - StringBuffer result = new StringBuffer(); - for (int i = 0; i < bytes.length; i++) { - byte b1 = (byte) ((bytes[i] & 0xf0) >> 4); - byte b2 = (byte) (bytes[i] & 0x0f); - if (b1 < 10) - result.append((char) ('0' + b1)); - else - result.append((char) ('A' + (b1 - 10))); - if (b2 < 10) - result.append((char) ('0' + b2)); - else - result.append((char) ('A' + (b2 - 10))); - } - return (result.toString()); - - } - - - /** - * Return the MessageDigest object to be used for calculating - * session identifiers. If none has been created yet, initialize - * one the first time this method is called. - */ - protected synchronized MessageDigest getDigest() { - - if (this.digest == null) { - try { - this.digest = MessageDigest.getInstance(algorithm); - } catch (NoSuchAlgorithmException e) { - try { - this.digest = MessageDigest.getInstance(DEFAULT_ALGORITHM); - } catch (NoSuchAlgorithmException f) { - this.digest = null; - } - } - } - - return (this.digest); - - } - - - /** - * Return the random number generator instance we should use for - * generating session identifiers. If there is no such generator - * currently defined, construct and seed a new one. - */ - protected synchronized Random getRandom() { - - if (this.random == null) { - try { - Class clazz = Class.forName(randomClass); - this.random = (Random) clazz.newInstance(); - long seed = System.currentTimeMillis(); - char entropy[] = getEntropy().toCharArray(); - for (int i = 0; i < entropy.length; i++) { - long update = ((byte) entropy[i]) << ((i % 8) * 8); - seed ^= update; - } - this.random.setSeed(seed); - } catch (Exception e) { - this.random = new java.util.Random(); - } - } - - return (this.random); - - } - - - /** - * Attempts reauthentication to the Realm using - * the credentials included in argument entry. - * - * @param ssoId identifier of SingleSignOn session with which the - * caller is associated - * @param request the request that needs to be authenticated - */ - protected boolean reauthenticateFromSSO(String ssoId, Request request) { - - if (sso == null || ssoId == null) - return false; - - boolean reauthenticated = false; - - Container parent = getContainer(); - if (parent != null) { - Realm realm = parent.getRealm(); - if (realm != null) { - reauthenticated = sso.reauthenticate(ssoId, realm, request); - } - } - - if (reauthenticated) { - associate(ssoId, request.getSessionInternal(true)); - - if (log.isDebugEnabled()) { - log.debug(" Reauthenticated cached principal '" + - request.getUserPrincipal().getName() + - "' with auth type '" + request.getAuthType() + "'"); - } - } - - return reauthenticated; - } - - - /** - * Register an authenticated Principal and authentication type in our - * request, in the current session (if there is one), and with our - * SingleSignOn valve, if there is one. Set the appropriate cookie - * to be returned. - * - * @param request The servlet request we are processing - * @param response The servlet response we are generating - * @param principal The authenticated Principal to be registered - * @param authType The authentication type to be registered - * @param username Username used to authenticate (if any) - * @param password Password used to authenticate (if any) - */ - protected void register(Request request, Response response, - Principal principal, String authType, - String username, String password) { - - if (log.isDebugEnabled()) { - // Bugzilla 39255: http://issues.apache.org/bugzilla/show_bug.cgi?id=39255 - String name = (principal == null) ? "none" : principal.getName(); - log.debug("Authenticated '" + name + "' with type '" - + authType + "'"); - } - - // Cache the authentication information in our request - request.setAuthType(authType); - request.setUserPrincipal(principal); - - Session session = request.getSessionInternal(false); - // Cache the authentication information in our session, if any - if (cache) { - if (session != null) { - session.setAuthType(authType); - session.setPrincipal(principal); - if (username != null) - session.setNote(Constants.SESS_USERNAME_NOTE, username); - else - session.removeNote(Constants.SESS_USERNAME_NOTE); - if (password != null) - session.setNote(Constants.SESS_PASSWORD_NOTE, password); - else - session.removeNote(Constants.SESS_PASSWORD_NOTE); - } - } - - // Construct a cookie to be returned to the client - if (sso == null) - return; - - // Only create a new SSO entry if the SSO did not already set a note - // for an existing entry (as it would do with subsequent requests - // for DIGEST and SSL authenticated contexts) - String ssoId = (String) request.getNote(Constants.REQ_SSOID_NOTE); - if (ssoId == null) { - // Construct a cookie to be returned to the client - ssoId = generateSessionId(); - Cookie cookie = new Cookie(Constants.SINGLE_SIGN_ON_COOKIE, ssoId); - cookie.setMaxAge(-1); - cookie.setPath("/"); - - // Bugzilla 41217 - cookie.setSecure(request.isSecure()); - - // Bugzilla 34724 - String ssoDomain = sso.getCookieDomain(); - if(ssoDomain != null) { - cookie.setDomain(ssoDomain); - } - - response.addCookie(cookie); - - // Register this principal with our SSO valve - sso.register(ssoId, principal, authType, username, password); - request.setNote(Constants.REQ_SSOID_NOTE, ssoId); - - } else { - // Update the SSO session with the latest authentication data - sso.update(ssoId, principal, authType, username, password); - } - - // Fix for Bug 10040 - // Always associate a session with a new SSO reqistration. - // SSO entries are only removed from the SSO registry map when - // associated sessions are destroyed; if a new SSO entry is created - // above for this request and the user never revisits the context, the - // SSO entry will never be cleared if we don't associate the session - if (session == null) - session = request.getSessionInternal(true); - sso.associate(ssoId, session); - - } - - - // ------------------------------------------------------ Lifecycle Methods - - - /** - * Add a lifecycle event listener to this component. - * - * @param listener The listener to add - */ - public void addLifecycleListener(LifecycleListener listener) { - - lifecycle.addLifecycleListener(listener); - - } - - - /** - * Get the lifecycle listeners associated with this lifecycle. If this - * Lifecycle has no listeners registered, a zero-length array is returned. - */ - public LifecycleListener[] findLifecycleListeners() { - - return lifecycle.findLifecycleListeners(); - - } - - - /** - * Remove a lifecycle event listener from this component. - * - * @param listener The listener to remove - */ - public void removeLifecycleListener(LifecycleListener listener) { - - lifecycle.removeLifecycleListener(listener); - - } - - - /** - * Prepare for the beginning of active use of the public methods of this - * component. This method should be called after configure(), - * and before any of the public methods of the component are utilized. - * - * @exception LifecycleException if this component detects a fatal error - * that prevents this component from being used - */ - public void start() throws LifecycleException { - - // Validate and update our current component state - if (started) - throw new LifecycleException - (sm.getString("authenticator.alreadyStarted")); - lifecycle.fireLifecycleEvent(START_EVENT, null); - started = true; - - // Look up the SingleSignOn implementation in our request processing - // path, if there is one - Container parent = context.getParent(); - while ((sso == null) && (parent != null)) { - if (!(parent instanceof Pipeline)) { - parent = parent.getParent(); - continue; - } - Valve valves[] = ((Pipeline) parent).getValves(); - for (int i = 0; i < valves.length; i++) { - if (valves[i] instanceof SingleSignOn) { - sso = (SingleSignOn) valves[i]; - break; - } - } - if (sso == null) - parent = parent.getParent(); - } - if (log.isDebugEnabled()) { - if (sso != null) - log.debug("Found SingleSignOn Valve at " + sso); - else - log.debug("No SingleSignOn Valve is present"); - } - - } - - - /** - * Gracefully terminate the active use of the public methods of this - * component. This method should be the last one called on a given - * instance of this component. - * - * @exception LifecycleException if this component detects a fatal error - * that needs to be reported - */ - public void stop() throws LifecycleException { - - // Validate and update our current component state - if (!started) - throw new LifecycleException - (sm.getString("authenticator.notStarted")); - lifecycle.fireLifecycleEvent(STOP_EVENT, null); - started = false; - - sso = null; - - } - - -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.apache.catalina.authenticator; + + +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.security.cert.X509Certificate; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Random; + +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; + +import org.apache.catalina.Authenticator; +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.Globals; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Pipeline; +import org.apache.catalina.Realm; +import org.apache.catalina.Session; +import org.apache.catalina.Valve; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.deploy.LoginConfig; +import org.apache.catalina.deploy.SecurityConstraint; +import org.apache.catalina.util.DateTool; +import org.apache.catalina.util.LifecycleSupport; +import org.apache.catalina.util.StringManager; +import org.apache.catalina.valves.ValveBase; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + + +/** + * Basic implementation of the Valve interface that enforces the + * <security-constraint> elements in the web application + * deployment descriptor. This functionality is implemented as a Valve + * so that it can be ommitted in environments that do not require these + * features. Individual implementations of each supported authentication + * method can subclass this base class as required. + *

+ * USAGE CONSTRAINT: When this class is utilized, the Context to + * which it is attached (or a parent Container in a hierarchy) must have an + * associated Realm that can be used for authenticating users and enumerating + * the roles to which they have been assigned. + *

+ * USAGE CONSTRAINT: This Valve is only useful when processing HTTP + * requests. Requests of any other type will simply be passed through. + * + * @author Craig R. McClanahan + * @version $Revision: 496025 $ $Date: 2007-01-14 04:18:06 +0100 (Sun, 14 Jan 2007) $ + */ + + +public abstract class AuthenticatorBase + extends ValveBase + implements Authenticator, Lifecycle { + private static Log log = LogFactory.getLog(AuthenticatorBase.class); + + + // ----------------------------------------------------- Instance Variables + + + /** + * The default message digest algorithm to use if we cannot use + * the requested one. + */ + protected static final String DEFAULT_ALGORITHM = "MD5"; + + + /** + * The number of random bytes to include when generating a + * session identifier. + */ + protected static final int SESSION_ID_BYTES = 16; + + + /** + * The message digest algorithm to be used when generating session + * identifiers. This must be an algorithm supported by the + * java.security.MessageDigest class on your platform. + */ + protected String algorithm = DEFAULT_ALGORITHM; + + + /** + * Should we cache authenticated Principals if the request is part of + * an HTTP session? + */ + protected boolean cache = true; + + + /** + * The Context to which this Valve is attached. + */ + protected Context context = null; + + + /** + * Return the MessageDigest implementation to be used when + * creating session identifiers. + */ + protected MessageDigest digest = null; + + + /** + * A String initialization parameter used to increase the entropy of + * the initialization of our random number generator. + */ + protected String entropy = null; + + + /** + * Descriptive information about this implementation. + */ + protected static final String info = + "org.apache.catalina.authenticator.AuthenticatorBase/1.0"; + + /** + * Flag to determine if we disable proxy caching, or leave the issue + * up to the webapp developer. + */ + protected boolean disableProxyCaching = true; + + /** + * Flag to determine if we disable proxy caching with headers incompatible + * with IE + */ + protected boolean securePagesWithPragma = true; + + /** + * The lifecycle event support for this component. + */ + protected LifecycleSupport lifecycle = new LifecycleSupport(this); + + + /** + * A random number generator to use when generating session identifiers. + */ + protected Random random = null; + + + /** + * The Java class name of the random number generator class to be used + * when generating session identifiers. + */ + protected String randomClass = "java.security.SecureRandom"; + + + /** + * The string manager for this package. + */ + protected static final StringManager sm = + StringManager.getManager(Constants.Package); + + + /** + * The SingleSignOn implementation in our request processing chain, + * if there is one. + */ + protected SingleSignOn sso = null; + + + /** + * Has this component been started? + */ + protected boolean started = false; + + + /** + * "Expires" header always set to Date(1), so generate once only + */ + private static final String DATE_ONE = + (new SimpleDateFormat(DateTool.HTTP_RESPONSE_DATE_HEADER, + Locale.US)).format(new Date(1)); + + + // ------------------------------------------------------------- Properties + + + /** + * Return the message digest algorithm for this Manager. + */ + public String getAlgorithm() { + + return (this.algorithm); + + } + + + /** + * Set the message digest algorithm for this Manager. + * + * @param algorithm The new message digest algorithm + */ + public void setAlgorithm(String algorithm) { + + this.algorithm = algorithm; + + } + + + /** + * Return the cache authenticated Principals flag. + */ + public boolean getCache() { + + return (this.cache); + + } + + + /** + * Set the cache authenticated Principals flag. + * + * @param cache The new cache flag + */ + public void setCache(boolean cache) { + + this.cache = cache; + + } + + + /** + * Return the Container to which this Valve is attached. + */ + public Container getContainer() { + + return (this.context); + + } + + + /** + * Set the Container to which this Valve is attached. + * + * @param container The container to which we are attached + */ + public void setContainer(Container container) { + + if (!(container instanceof Context)) + throw new IllegalArgumentException + (sm.getString("authenticator.notContext")); + + super.setContainer(container); + this.context = (Context) container; + + } + + + /** + * Return the entropy increaser value, or compute a semi-useful value + * if this String has not yet been set. + */ + public String getEntropy() { + + // Calculate a semi-useful value if this has not been set + if (this.entropy == null) + setEntropy(this.toString()); + + return (this.entropy); + + } + + + /** + * Set the entropy increaser value. + * + * @param entropy The new entropy increaser value + */ + public void setEntropy(String entropy) { + + this.entropy = entropy; + + } + + + /** + * Return descriptive information about this Valve implementation. + */ + public String getInfo() { + + return (info); + + } + + + /** + * Return the random number generator class name. + */ + public String getRandomClass() { + + return (this.randomClass); + + } + + + /** + * Set the random number generator class name. + * + * @param randomClass The new random number generator class name + */ + public void setRandomClass(String randomClass) { + + this.randomClass = randomClass; + + } + + /** + * Return the flag that states if we add headers to disable caching by + * proxies. + */ + public boolean getDisableProxyCaching() { + return disableProxyCaching; + } + + /** + * Set the value of the flag that states if we add headers to disable + * caching by proxies. + * @param nocache true if we add headers to disable proxy + * caching, false if we leave the headers alone. + */ + public void setDisableProxyCaching(boolean nocache) { + disableProxyCaching = nocache; + } + + /** + * Return the flag that states, if proxy caching is disabled, what headers + * we add to disable the caching. + */ + public boolean getSecurePagesWithPragma() { + return securePagesWithPragma; + } + + /** + * Set the value of the flag that states what headers we add to disable + * proxy caching. + * @param securePagesWithPragma true if we add headers which + * are incompatible with downloading office documents in IE under SSL but + * which fix a caching problem in Mozilla. + */ + public void setSecurePagesWithPragma(boolean securePagesWithPragma) { + this.securePagesWithPragma = securePagesWithPragma; + } + + // --------------------------------------------------------- Public Methods + + + /** + * Enforce the security restrictions in the web application deployment + * descriptor of our associated Context. + * + * @param request Request to be processed + * @param response Response to be processed + * + * @exception IOException if an input/output error occurs + * @exception ServletException if thrown by a processing element + */ + public void invoke(Request request, Response response) + throws IOException, ServletException { + + if (log.isDebugEnabled()) + log.debug("Security checking request " + + request.getMethod() + " " + request.getRequestURI()); + LoginConfig config = this.context.getLoginConfig(); + + // Have we got a cached authenticated Principal to record? + if (cache) { + Principal principal = request.getUserPrincipal(); + if (principal == null) { + Session session = request.getSessionInternal(false); + if (session != null) { + principal = session.getPrincipal(); + if (principal != null) { + if (log.isDebugEnabled()) + log.debug("We have cached auth type " + + session.getAuthType() + + " for principal " + + session.getPrincipal()); + request.setAuthType(session.getAuthType()); + request.setUserPrincipal(principal); + } + } + } + } + + // Special handling for form-based logins to deal with the case + // where the login form (and therefore the "j_security_check" URI + // to which it submits) might be outside the secured area + String contextPath = this.context.getPath(); + String requestURI = request.getDecodedRequestURI(); + if (requestURI.startsWith(contextPath) && + requestURI.endsWith(Constants.FORM_ACTION)) { + if (!authenticate(request, response, config)) { + if (log.isDebugEnabled()) + log.debug(" Failed authenticate() test ??" + requestURI ); + return; + } + } + + Realm realm = this.context.getRealm(); + // Is this request URI subject to a security constraint? + SecurityConstraint [] constraints + = realm.findSecurityConstraints(request, this.context); + + // Make sure that constrained resources are not cached by web proxies + // or browsers as caching can provide a security hole + if (disableProxyCaching && + // FIXME: Disabled for Mozilla FORM support over SSL + // (improper caching issue) + //!request.isSecure() && + !"POST".equalsIgnoreCase(request.getMethod())) { + if (securePagesWithPragma) { + // FIXME: These cause problems with downloading office docs + // from IE under SSL and may not be needed for newer Mozilla + // clients. + response.setHeader("Pragma", "No-cache"); + response.setHeader("Cache-Control", "no-cache"); + } else { + response.setHeader("Cache-Control", "private"); + } + response.setHeader("Expires", DATE_ONE); + } + + int i; + if (constraints != null) { + // Enforce any user data constraint for this security constraint + if (log.isDebugEnabled()) { + log.debug(" Calling hasUserDataPermission()"); + } + if (!realm.hasUserDataPermission(request, response, + constraints)) { + if (log.isDebugEnabled()) { + log.debug(" Failed hasUserDataPermission() test"); + } + /* + * ASSERT: Authenticator already set the appropriate + * HTTP status code, so we do not have to do anything special + */ + return; + } + } + + // Since authenticate modifies the response on failure, + // we have to check for allow-from-all first. + boolean authRequired; + if (constraints == null) { + authRequired = false; + } else { + authRequired = true; + for(i=0; i < constraints.length && authRequired; i++) { + if(!constraints[i].getAuthConstraint()) { + authRequired = false; + } else if(!constraints[i].getAllRoles()) { + String [] roles = constraints[i].findAuthRoles(); + if(roles == null || roles.length == 0) { + authRequired = false; + } + } + } + } + + if (!authRequired) + { + authRequired = + request.getCoyoteRequest().getMimeHeaders(). + getValue("authorization") != null; + } + + if (!authRequired) + { + X509Certificate[] certs = + (X509Certificate[]) request.getAttribute(Globals.CERTIFICATES_ATTR); + + authRequired = certs != null && certs.length > 0; + } + + if(authRequired) { + if (log.isDebugEnabled()) { + log.debug(" Calling authenticate()"); + } + if (!authenticate(request, response, config)) { + if (log.isDebugEnabled()) { + log.debug(" Failed authenticate() test"); + } + /* + * ASSERT: Authenticator already set the appropriate + * HTTP status code, so we do not have to do anything + * special + */ + return; + } + } + + if (constraints != null) { + if (log.isDebugEnabled()) { + log.debug(" Calling accessControl()"); + } + if (!realm.hasResourcePermission(request, response, + constraints, + this.context)) { + if (log.isDebugEnabled()) { + log.debug(" Failed accessControl() test"); + } + /* + * ASSERT: AccessControl method has already set the + * appropriate HTTP status code, so we do not have to do + * anything special + */ + return; + } + } + + // Any and all specified constraints have been satisfied + if (log.isDebugEnabled()) { + log.debug(" Successfully passed all security constraints"); + } + getNext().invoke(request, response); + + } + + + // ------------------------------------------------------ Protected Methods + + + + + /** + * Associate the specified single sign on identifier with the + * specified Session. + * + * @param ssoId Single sign on identifier + * @param session Session to be associated + */ + protected void associate(String ssoId, Session session) { + + if (sso == null) + return; + sso.associate(ssoId, session); + + } + + + /** + * Authenticate the user making this request, based on the specified + * login configuration. Return true if any specified + * constraint has been satisfied, or false if we have + * created a response challenge already. + * + * @param request Request we are processing + * @param response Response we are creating + * @param config Login configuration describing how authentication + * should be performed + * + * @exception IOException if an input/output error occurs + */ + protected abstract boolean authenticate(Request request, + Response response, + LoginConfig config) + throws IOException; + + + /** + * Generate and return a new session identifier for the cookie that + * identifies an SSO principal. + */ + protected synchronized String generateSessionId() { + + // Generate a byte array containing a session identifier + byte bytes[] = new byte[SESSION_ID_BYTES]; + getRandom().nextBytes(bytes); + bytes = getDigest().digest(bytes); + + // Render the result as a String of hexadecimal digits + StringBuffer result = new StringBuffer(); + for (int i = 0; i < bytes.length; i++) { + byte b1 = (byte) ((bytes[i] & 0xf0) >> 4); + byte b2 = (byte) (bytes[i] & 0x0f); + if (b1 < 10) + result.append((char) ('0' + b1)); + else + result.append((char) ('A' + (b1 - 10))); + if (b2 < 10) + result.append((char) ('0' + b2)); + else + result.append((char) ('A' + (b2 - 10))); + } + return (result.toString()); + + } + + + /** + * Return the MessageDigest object to be used for calculating + * session identifiers. If none has been created yet, initialize + * one the first time this method is called. + */ + protected synchronized MessageDigest getDigest() { + + if (this.digest == null) { + try { + this.digest = MessageDigest.getInstance(algorithm); + } catch (NoSuchAlgorithmException e) { + try { + this.digest = MessageDigest.getInstance(DEFAULT_ALGORITHM); + } catch (NoSuchAlgorithmException f) { + this.digest = null; + } + } + } + + return (this.digest); + + } + + + /** + * Return the random number generator instance we should use for + * generating session identifiers. If there is no such generator + * currently defined, construct and seed a new one. + */ + protected synchronized Random getRandom() { + + if (this.random == null) { + try { + Class clazz = Class.forName(randomClass); + this.random = (Random) clazz.newInstance(); + long seed = System.currentTimeMillis(); + char entropy[] = getEntropy().toCharArray(); + for (int i = 0; i < entropy.length; i++) { + long update = ((byte) entropy[i]) << ((i % 8) * 8); + seed ^= update; + } + this.random.setSeed(seed); + } catch (Exception e) { + this.random = new java.util.Random(); + } + } + + return (this.random); + + } + + + /** + * Attempts reauthentication to the Realm using + * the credentials included in argument entry. + * + * @param ssoId identifier of SingleSignOn session with which the + * caller is associated + * @param request the request that needs to be authenticated + */ + protected boolean reauthenticateFromSSO(String ssoId, Request request) { + + if (sso == null || ssoId == null) + return false; + + boolean reauthenticated = false; + + Container parent = getContainer(); + if (parent != null) { + Realm realm = parent.getRealm(); + if (realm != null) { + reauthenticated = sso.reauthenticate(ssoId, realm, request); + } + } + + if (reauthenticated) { + associate(ssoId, request.getSessionInternal(true)); + + if (log.isDebugEnabled()) { + log.debug(" Reauthenticated cached principal '" + + request.getUserPrincipal().getName() + + "' with auth type '" + request.getAuthType() + "'"); + } + } + + return reauthenticated; + } + + + /** + * Register an authenticated Principal and authentication type in our + * request, in the current session (if there is one), and with our + * SingleSignOn valve, if there is one. Set the appropriate cookie + * to be returned. + * + * @param request The servlet request we are processing + * @param response The servlet response we are generating + * @param principal The authenticated Principal to be registered + * @param authType The authentication type to be registered + * @param username Username used to authenticate (if any) + * @param password Password used to authenticate (if any) + */ + protected void register(Request request, Response response, + Principal principal, String authType, + String username, String password) { + + if (log.isDebugEnabled()) { + // Bugzilla 39255: http://issues.apache.org/bugzilla/show_bug.cgi?id=39255 + String name = (principal == null) ? "none" : principal.getName(); + log.debug("Authenticated '" + name + "' with type '" + + authType + "'"); + } + + // Cache the authentication information in our request + request.setAuthType(authType); + request.setUserPrincipal(principal); + + Session session = request.getSessionInternal(false); + // Cache the authentication information in our session, if any + if (cache) { + if (session != null) { + session.setAuthType(authType); + session.setPrincipal(principal); + if (username != null) + session.setNote(Constants.SESS_USERNAME_NOTE, username); + else + session.removeNote(Constants.SESS_USERNAME_NOTE); + if (password != null) + session.setNote(Constants.SESS_PASSWORD_NOTE, password); + else + session.removeNote(Constants.SESS_PASSWORD_NOTE); + } + } + + // Construct a cookie to be returned to the client + if (sso == null) + return; + + // Only create a new SSO entry if the SSO did not already set a note + // for an existing entry (as it would do with subsequent requests + // for DIGEST and SSL authenticated contexts) + String ssoId = (String) request.getNote(Constants.REQ_SSOID_NOTE); + if (ssoId == null) { + // Construct a cookie to be returned to the client + ssoId = generateSessionId(); + Cookie cookie = new Cookie(Constants.SINGLE_SIGN_ON_COOKIE, ssoId); + cookie.setMaxAge(-1); + cookie.setPath("/"); + + // Bugzilla 41217 + cookie.setSecure(request.isSecure()); + + // Bugzilla 34724 + String ssoDomain = sso.getCookieDomain(); + if(ssoDomain != null) { + cookie.setDomain(ssoDomain); + } + + response.addCookie(cookie); + + // Register this principal with our SSO valve + sso.register(ssoId, principal, authType, username, password); + request.setNote(Constants.REQ_SSOID_NOTE, ssoId); + + } else { + // Update the SSO session with the latest authentication data + sso.update(ssoId, principal, authType, username, password); + } + + // Fix for Bug 10040 + // Always associate a session with a new SSO reqistration. + // SSO entries are only removed from the SSO registry map when + // associated sessions are destroyed; if a new SSO entry is created + // above for this request and the user never revisits the context, the + // SSO entry will never be cleared if we don't associate the session + if (session == null) + session = request.getSessionInternal(true); + sso.associate(ssoId, session); + + } + + + // ------------------------------------------------------ Lifecycle Methods + + + /** + * Add a lifecycle event listener to this component. + * + * @param listener The listener to add + */ + public void addLifecycleListener(LifecycleListener listener) { + + lifecycle.addLifecycleListener(listener); + + } + + + /** + * Get the lifecycle listeners associated with this lifecycle. If this + * Lifecycle has no listeners registered, a zero-length array is returned. + */ + public LifecycleListener[] findLifecycleListeners() { + + return lifecycle.findLifecycleListeners(); + + } + + + /** + * Remove a lifecycle event listener from this component. + * + * @param listener The listener to remove + */ + public void removeLifecycleListener(LifecycleListener listener) { + + lifecycle.removeLifecycleListener(listener); + + } + + + /** + * Prepare for the beginning of active use of the public methods of this + * component. This method should be called after configure(), + * and before any of the public methods of the component are utilized. + * + * @exception LifecycleException if this component detects a fatal error + * that prevents this component from being used + */ + public void start() throws LifecycleException { + + // Validate and update our current component state + if (started) + throw new LifecycleException + (sm.getString("authenticator.alreadyStarted")); + lifecycle.fireLifecycleEvent(START_EVENT, null); + started = true; + + // Look up the SingleSignOn implementation in our request processing + // path, if there is one + Container parent = context.getParent(); + while ((sso == null) && (parent != null)) { + if (!(parent instanceof Pipeline)) { + parent = parent.getParent(); + continue; + } + Valve valves[] = ((Pipeline) parent).getValves(); + for (int i = 0; i < valves.length; i++) { + if (valves[i] instanceof SingleSignOn) { + sso = (SingleSignOn) valves[i]; + break; + } + } + if (sso == null) + parent = parent.getParent(); + } + if (log.isDebugEnabled()) { + if (sso != null) + log.debug("Found SingleSignOn Valve at " + sso); + else + log.debug("No SingleSignOn Valve is present"); + } + + } + + + /** + * Gracefully terminate the active use of the public methods of this + * component. This method should be the last one called on a given + * instance of this component. + * + * @exception LifecycleException if this component detects a fatal error + * that needs to be reported + */ + public void stop() throws LifecycleException { + + // Validate and update our current component state + if (!started) + throw new LifecycleException + (sm.getString("authenticator.notStarted")); + lifecycle.fireLifecycleEvent(STOP_EVENT, null); + started = false; + + sso = null; + + } + + +}