Tuesday, October 26, 2010

JAASRealm + SSL

http://tomcat.apache.org/tomcat-6.0-doc/realm-howto.html#JAASRealm

This is a good article on the topic: http://blog.frankel.ch/custom-loginmodule-in-tomcat. Some of following code is borrowed from the article.

Following steps are listed on the official tomcat doc. I will elaborate each of them.

  1. "Write your own LoginModule, User and Role classes based on JAAS (see the JAAS Authentication Tutorial and the JAAS Login Module Developer's Guide) to be managed by the JAAS Login Context (javax.security.auth.login.LoginContext) When developing your LoginModule, note that JAASRealm's built-in CallbackHandler only recognizes the NameCallback and PasswordCallback at present. "
    package test;
    
    import java.io.IOException;
    import java.util.Map;
    
    import javax.security.auth.Subject;
    import javax.security.auth.callback.Callback;
    import javax.security.auth.callback.CallbackHandler;
    import javax.security.auth.callback.NameCallback;
    import javax.security.auth.callback.PasswordCallback;
    import javax.security.auth.callback.UnsupportedCallbackException;
    import javax.security.auth.login.LoginException;
    import javax.security.auth.spi.LoginModule;
    
    /**
     * Login module that simply matches name and password to perform authentication.
     * If successful, set principal to name and credential to "AuthorizedUser".
     *
     * @author Nicolas Fränkel. Modified by Gerald Guo.
     * @since 2 avr. 2009
     */
    public class PlainLoginModule implements LoginModule {
    
        /** Callback handler to store between initialization and authentication. */
        private CallbackHandler handler;
    
        /** Subject to store. */
        private Subject subject;
    
        /** Login name. */
        private String login;
    
        /**
         * This implementation always return false.
         *
         * @see javax.security.auth.spi.LoginModule#abort()
         */
        @Override
        public boolean abort() throws LoginException {
    
            return false;
        }
    
        /**
         * This is where, should the entire authentication process succeeds,
         * principal would be set.
         *
         * @see javax.security.auth.spi.LoginModule#commit()
         */
        @Override
        public boolean commit() throws LoginException {
    
            try {
    
                PlainUserPrincipal user = new PlainUserPrincipal(login);
                PlainRolePrincipal role = new PlainRolePrincipal("AuthorizedUser");
    
                subject.getPrincipals().add(user);
                subject.getPrincipals().add(role);
    
                return true;
    
            } catch (Exception e) {
    
                throw new LoginException(e.getMessage());
            }
        }
    
        /**
         * This implementation ignores both state and options.
         *
         * @see javax.security.auth.spi.LoginModule#initialize(javax.security.auth.Subject,
         *      javax.security.auth.callback.CallbackHandler, java.util.Map,
         *      java.util.Map)
         */
        @Override
        public void initialize(Subject aSubject, CallbackHandler aCallbackHandler, Map aSharedState, Map aOptions) {
    
            handler = aCallbackHandler;
            subject = aSubject;
        }
    
        /**
         * This method checks whether the name and the password are the same.
         *
         * @see javax.security.auth.spi.LoginModule#login()
         */
        @Override
        public boolean login() throws LoginException {
    
            Callback[] callbacks = new Callback[2];
            callbacks[0] = new NameCallback("login");
            callbacks[1] = new PasswordCallback("password", true);
    
            try {
    
                handler.handle(callbacks);
    
                String name = ((NameCallback) callbacks[0]).getName();
                String password = String.valueOf(((PasswordCallback) callbacks[1]).getPassword());
    
                if (!name.equals(password)) {
    
                    throw new LoginException("Authentication failed");
                }
    
                login = name;
    
                return true;
    
            } catch (IOException e) {
    
                throw new LoginException(e.getMessage());
    
            } catch (UnsupportedCallbackException e) {
    
                throw new LoginException(e.getMessage());
            }
        }
    
        /**
         * Clears subject from principal and credentials.
         *
         * @see javax.security.auth.spi.LoginModule#logout()
         */
        @Override
        public boolean logout() throws LoginException {
    
            try {
    
                PlainUserPrincipal user = new PlainUserPrincipal(login);
                PlainRolePrincipal role = new PlainRolePrincipal("admin");
    
                subject.getPrincipals().remove(user);
                subject.getPrincipals().remove(role);
    
                return true;
    
            } catch (Exception e) {
    
                throw new LoginException(e.getMessage());
            }
        }
    }
  2. "Although not specified in JAAS, you should create seperate classes to distinguish between users and roles, extending javax.security.Principal, so that Tomcat can tell which Principals returned from your login module are users and which are roles (see org.apache.catalina.realm.JAASRealm). Regardless, the first Principal returned is always treated as the user Principal. "
    Also read the API doc http://tomcat.apache.org/tomcat-5.5-doc/catalina/docs/api/org/apache/catalina/realm/JAASRealm.html. If authentication succeeds, your LoginModule must attach at least a user principal and a user role to subject.
    package test;
    
    import java.security.Principal;
    
    public class PlainRolePrincipal implements Principal {
    
        String roleName;
        
        public PlainRolePrincipal(String name) {
            roleName = name;
        }
        public String getName() {
            return roleName;
        }
        
        public String toString() {
            return ("RolePrincipal: " + roleName);
        }   
    
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }   
            if (obj instanceof PlainRolePrincipal) {
                PlainRolePrincipal other = (PlainRolePrincipal) obj;
                return roleName.equals(other.roleName);
            }   
            return false;
        }   
    
        public int hashCode() {
            return roleName.hashCode();
        }   
    }
    You get the idea, you can implement class PlainUserPrincipal in a similar way.
  3. "Place the compiled classes on Tomcat's classpath "
  4. "Set up a login.config file for Java (see JAAS LoginConfig file) and tell Tomcat where to find it by specifying its location to the JVM, for instance by setting the environment variable: JAVA_OPTS=$JAVA_OPTS -Djava.security.auth.login.config==$CATALINA_BASE/conf/jaas.config "
    Create a JAAS config file:
    -----------------------------
    CertBasedCustomLogin {
        test.CertBasedLoginModule
        sufficient;
    };
    -----------------------------
    When you launch tomcat, use  -Djava.security.auth.login.config= to specify where the config file is stored.
  5. "Configure your security-constraints in your web.xml for the resources you want to protect"
    The goal of the whole process is to protect some resources. This step specifies which resources should be protected.
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>Secure Content</web-resource-name>
            <url-pattern>/cert-protected-users/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>AuthorizedUser</role-name>
        </auth-constraint>
        <user-data-constraint>
            <transport-guarantee>NONE</transport-guarantee>
        </user-data-constraint>
    </security-constraint>
    <!-- ... -->
    <login-config>
        <auth-method>CLIENT-CERT</auth-method>
        <realm-name>The Restricted Zone</realm-name>
    </login-config>
    <!-- ... -->
    <security-role>
        <description>The role required to access restricted content </description>
        <role-name>AuthorizedUser</role-name>
    </security-role>
    Basically, it says only users with role "AuthorizedUser" can access the resources cert-protected-users/*.
    Note:role-name must match the role attached to subject in step 1) ("AuthorizedUser" in our case) for successful access.
  6. "Configure the JAASRealm module in your server.xml"
    Actually, to put web app specific context config into server.xml is not recommended. Instead, I put a file named context.xml under directory META-INF.
    <Context>
        <Realm className="org.apache.catalina.realm.JAASRealm" appName="CertBasedCustomLogin"
            userClassNames="test.PlainUserPrincipal"
            roleClassNames="test.PlainRolePrincipal">
        </Realm>
    </Context>
    The value of appName must match the name specified in step 4.
  7. Add "-Dsun.security.ssl.allowUnsafeRenegotiation=true" for renegotiation support. (Read http://java.sun.com/javase/javaseforbusiness/docs/TLSReadme.html for more information)

Note

Some versions of Tomcat have problems to support JAASRealm + SSL mutual auth (https://issues.apache.org/bugzilla/show_bug.cgi?id=45576).  I tried 6.0.18, 6.0.20 and 6.0.29. Only 6.0.20 works for me. 6.0.29 gave errors when I tried.

More resources:

http://tomcat.apache.org/tomcat-6.0-doc/realm-howto.html#JAASRealm

http://tomcat.apache.org/tomcat-5.5-doc/catalina/docs/api/org/apache/catalina/realm/JAASRealm.html

http://wiki.metawerx.net/wiki/Web.xml.AuthConstraint

https://issues.apache.org/bugzilla/show_bug.cgi?id=45576

http://java.sun.com/javase/javaseforbusiness/docs/TLSReadme.html