Mantis - Resin
Viewing Issue Advanced Details
4044 feature always 05-18-10 20:32 02-11-11 16:04
alex  
ferg  
normal  
closed 3.1.10  
fixed  
none    
none 4.0.16  
0004044: support for chained auth with client_cert and form auth
I am wanting to implement client certificate authentication using Resin 3.1.8/9/10 and JSSE. I also need to make the site compatible for customers that don't require certificate auth. We have a custom authenticator that constructs and returns a subclass of Principal from its loginImpl method, and I would like to keep this behaviour. I quickly got basic Client Certificate authentication working, and thought I was home and hosed but when I put in our custom authenticator, I found that no matter what I returned from the loginImpl method, the result of request.getUserPrincipal() was always the same - an X500Name object.

We use request.getUserPrincipal() throughout our code (an old style JSP based J2EE application) to get a Login object that contains the user's details - name, roles, etc. It is really imperative for us that this functionality continues. I tried overriding the ClientCertLogin class to provide a different version of authenticate and getUserPrincipal that returned our subclass of Principal, but even though I saw these methods being called, the request.getUserPrincipal was still an X500Name.

This is the setup I tried:

RESIN.CONF

        <http address="127.0.0.1" port="443" server-id="">
           <jsse-ssl>
               <key-store-type>jks</key-store-type>
               <key-store-file>/Users/adam/resin-3.1.8/registry-adam.keystore</key-store-file>
               <password>changeit</password>
               <verify-client>required</verify-client>
           </jsse-ssl>
        </http>


WEB.XML

    <!-- Authentication and Security Access Settings -->

    <!-- Uncomment for normal form based login -->
    <!--
    <authenticator>
        <type>test.SiteAuthenticator</type>
        <init>
            <password-digest>none</password-digest>
        </init>
    </authenticator>

    <login-config auth-method='FORM'>
        <form-login-config>
            <form-login-page>/login.jsp</form-login-page>
            <form-error-page>/login.jsp?error</form-error-page>
        </form-login-config>
    </login-config>
-->

    <!-- Uncomment for Certificate based login -->
    <login-config type="test.CertLogin">
        <auth-method>CLIENT-CERT</auth-method>
    </login-config>

    <!-- End of authentication stuff -->

CertLogin.java
<snip>
    public Principal authenticate(HttpServletRequest request, HttpServletResponse response, ServletContext context) throws ServletException, IOException {
        Principal x = null;
        try {
            System.out.println("XXXXXXXXXXXXXXXXXXXXXXXXXXX - CertLogin authenticate called");

            X509CertImpl cert = (X509CertImpl) request.getAttribute("javax.servlet.request.X509Certificate");
            System.out.println("XXX - subject DN is " + cert.getSubjectDN());
            System.out.println("XXX - subject DN name is " + cert.getSubjectDN().getName());
            System.out.println("XXX - subject DN class is " + cert.getSubjectDN().getClass());
            System.out.println("XXX - subject DN common name is " + ((X500Name) cert.getSubjectDN()).getCommonName());

        // This bit does the authentication using our custom Authenticator and returns us a Login object if successful
        // This object should get stores as the session user and returned to the caller (AbstractHttpRequest.authenticate())
                x = authenticator.login(request, response, context, ((X500Name) cert.getSubjectDN()).getCommonName(), null);
                if (x != null) {
                    ((SessionImpl) request.getSession(true)).setUser(x);
                }
        } catch (Exception e) {
            e.printStackTrace(System.out);
        } finally {
            System.out.println("XXX - super authenticate returned a " + x);
        }
        return x;
    }
<snip>

Yeah, so this code and setup worked - in that each part of it was used, the code was called, the correct object was returned and put into the SessionImpl - but request.getUserPrincipal() stubbornly continued to return the X500Name!!!

The reason for this is in the class HttpRequest (com.caucho.server.http.HttpRequest), on line 1548 (v3.1.10) - the method is called initAttributes, and it is responsible for initializing the attributes of the request before any processing by user code occurs. The method's source is as follows:

 /**
   * Initialize any special attributes.
   */
  private void initAttributes()
  {
    _initAttributes = true;

    TcpConnection tcpConn = _tcpConn;
    
    if (! _isSecure || tcpConn == null)
      return;

    QSocket socket = tcpConn.getSocket();

    String cipherSuite = socket.getCipherSuite();
    super.setAttribute("javax.servlet.request.cipher_suite", cipherSuite);

    int keySize = socket.getCipherBits();
    if (keySize != 0)
      super.setAttribute("javax.servlet.request.key_size",
                         new Integer(keySize));

    try {
      X509Certificate []certs = socket.getClientCertificates();
      if (certs != null && certs.length > 0) {
        super.setAttribute("javax.servlet.request.X509Certificate", certs[0]);
        //super.setAttribute(com.caucho.server.security.AbstractAuthenticator.LOGIN_NAME,
                           //certs[0].getSubjectDN());
      }
    } catch (Exception e) {
      log.log(Level.FINER, e.toString(), e);
    }
  }

The block where my problem is detects any client certificates and if found, sets the javax.servlet.request.X509Certificate attribute with the certificate. I have commented out line 1548 and 9 - where my problem occurs - because this sets the Certificate DN into the LOGIN_NAME attribute. This attribute is special - if we look at AbstractHttpRequest (the parent class) and the getUserPrincipal method:

  /**
   * Returns the Principal representing the logged in user.
   */
  public Principal getUserPrincipal()
  {
    try {
      Principal user;
      user = (Principal) getAttribute(AbstractAuthenticator.LOGIN_NAME);

      if (user != null)
    return user;

      if (_session == null)
        getSession(false);
      
      // If the user object is already an attribute, return it.
      if (_session != null) {
        user = _session.getUser();
        if (user != null)
          return user;
      }

      WebApp app = getWebApp();
      if (app == null)
        return null;
    
      // If the authenticator can find the user, return it.
      AbstractLogin login = app.getLogin();

      if (login != null) {
        user = login.getUserPrincipal(this, getResponse(), app);

        if (user != null) {
          getSession(true);
          
          _session.setUser(user);

      _response.setPrivateCache(true);
        }
    else {
      // server/123h, server/1920
      // distinguishes between setPrivateCache and setPrivateOrResinCache
      // _response.setPrivateOrResinCache(true);
    }
      }

      return user;
    } catch (ServletException e) {
      log.log(Level.WARNING, e.toString(), e);

      return null;
    }
  }

The line I have coloured RED will check the LOGIN_NAME attribute first and if that attribute is set, will not go on to check the Session's User - the Principal returned by the ClientCertLogin.

I think that HttpRequest should not set the certificate DN into the LOGIN_USER attribute, but should allow the Session's user principal to be retrieved via the normal course of events. Commenting out line 1548 allows this to happen - I have made this modification and now I can set a subclass of Principal into the request as I would expect. It also allows developers to combine Client Cert authentication with other types, such as a form before authenticating. If the HttpRequest automatically populates its UserPrincipal from the SSL Certificate there's no way to make a decision about the authentication, or whether in fact the certificate is valid or the user allowed! Even if you return null from ClientCertLogin.authenticate, the next request will have the User Principal set from the Certificate.

Notes
(0004595)
alex   
05-19-10 09:44   
The client certificate authentication stuff is really cool, I think it very uniquely identifies people but I really wanted to chain 2 auth methods together so I could do some checking on the certificate, and also make use of that custom login UserPrincipal I mentioned. With no chaining, the certificate logs you in with a DN but you get no chance to check who signed the certificate! That means any cert that's trusted by the JVM can log in - I could make a self signed cert with the details for you, for example and as long as Java trusted my cert (by importing it into the keystore) I could log in as you. As it is now you can't do anything about vetoing a login, because every HttpRequest has the UserPrincipal set!

The part of the app I am writing right now extends the certificate auth to specify a signing DN , so I can check that the signer is who I expect it to be. I am putting that in my ServletAuthenticator so that when someone logs in we can ask for a password, and also check their certificate.

One neat quirk I have found with Resin is you can leave that JSSE "verify-client" turned on so that the client needs a cert, then set up form based authentication on the web app. I am doing this so the user gets bounced to a J2EE login form, and needs to specify the password and a secure token. I populate j_username from the certificate's CN. Then in my ServletAuthenticator I can check the username and make sure it's the certificate's CN (no changing who you're logged in as) and check the password. This seems to be a nice extra layer of security!

Just an aside, I would have thought that it was the job of the ClientCertLogin class to set the request's user principal? That class could easily get the certificate's DN and return it from its authenticate method - that way if the developer wanted teh default behaviour (i.e. the X500Name being set as the user principal) then they could just use that default login class and and they'd not notice any change.
(0004614)
adamknight   
05-26-10 21:03   
I reported this issue, my licence # is 1011956