--- 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;
+
+ }
+
+
+}