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.
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.