1

I have been trying to mirror an implementation I had in Grails 2 for some time now as I try and upgrade to Grails 3.

I need to support X509 certificate based authentication using the "client-cert" auth method, that is, I only want to be prompted for a certificate once a protected resource has been requested. See current implementation below in Application.groovy.

@Bean
EmbeddedServletContainerCustomizer containerCustomizer() throws Exception {

    return new EmbeddedServletContainerCustomizer() {
        @Override
        public void customize(ConfigurableEmbeddedServletContainer container) {
            TomcatEmbeddedServletContainerFactory tomcat = (TomcatEmbeddedServletContainerFactory) container
            tomcat.addConnectorCustomizers(
                    new TomcatConnectorCustomizer() {
                        @Override
                        public void customize(Connector connector) {
                            connector.setPort(8443)
                            connector.setSecure(true)
                            connector.setScheme("https")

                            Http11NioProtocol proto = (Http11NioProtocol) connector.getProtocolHandler()
                            proto.setMinSpareThreads(5)
                            proto.setSSLEnabled(true)
                            proto.setClientAuth("false")

                            proto.setKeystoreFile("/tmp/keys/app.jks")
                            proto.setKeystorePass("changeit")
                            proto.setKeystoreType("JKS")
                            proto.setKeyAlias("ssl_server")
                            proto.setTruststoreFile("/tmp/keys/app.jts")
                            proto.setTruststoreType("JKS")
                            proto.setTruststorePass("changeit")
                        }
                    })
            tomcat.addContextCustomizers(new TomcatContextCustomizer() {
                @Override
                public void customize(Context context) {
                    context.setPath("/myapp")
                    SecurityConstraint sc = new SecurityConstraint()
                    SecurityCollection securityCollection = new SecurityCollection()
                    securityCollection.setName("Protected")
                    securityCollection.addPattern("/*")

                    sc.addCollection(securityCollection)

                    sc.addAuthRole("mySecureConnection")
                    sc.setUserConstraint("CONFIDENTIAL")
                    context.addConstraint(sc)
                    context.addSecurityRole("mySecureConnection")
                    context.setRealm(new MySecurityRealm())

                    LoginConfig loginConfig = new LoginConfig()
                    loginConfig.setAuthMethod("CLIENT-CERT")
                    loginConfig.setRealmName("MySecurityRealm")
                    context.setLoginConfig(loginConfig)
                    sc.setAuthConstraint(true)

                }
            });
        }
    }

But no matter how many different ways I try and cut it, the application will not request a cert upon access (which it should based on my catch all pattern above). Note that this mechanism does work as expected when clientAuth is set to true;

proto.setClientAuth("true")

but this means a cert is always requested which is not ultimately what I am looking for (I intend to update the pattern above). Any help would be much appreciated.

dre
  • 1,027
  • 1
  • 11
  • 31

1 Answers1

1

Got this working myself in the after working on a proof of concept at the vanilla tomcat and Spring Boot level before returning to Grails 3.3.x to apply what worked. I think probably the most important piece of the jigsaw was the addition of a tomcat valve component (using the SSLAuthenticator implementation obviously) which was the only way I could manage to get the browser to prompt for a certificate. This then required me to use a custom realm to retrieve the principal from the certificate (I know of no other way around this at present). Code is as follows;

@Bean
public EmbeddedServletContainerFactory servletContainer() {
    final TomcatEmbeddedServletContainerFactory tomcat = new TomcatEmbeddedServletContainerFactory();
    tomcat.addContextValves(new SSLAuthenticator());

    tomcat.addContextCustomizers(new TomcatContextCustomizer() {
        @Override
        public void customize(Context ctx) {

            String AUTH_ROLE = "mySecureRole";
            ctx.addSecurityRole(AUTH_ROLE);
            ctx.setRealm(new MySecurityRealm())

            LoginConfig config = new LoginConfig();
            config.setAuthMethod("CLIENT-CERT");
            config.setRealmName("MySecurityRealm");
            ctx.setLoginConfig(config);

            SecurityConstraint constraint = new SecurityConstraint();
            constraint.addAuthRole(AUTH_ROLE);
            SecurityCollection collection = new SecurityCollection();
            collection.addPattern("/secure");
            constraint.addCollection(collection);
            ctx.addConstraint(constraint);
        }
    })

    tomcat.addAdditionalTomcatConnectors(createConnector());
    return tomcat;
}


private Connector createConnector() {
    Connector connector = new Connector(TomcatEmbeddedServletContainerFactory.DEFAULT_PROTOCOL);

    connector.setPort(8443);
    connector.setSecure(true);
    connector.setScheme("https");

    Http11NioProtocol proto = (Http11NioProtocol) connector.getProtocolHandler();
    proto.setMinSpareThreads(5);
    proto.setSSLEnabled(true);
    proto.setClientAuth("false");
    proto.setSSLProtocol("all");

    proto.setKeystoreFile("/path/store.jks");
    proto.setKeystorePass("changeit");
    proto.setKeystoreType("JKS");
    proto.setKeyAlias("ssl_server");
    proto.setTruststoreFile("/path/store.jts");
    proto.setTruststoreType("JKS");
    proto.setTruststorePass("changeit");

    proto.setSSLVerifyDepth(2);
    return connector;
}

I'm leaving in the connector details for completeness but of course all of the important stuff is happening in the context customizer. Now, when I visit this web application I do not get prompted for a certificate. This only happens when I visit the /secure path which is exactly what I required.

dre
  • 1,027
  • 1
  • 11
  • 31