You are viewing a plain text version of this content. The canonical link for it is here.
Posted to users@tomcat.apache.org by Louis <la...@gmail.com> on 2007/12/21 16:02:08 UTC

TrustedPrincipalAuthorizor (extends AuthenticatorBase) Discussion / Comments

Hi, I'm new to the list though I've been developing on Tomcat and JBoss 
for a few years now. Presented below are two classes I'd like feedback 
on. Anything is appreciated but I'm hoping for feedback mostly on the 
ideas behind the implementation more than particular bug fixes (that can 
come if I'm on the right track).

Also, I've introduced this to the users list first in case I've gone 
astray and there's a better way to do this elsewhere.

## Boring Backstory ## (This rambles for a bit. You can skip to the 
juicy bit by scrolling down to # Implementation Details #)

My background is primarily struts/hibernate based development. In that 
time I've had a few chances to write LoginModules (Similar to custom 
Realm in Tomcat) for a couple of different needs.

I recently joined a team that was using the IIS isapi_redirector.dll for 
tomcat to do transparent authentication. This is great for the end user. 
However a problem the team was having is that they're still doing 
programmatic security for everything. Why? The IIS connector connects to 
Tomcat and embeds a Principal with the users Windows Domain / Username 
in it. The team simply took this and then manually checked the users 
permissions for each operation. Yes there's a time and a place for 
programmatic security, but if you can let the container manage security 
why not? (Note: This is all being done for a MS Windows Corporate 
Intranet which is why the use of NTLM is accepted)

It was trivial to write a JBoss Login Module for their existing security 
database. However, I came across a problem when getting the 
authentication to happen: Tomcat wasn't authenticating the users. This 
makes sense since I hadn't told Tomcat to do any authentication in the 
WEB.XML file.

When trying to decide which Authentication I should use I reviewed the 
source for the existing ones and realized that none of them would work. 
The existing authenticators all check to see if a principal is already 
in the request. If it is then the user has already been authenticated 
and the authenticator never goes to the Realm to see what permissions / 
roles the user has. Since the IIS connector plugs-in a Principal for the 
user, this effectively short-circuited the ability of the container to 
bind roles to the user.

My solution was to code my own TrustedPrincipalAuthorizor (extends 
AuthenticatorBase). I called it an authorizor since it wasn't doing any 
actual Authentication. This class checks for the existence of a 
principal in the request. It then uses that principal as the key to tell 
the security realm to lookup the users permissions and bind them into 
the container for this request. Ergo the name. The last step of the 
puzzle is to make 'authenticating' (ie: checking the password) optional 
in your realm. I coded my LoginModule to have a flag checkPassword="false".

I know that's not the whole picture but it's the gist of it to get a 
discussion going. What do people here think, am I following a logical 
path or have I completely jumped the shark?

TrustedPrincipalAuthorizor.java

This allows me to force authentication to continue on to the realms in 
order to populate the user's request with their roles. (IE: use the full 
power of container managed security).

ContextLookupAuthenticatorBase.java

This came about because: 1. JBoss 3.2.5 doesn't support the <context> 
element or a context.xml file from tomcat to be able to make deployment 
changes to a program.
2. After studying the AuthenticatorBase I determined that it didn't 
actually _need_ to be hosted inside of a context element. It can get the 
context from the request, so it can determine dynamically which context 
and realm apply to the request.

This was desireable since we use this function across all our intranet 
applications and didn't want to have to declare it again for each app. 
This is actually fairly redundant and I (at present) don't see why the 
AuthenticatorBase must be implmented to be context dependant. (I can see 
why it is, just not the architectural reason why it _must be_).


## Implementation Details ##

Here's the rough source for my TrustedPrincipalAuthorizor.

The java files can be downloaded from:
http://www.laj.ca/source/tomcat/TrustedPrincipalAuthorizor.java
http://www.laj.ca/source/tomcat/ContextLookupAuthenticatorBase.java

/*
 * Copyright 2007 Louis A. J. Pierrson
 * based on code
 * Copyright 1999-2001,2004 The Apache Software Foundation.
 *
 * Licensed 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 ca.laj.catalina.authenticator;

import java.io.IOException;
import java.security.Principal;

import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.catalina.Lifecycle;
import org.apache.catalina.Realm;
import org.apache.catalina.authenticator.ContextLookupAuthenticatorBase;
import org.apache.catalina.deploy.LoginConfig;
import org.apache.catalina.deploy.SecurityConstraint;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * This class relies on a host container to supply the users name as a 
Principal.
 *
 * The host container used during development was IIS with the Apache
 * isapi_redirector2.dll for IIS. This dll redirects IIS requests from the
 * specified URI's to Tomcat (or JBoss). With
 * this, IIS includes the user's LAN domain/username from IIS' NTLM 
authentication of
 * the user. In this way, we now have an authenticated security 
credential for
 * the user. ie: We know who they are. What we don't know is what they 
can do.
 * That is for another zone.
 * <p>
 * Due to the need to force an 'authentication' to take place in order to be
 * able to use the IIS user credential with the normal tomcat/JBoss (Java
 * servlet container) security model, (ie: to lookup the user's roles)
 * this custom authenticator provides a
 * transparent challenge of the user. This allows us to rely on normal
 * container based (declarative) security.
 * <p>
 * We can use this trusted credential to retreive the user's authorizations
 * (via either Active Directory, company database, property file, KERBEROS,
 * etc...)
 *
 * <p>
 * PRE: This class must be used in conjunction with the 
isapi_redirector2.dll
 * or some other mechanism that results in the user's login getting embedded
 * in the session in a <em>trusted</em> fashion. A base user principal 
is set and the call
 * request.getRemoteUser(); should not be returning null at this point.
 * <p>
 * To have this Authorizor recognized in web.xml by tomcat
 * {@link org.apache.catalina.startup.ContextConfig}
 * needs to be updated.
 *
 * <p>Installation:
 * <code>
 *  &lt;Host ...>
 *     &lt;!--
 *         IISAuthorizer is embedded in a context. The context requires 
a directory to exist
 *         under &lt;server-instance>\iisauthorizer as well as having a 
web-application using
 *         the context path &lt;server-instance>\deploy\iisauthorizer.war
 *         The crossContext flag enables the operation of the 
IISConnectorAuthorizer across other
 *         contexts.
 *     -->
 *     &lt;Context path="iisauthorizer" crossContext="true">
 *         &lt;Valve 
className="ca.laj.catalina.authenticator.IISConnectorAuthorizer" />
 *     &lt;/Context>
 * &lt;/Host>
 * </code>
 *
 * @author Louis A. J. Pierrson
 */
public class TrustedPrincipalAuthorizor extends 
ContextLookupAuthenticatorBase implements Lifecycle {

    private static final Log log = 
LogFactory.getLog(TrustedPrincipalAuthorizor.class);

   
    // Authentication methods for login configuration
    public static final String NTLM_METHOD = "NTLM";
    public static final String IIS_NTLM_METHOD = "IIS_NTLM";
   
    public String removeDomainFromPrincipal = "true";
   
    /**
     * @return the removeDomainFromPrincipal
     */
    public String getRemoveDomainFromPrincipal() {
        return removeDomainFromPrincipal;
    }
    /**
     * @param removeDomainFromPrincipal the removeDomainFromPrincipal to set
     */
    public void setRemoveDomainFromPrincipal(String 
removeDomainFromPrincipal) {
       
        if(removeDomainFromPrincipal == null) {
            log.trace("removeDomainFromPrincipal set to default. (true)");
        }
       
        log.trace("Set removeDomainFromPrincipal to [" + 
removeDomainFromPrincipal + "]");
       
        this.removeDomainFromPrincipal = removeDomainFromPrincipal;
    }
    /**
     * Authenticate the user making this request, based on the specified
     * login configuration.  Return <code>true</code> if any specified
     * constraint has been satisfied, or <code>false</code> 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
     * @see 
ContextLookupAuthenticatorBase#authenticate(org.apache.catalina.HttpRequest, 
org.apache.catalina.HttpResponse, LoginConfig)
     */
    public boolean authenticate(org.apache.catalina.HttpRequest 
catRequest, org.apache.catalina.HttpResponse catResponse, LoginConfig 
loginConfig)
    throws IOException
    {
        log.trace("called");
       
        HttpServletRequest request = 
(HttpServletRequest)catRequest.getRequest();
        ServletResponse response = catResponse.getResponse();
       
        HttpSession session = request.getSession();
       
        // The following calls should all work
        // Get the user principal set by the isapi_redirector2.dll
        String remoteUser = request.getRemoteUser(); // should be not 
null and a string like this: <domain>\\<username>
        String authType = request.getAuthType(); // should be NTLM
        String remoteAddr = request.getRemoteAddr();
        String remoteHost = request.getRemoteHost();
        Integer remotePort = new Integer(request.getRemotePort());
        Principal userPrincipal = request.getUserPrincipal();
        String authorization = request.getHeader("authorization");

       
        // If the userPrincipal isn't set then there is an error in the
        // configuration
        //XXX DISCUSS: This could fail over to BASIC, FORM or some other 
form of
        // authentication
        if(userPrincipal == null)
        {
            log.error("Could not authenticate the user as the expected " +
                    "user credential was not found.");
            HttpServletResponse hres =
                (HttpServletResponse) catResponse.getResponse();

            hres.setStatus(HttpServletResponse.SC_FORBIDDEN);
           
            return false;
        }
       
        // Process an IIS based NTLM login
        if(!"NTLM".equals(authType)) {
            log.info("Not NTLM authType");
            return false;
        }
           
        // Pick the user name and password from the remoteUser & (possibly)
        // NTLM challenge
        String ntUsername = getNTUsername(remoteUser);
        String ntDomain = getNTDomain(remoteUser);
        String ntPassword = null; // Use NTLM to get the 
password/credential data?
       
        //FIXME
        String parameterPassword = request.getParameter("password");
        if(ntPassword == null && parameterPassword != null) {
            log.warn("Using supplied password to spoof the realm.");
            ntPassword = parameterPassword;
        }
       
        log.debug("Authorizing user [" +
                ntDomain + ":" +
                ntUsername +
                "] using supplied NTLM credentials.");
       
       
        // Instead of null, it would be good to be able to actually
        // provide some mechanism of knowing how to trust this at the
        // LoginModule layer (if indeed this goes to the login module
        // layer)
        Realm realm = getContext(catRequest).getRealm();
       

        log.debug("Realm container: " + realm.getContainer().getName());
        log.debug("Realm info: " + realm.getInfo());
       
        String constraints = "";
        SecurityConstraint[] sc = 
realm.findSecurityConstraints(catRequest, getContext(catRequest));
        for(int i = 0; i < sc.length; i++) {
            constraints += sc[i] + ", ";
        }
        log.debug("Realm info: " + constraints);
       
        String challengeUsername = ntUsername;
        if("true".equals(removeDomainFromPrincipal)) {
            userPrincipal.getName();           
        }
       
        log.info("Challenging the realm with [" +
                challengeUsername +
                ":" +
                ntPassword +
                "] using supplied NTLM credentials.");
       
        Principal realmPrincipal =
            realm.authenticate(challengeUsername,ntPassword);
       
        if(realmPrincipal == null) {
            log.info("User was not authenticated in the realm.");
            HttpServletResponse hres =
                (HttpServletResponse) catResponse.getResponse();
            hres.setStatus(HttpServletResponse.SC_FORBIDDEN);
            return false;
        }

//            Set the username/password on the session and set the 
principal in request
//            session.setNote(Constants.SESS_USERNAME_NOTE, ntUsername);
//            session.setNote(Constants.SESS_PASSWORD_NOTE, 
ntPassword);           
       
        log.info("User was authenticated. Registering in request.");
        // Register the retrieved realm information
        register(catRequest, catResponse, realmPrincipal, authType, 
challengeUsername, ntPassword);
   
        return true;
    }
   
   
    final static int NTDOMAIN = 1;
    final static int NTUSER = 0;
    public String getNTUsername(String ntRemoteUser) {
        return splitNTRemoteUser(ntRemoteUser, NTUSER);
    }
    public String getNTDomain(String ntRemoteUser) {
        return splitNTRemoteUser(ntRemoteUser, NTDOMAIN);
    }
    public String splitNTRemoteUser(String ntRemoteUser, int position) {
        if(ntRemoteUser == null)
            return null;
       
        String[] parts = ntRemoteUser.split("\\\\");
       
        // Reverse the order to give priority to the name
        String[] tmp = new String[parts.length];
        for(int i = 0; i < parts.length; i++)
            tmp[i] = parts[parts.length-1 -i];
        parts = tmp;
       
        // If domain isn't supplied return null for the domain
        if(position >= parts.length)
            return null;
       
        return parts[position];       
    }
   
}









/*
 * Copyright 2007 Louis A. J. Pierrson
 * from original code
 * Copyright 1999-2001,2004 The Apache Software Foundation.
 *
 * Licensed 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.lang.reflect.Method;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.catalina.Authenticator;
import org.apache.catalina.Container;
import org.apache.catalina.Context;
import org.apache.catalina.HttpRequest;
import org.apache.catalina.HttpResponse;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Logger;
import org.apache.catalina.Manager;
import org.apache.catalina.Pipeline;
import org.apache.catalina.Realm;
import org.apache.catalina.Request;
import org.apache.catalina.Response;
import org.apache.catalina.Session;
import org.apache.catalina.Valve;
import org.apache.catalina.ValveContext;
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.commons.logging.Log;
import org.apache.commons.logging.LogFactory;


/**
 * JBoss 3.2.5 doesn't properly support the context elements and anything
 * it doesn't explicitly support aren't supported at all.
 * 3.2.6 accepts and reads the context.xml file.
 * This is a work around needed to define the authenticator in
 * a &lt;context&gt;
 * <p>
 * Specifically, this AuthenticatorBase dynamically checks which context it
 * is being called by to determine the relevant context specific 
information.
 * (Like which SecurityRealm applies to the context)
 * <p>
 * This class completelely wraps the AuthenticatorBase in order to be 
able to
 * dynamically discover which Context(and thus Realm) a Request belongs 
to. (This
 * is why the original comments for the AuthenticatorBase are included.)
 *
 * @author Louis A. J. Pierrson
 * @see AuthenticatorBase
 *
 *<p>
 * Basic implementation of the <b>Valve</b> interface that enforces the
 * <code>&lt;security-constraint&gt;</code> 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.
 * <p>
 * <b>USAGE CONSTRAINT</b>:  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.
 * <p>
 * <b>USAGE CONSTRAINT</b>:  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: 1.19 $ $Date: 2004/04/26 21:54:15 $
 */


public abstract class ContextLookupAuthenticatorBase
    extends AuthenticatorBase
    implements Authenticator, Lifecycle {
    private static Log log = 
LogFactory.getLog(ContextLookupAuthenticatorBase.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
     * <code>java.security.MessageDigest</code> 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;


    /**
     * The debugging detail level for this component.
     */
    protected int debug = 0;


    /**
     * 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;

    /**
     * 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;

    }

    /**
     * Checks if the defined <code>container</code> is a Context. If not
     * it will look up the context for the supplied request.
     *
     * @param request
     * @return
     */
    protected Context getContext(Request request) {
        if(this.context instanceof Context)
            return this.context;
        else
            return request.getContext();
    }

    /**
     * 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) {
        // Ignore behaviour of original AuthenticatorBase
        //super.setContainer(container);
        this.container = container;

        if (!(container instanceof Context)) {
            log.warn("Running using context lookup for each request.");
//            throw new IllegalArgumentException
//                (sm.getString("authenticator.notContext"));
        } else {
            this.context = (Context) container;
        }


    }


    /**
     * Return the debugging detail level for this component.
     */
    public int getDebug() {

        return (this.debug);

    }


    /**
     * Set the debugging detail level for this component.
     *
     * @param debug The new debugging detail level
     */
    public void setDebug(int debug) {

        this.debug = debug;

    }


    /**
     * 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 <code>true</code> if we add headers to disable proxy
     *              caching, <code>false</code> if we leave the headers 
alone.
     */
    public void setDisableProxyCaching(boolean nocache) {
        disableProxyCaching = nocache;
    }

    // --------------------------------------------------------- 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
     * @param context The valve context used to invoke the next valve
     *  in the current processing pipeline
     *
     * @exception IOException if an input/output error occurs
     * @exception ServletException if thrown by a processing element
     */
    public void invoke(Request request, Response response,
                       ValveContext context)
        throws IOException, ServletException {

        // If this is not an HTTP request, do nothing
        if (!(request instanceof HttpRequest) ||
            !(response instanceof HttpResponse)) {
            context.invokeNext(request, response);
            return;
        }
        if (!(request.getRequest() instanceof HttpServletRequest) ||
            !(response.getResponse() instanceof HttpServletResponse)) {
            context.invokeNext(request, response);
            return;
        }
        HttpRequest hrequest = (HttpRequest) request;
        HttpResponse hresponse = (HttpResponse) response;
        if (log.isDebugEnabled())
            log.debug("Security checking request " +
                ((HttpServletRequest) request.getRequest()).getMethod() 
+ " " +
                ((HttpServletRequest) 
request.getRequest()).getRequestURI());
        LoginConfig config = getContext(request).getLoginConfig();
       
        if(config == null || !"IIS-NTLM".equals(config.getAuthMethod())) {
            context.invokeNext(request, response);
            return;
        }

        // Have we got a cached authenticated Principal to record?
        if (cache) {
            Principal principal =
                ((HttpServletRequest) 
request.getRequest()).getUserPrincipal();
            if (principal == null) {
                Session session = getSession(hrequest);
                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());
                        hrequest.setAuthType(session.getAuthType());
                        hrequest.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 = getContext(request).getPath();
        String requestURI = hrequest.getDecodedRequestURI();
        if (requestURI.startsWith(contextPath) &&
            requestURI.endsWith(Constants.FORM_ACTION)) {
            if (!authenticate(hrequest, hresponse, config)) {
                if (log.isDebugEnabled())
                    log.debug(" Failed authenticate() test ??" + 
requestURI );
                return;
            }
        }

        Realm realm = getContext(request).getRealm();
        // Is this request URI subject to a security constraint?
        SecurityConstraint [] constraints
            = realm.findSecurityConstraints(hrequest, getContext(request));
      
        if ((constraints == null) /* &&
            (!Constants.FORM_METHOD.equals(config.getAuthMethod())) */ ) {
            if (log.isDebugEnabled())
                log.debug(" Not subject to any constraint");
            context.invokeNext(request, response);
            return;
        }

        // Make sure that constrained resources are not cached by web 
proxies
        // or browsers as caching can provide a security hole
        HttpServletRequest hsrequest = 
(HttpServletRequest)hrequest.getRequest();
        if (disableProxyCaching &&
            // FIXME: Disabled for Mozilla FORM support over SSL
            // (improper caching issue)
            //!hsrequest.isSecure() &&
            !"POST".equalsIgnoreCase(hsrequest.getMethod())) {
            HttpServletResponse sresponse =
                (HttpServletResponse) response.getResponse();
            sresponse.setHeader("Pragma", "No-cache");
            sresponse.setHeader("Cache-Control", "no-cache");
            sresponse.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(hrequest, hresponse,
                                         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;
        }
      
        for(i=0; i < constraints.length; i++) {
            // Authenticate based upon the specified login configuration
            if (constraints[i].getAuthConstraint()) {
                if (log.isDebugEnabled()) {
                    log.debug(" Calling authenticate()");
                }
                if (!authenticate(hrequest, hresponse, 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;
                } else {
                    break;
                }
            }
        }
        if (log.isDebugEnabled()) {
            log.debug(" Calling accessControl()");
        }
        if (!realm.hasResourcePermission(hrequest, hresponse,
                                         constraints,
                                         getContext(request))) {
            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");
        }
        context.invokeNext(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 <code>true</code> if any specified
     * constraint has been satisfied, or <code>false</code> 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(HttpRequest request,
                                            HttpResponse 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);

    }


    /**
     * Return the internal Session that is associated with this HttpRequest,
     * or <code>null</code> if there is no such Session.
     *
     * @param request The HttpRequest we are processing
     */
    protected Session getSession(HttpRequest request) {

        return (getSession(request, false));

    }


    /**
     * Return the internal Session that is associated with this HttpRequest,
     * possibly creating a new one if necessary, or <code>null</code> if
     * there is no such session and we did not create one.
     *
     * @param request The HttpRequest we are processing
     * @param create Should we create a session if needed?
     */
    protected Session getSession(HttpRequest request, boolean create) {

        HttpServletRequest hreq =
            (HttpServletRequest) request.getRequest();
        HttpSession hses = hreq.getSession(create);
        if (hses == null)
            return (null);
        Manager manager = getContext(request).getManager();
        if (manager == null)
            return (null);
        else {
            try {
                return (manager.findSession(hses.getId()));
            } catch (IOException e) {
                return (null);
            }
        }

    }


    /**
     * Log a message on the Logger associated with our Container (if any).
     *
     * @param message Message to be logged
     */
    protected void log(String message) {

        Logger logger = context.getLogger();
        if (logger != null)
            logger.log("Authenticator[" + context.getPath() + "]: " +
                       message);
        else
            System.out.println("Authenticator[" + context.getPath() +
                               "]: " + message);

    }


    /**
     * Log a message on the Logger associated with our Container (if any).
     *
     * @param message Message to be logged
     * @param throwable Associated exception
     */
    protected void log(String message, Throwable throwable) {

        Logger logger = null;
        if(context != null)
            logger = context.getLogger();
       
       
        if (logger != null)
            logger.log("Authenticator[" + context.getPath() + "]: " +
                       message, throwable);
        else {
            System.out.println("Authenticator[" + context.getPath() +
                               "]: " + message);
            throwable.printStackTrace(System.out);
        }

    }


    /**
     * Attempts reauthentication to the <code>Realm</code> using
     * the credentials included in argument <code>entry</code>.
     *
     * @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, HttpRequest 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, getSession(request, true));

            if (log.isDebugEnabled()) {
                HttpServletRequest hreq =
                    (HttpServletRequest) request.getRequest();
                log.debug(" Reauthenticated cached principal '" +
                          hreq.getUserPrincipal().getName() +
                          "' with auth type '" +  hreq.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(HttpRequest request, HttpResponse response,
                            Principal principal, String authType,
                            String username, String password) {

        if (log.isDebugEnabled())
            log.debug("Authenticated '" + principal.getName() + "' with 
type '"
                + authType + "'");

        // Cache the authentication information in our request
        request.setAuthType(authType);
        request.setUserPrincipal(principal);

        Session session = getSession(request, 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
            HttpServletResponse hres =
                (HttpServletResponse) response.getResponse();
            ssoId = generateSessionId();
            Cookie cookie = new Cookie(Constants.SINGLE_SIGN_ON_COOKIE, 
ssoId);
            cookie.setMaxAge(-1);
            cookie.setPath("/");
            hres.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 = getSession(request, 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 
<code>configure()</code>,
     * 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);
        if(context != null)
        if ("org.apache.catalina.core.StandardContext".equals
            (context.getClass().getName())) {
            try {
                // XXX What is this ???
                Class paramTypes[] = new Class[0];
                Object paramValues[] = new Object[0];
                Method method =
                    container.getClass().getMethod("getDebug", paramTypes);
                Integer result = (Integer) method.invoke(context, 
paramValues);
                setDebug(result.intValue());
            } catch (Exception e) {
                log.error("Exception getting debug value", e);
            }
        }
        started = true;

        // Look up the SingleSignOn implementation in our request processing
        // path, if there is one
        Container parent = container;
       
        if(context != null)
            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;

    }


}


---------------------------------------------------------------------
To start a new topic, e-mail: users@tomcat.apache.org
To unsubscribe, e-mail: users-unsubscribe@tomcat.apache.org
For additional commands, e-mail: users-help@tomcat.apache.org


Re: TrustedPrincipalAuthorizor (extends AuthenticatorBase) Discussion / Comments

Posted by Louis <la...@gmail.com>.
Christopher Schultz wrote:
> Louis wrote:
>   
>> When trying to decide which Authentication I should use I reviewed the
>> source for the existing ones and realized that none of them would work.
>> The existing authenticators all check to see if a principal is already
>> in the request. If it is then the user has already been authenticated
>> and the authenticator never goes to the Realm to see what permissions /
>> roles the user has.
>>     
>
> That's not how it's supposed to work; authentication and authorization
> are separate. Without looking at the source (don't have it handy right
> now), the flow ought to be:
>
> - - Determine authentication and authorization requirements
>   based upon request URL
> - - Check request for principal
>   |- if not found, check request for credentials
>   |   - if found, load principal into request
>   |   - if not found, display login challenge and exit
> - - Use found principal for authorization (check roles)
>
> Tomcat's built-in Principal and Request objects conspire to correctly
> wire HttpServletRequest.isUserInRole to something within the Principal.
> If you choose to gather your Principal information from somewhere else
> (NTLM), then you need to take care to play along with Tomcat's notion of
> where roles are stored.
>
> If Tomcat does not use the HttpServletRequest.isUserInRole method to
> perform its authorization, then you will need to pay attention to the
> way it /does/ check for the roles.
>
>   
>> Since the IIS connector plugs-in a Principal for the
>> user, this effectively short-circuited the ability of the container to
>> bind roles to the user.
>>     
>
> You should be able to wrap that Principal in some other object if necessary.
>
>   
>> My solution was to code my own TrustedPrincipalAuthorizor (extends
>> AuthenticatorBase). I called it an authorizor since it wasn't doing any
>> actual Authentication.
>>     
>
> Is it doing any authorization, or is it just attaching roles to a
> Principal and allowing Tomcat to perform the authorization?
>
>   
>> This class checks for the existence of a
>> principal in the request. It then uses that principal as the key to tell
>> the security realm to lookup the users permissions and bind them into
>> the container for this request.
>>     
>
> That seems a little time-consuming to do for every request. Since you're
> working with Tomcat's internals, why not permanently attach the role
> information to the Principal object one time?
>
>   
>> Ergo the name. The last step of the
>> puzzle is to make 'authenticating' (ie: checking the password) optional
>> in your realm. I coded my LoginModule to have a flag checkPassword="false".
>>
>> I know that's not the whole picture but it's the gist of it to get a
>> discussion going. What do people here think, am I following a logical
>> path or have I completely jumped the shark?
>>     
>
> Why not work with the code that the redirector uses to set the Principal
> in the first place? I think it would be a cleaner implementation to set
> the roles at the time the Principal object is created.
>
> - -chris
Thanks Chris for the detailed response.

I'll have to go back and review how the IIS isapi_redirector.dll is 
hooked into Tomcat to see if changing the redirector would better suit 
my needs.

The reason why I went with this style implementation: it was the first 
way I've figured out of implementing my requirements and it worked. (ie: 
not a good reason architecturally, it just worked). One argument in 
favour of this style of implementation is that it can work with other 
external redirector implementations that embed a Principal in the Request.

I'm not sure about your question re: Is it doing any authorization? My 
understanding of JAAS is that the binding of Roles to the Principal was 
the Authorization. Doing this does allows the security declarations in 
the web.xml to work as expected. ie: The container (tomcat/jboss/etc.) 
is doing the work of checking if the user is in the role.

Yes it is time consuming to do bind the roles on every request. I'm not 
sure yet how the redirector binds the Principal to the Request (per 
session or per request) and/or how Roles are normally bound to the 
Principal.
 I'll have to go back to the Tomcat source and trace what's happening in 
the Realm source.

I don't have the experience or tools to be able to create an updated 
redirector. I'm trying to make an implementation that isn't specific to 
the IIS redirector and could be used with other types of redirectors. 
Also, I'm trying to keep my implementation 'java' and I don't know what 
the redirector .dll is written in.

I see where you're going with your suggestions. If the Principal from 
the redirector is created once and the same one is used across requests 
(in a session) then it should be trivial to mutate it (by binding the 
roles in) instead of substituting it with a new one with the roles bound in.

Thanks for taking the time to write a detailed response. I'll get back 
to the list with my changes when I've got them done.

Best Regards,

Louis

---------------------------------------------------------------------
To start a new topic, e-mail: users@tomcat.apache.org
To unsubscribe, e-mail: users-unsubscribe@tomcat.apache.org
For additional commands, e-mail: users-help@tomcat.apache.org


Re: TrustedPrincipalAuthorizor (extends AuthenticatorBase) Discussion / Comments

Posted by Christopher Schultz <ch...@christopherschultz.net>.
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Louis,

Louis wrote:
> When trying to decide which Authentication I should use I reviewed the
> source for the existing ones and realized that none of them would work.
> The existing authenticators all check to see if a principal is already
> in the request. If it is then the user has already been authenticated
> and the authenticator never goes to the Realm to see what permissions /
> roles the user has.

That's not how it's supposed to work; authentication and authorization
are separate. Without looking at the source (don't have it handy right
now), the flow ought to be:

- - Determine authentication and authorization requirements
  based upon request URL
- - Check request for principal
  |- if not found, check request for credentials
  |   - if found, load principal into request
  |   - if not found, display login challenge and exit
- - Use found principal for authorization (check roles)

Tomcat's built-in Principal and Request objects conspire to correctly
wire HttpServletRequest.isUserInRole to something within the Principal.
If you choose to gather your Principal information from somewhere else
(NTLM), then you need to take care to play along with Tomcat's notion of
where roles are stored.

If Tomcat does not use the HttpServletRequest.isUserInRole method to
perform its authorization, then you will need to pay attention to the
way it /does/ check for the roles.

> Since the IIS connector plugs-in a Principal for the
> user, this effectively short-circuited the ability of the container to
> bind roles to the user.

You should be able to wrap that Principal in some other object if necessary.

> My solution was to code my own TrustedPrincipalAuthorizor (extends
> AuthenticatorBase). I called it an authorizor since it wasn't doing any
> actual Authentication.

Is it doing any authorization, or is it just attaching roles to a
Principal and allowing Tomcat to perform the authorization?

> This class checks for the existence of a
> principal in the request. It then uses that principal as the key to tell
> the security realm to lookup the users permissions and bind them into
> the container for this request.

That seems a little time-consuming to do for every request. Since you're
working with Tomcat's internals, why not permanently attach the role
information to the Principal object one time?

> Ergo the name. The last step of the
> puzzle is to make 'authenticating' (ie: checking the password) optional
> in your realm. I coded my LoginModule to have a flag checkPassword="false".
> 
> I know that's not the whole picture but it's the gist of it to get a
> discussion going. What do people here think, am I following a logical
> path or have I completely jumped the shark?

Why not work with the code that the redirector uses to set the Principal
in the first place? I think it would be a cleaner implementation to set
the roles at the time the Principal object is created.

- -chris
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.8 (MingW32)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org

iEYEARECAAYFAkdt67EACgkQ9CaO5/Lv0PCxWgCfTrpnpbtbocP5XFna6akX9hNM
UIIAoJwGgxGIL/wkVscAeh47g2rjk2RQ
=simi
-----END PGP SIGNATURE-----

---------------------------------------------------------------------
To start a new topic, e-mail: users@tomcat.apache.org
To unsubscribe, e-mail: users-unsubscribe@tomcat.apache.org
For additional commands, e-mail: users-help@tomcat.apache.org