Tuesday, 14 January 2020

Enabling https in DXA Spring Boot Application

In my previous article, I presented the steps for converting a Java DXA webapp to a Spring Boot application. I would now like to present the steps for enabling https in this context.

Spring Boot's documentation specifies a few simple configuration additions to the application.properties file to enable https. However, in my experience these were insufficient to make the DXA Spring Boot application connect to the Tridion Sites microservices over https.

The alternative working approach involved extending the embedded Tomcat application server by setting some properties against the Connector and Http11NioProtocol.

The following bespoke properties were added to the dxa.properties file.

# Tomcat connector configuration
server.tomcat.port=8080
server.tomcat.ssl.enabled=true
server.tomcat.ssl.key-alias=ssl-key-alias.com
server.tomcat.ssl.key-store=certificates/key-store-file.jks
server.tomcat.ssl.key-password=SslKeyPassw0rd!
server.tomcat.ssl.key-store-type=JKS
server.tomcat.ssl.trust-store=certificates/trustStore.jks
server.tomcat.ssl.trust-store-password=SslTrustStorePassw0rd!
server.tomcat.ssl.certificate-fs-store-location=DXA/certificates

The certificate files are placed within the /src/main/resources/certificates folder in the application. This means these files will be embedded in the built WAR file inside the /certificates folder.

The certificate files cannot be referenced using the absolute path when deployed by Spring Boot inside an embedded Tomcat application server, so these need to be written to the file system onto the location specified by the server.tomcat.ssl.certificate-fs-store-location property.

IMPORTANT NOTE: if using Maven with a <resource> configuration entry specifying <filtering>true</filtering>, this will cause the certificate files (.jks in this example) to become corrupted.

The bespoke properties are referenced in the Spring Boot application using the @Value annotation.

@Value("${server.tomcat.port}")
private Integer port;

@Value("${server.tomcat.ssl.enabled:false}")
private boolean enableSsl;

@Value("${server.tomcat.ssl.key-alias:}")
private String sslKeyAlias;

@Value("${server.tomcat.ssl.key-store:}")
private String sslKeyStore;

@Value("${server.tomcat.ssl.key-password:}")
private String sslKeyPassword;

@Value("${server.tomcat.ssl.key-store-type:}")
private String sslKeyStoreType;

@Value("${server.tomcat.ssl.trust-store:}")
private String sslTrustStore;

@Value("${server.tomcat.ssl.trust-store-password:}")
private String sslTrustStorePassword;

@Value("${server.tomcat.ssl.certificate-fs-store-location:}")
private String certificateFileSystemStoreLocation;

An org.springframework.core.io.ResourceLoader is autowired to support reading the .jks certificate files from the configured location (i.e. certificates/key-store-file.jks).

The customization is implemented by invoking the addConnectorCustomizers method against the TomcatEmbeddedServletContainerFactory as shown below.

@Bean
public EmbeddedServletContainerCustomizer addConnectorCustomizers(){
  return container -> {
    if(container instanceof TomcatEmbeddedServletContainerFactory){
      TomcatEmbeddedServletContainerFactory factory =
        (TomcatEmbeddedServletContainerFactory)container;
      factory.addConnectorCustomizers(connector -> {
        connector.setPort(port);
        if (enableSsl) {
          Http11NioProtocol protocol = 
            (Http11NioProtocol)connector.getProtocolHandler();
          connector.setScheme("https");
          connector.setSecure(true);
          protocol.setSSLEnabled(true);
          try {
            FileSystemResource keyStoreFile = new 
              FileSystemResource(getResource(sslKeyStore,
                sslKeyPassword));
            FileSystemResource trustStoreFile = new 
              FileSystemResource(getResource(sslTrustStore, 
                sslTrustStorePassword));

            System.setProperty("javax.net.ssl.trustStore", 
              trustStoreFile.getFile().getAbsolutePath());
            System.setProperty("javax.net.ssl.trustStorePassword",
              sslTrustStorePassword);

            protocol.setKeyAlias(sslKeyAlias);
            protocol.setKeyPass(sslKeyPassword);

            protocol.setKeystoreFile(keyStoreFile.getFile()
              .getAbsolutePath());
            protocol.setKeystorePass(sslKeyPassword);

            protocol.setTruststoreFile(trustStoreFile.getFile()
              .getAbsolutePath());

            protocol.setTruststorePass(sslTrustStorePassword);
          }
          catch (Exception exception) {
            throw new RuntimeException(
              "Error setting up the SSL configuration [" +
                exception.getMessage() + "]");
          }
        }
      });
    }
  };
}

Resources


For completeness, the supporting code and property extracts can be found here.

#@formatter:off
## You can find the complete list of properties on DXA documentation page.
## This file is an example of how to override DXA properties and configure your DXA application.
dxa.caching.required.caches=defaultCache, pageModels, entityModels, failures
# Disabling ADF results in a significant performance gain,
# but ADF is needed for XPM Session Preview, Experience Optimization and Context Expressions.
dxa.web.adf.enabled=false
dxa.csrf.allowed=true
### ===================================================================================================================
### Model Service client configuration
### ===================================================================================================================
# By default DXA gets the URL of Model Service through Discovery Service. If you are not happy with this for any reason,
# you can specify the URL to your Model Service, which will be used instead.
dxa.model.service.url=https://model-service-url:9082
# Model Service doesn't have its own CIS capability and registers as a part of Content Service. This property sets a key name for the URL in extension properties of CS.
dxa.model.service.key=dxa-model-service
# These four properties set a default mapping of MS REST endpoints. Unless you really know what you're doing, don't change them.
dxa.model.service.url.entity.model=/EntityModel/{uriType}/{localizationId}/{componentId}-{templateId}
dxa.model.service.url.page.model=/PageModel/{uriType}/{localizationId}/{pageUrl}?includes={pageInclusion}
dxa.model.service.url.api.navigation=/api/navigation/{localizationId}
dxa.model.service.url.api.navigation.subtree=/api/navigation/{localizationId}/subtree/{siteMapId}?includeAncestors={includeAncestors}&descendantLevels={descendantLevels}
# Tomcat connector configuration
server.tomcat.port=8080
server.tomcat.ssl.enabled=true
server.tomcat.ssl.key-alias=ssl-key-alias.com
server.tomcat.ssl.key-store=certificates/key-store-file.jks
server.tomcat.ssl.key-password=SslKeyPassw0rd!
server.tomcat.ssl.key-store-type=JKS
server.tomcat.ssl.trust-store=certificates/trustStore.jks
server.tomcat.ssl.trust-store-password=SslTrustStorePassw0rd!
# The certificates embedded inside the Spring Boot executable cannot be referenced, so need to be written to the file system to the location specified below
server.tomcat.ssl.certificate-fs-store-location=DXA/certificates
#@formatter:on
view raw dxa.properties hosted with ❤ by GitHub
package com.sdl.webapp.main;
import com.sdl.dxa.DxaSpringInitialization;
import org.apache.coyote.http11.Http11NioProtocol;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
@Import(DxaSpringInitialization.class)
@Configuration
public class SpringInitializer {
private static final Logger LOG = LoggerFactory.getLogger(SpringInitializer.class);
@Value("${server.tomcat.port}")
private Integer port;
@Value("${server.tomcat.ssl.enabled:false}")
private boolean enableSsl;
@Value("${server.tomcat.ssl.key-alias:}")
private String sslKeyAlias;
@Value("${server.tomcat.ssl.key-store:}")
private String sslKeyStore;
@Value("${server.tomcat.ssl.key-password:}")
private String sslKeyPassword;
@Value("${server.tomcat.ssl.key-store-type:}")
private String sslKeyStoreType;
@Value("${server.tomcat.ssl.trust-store:}")
private String sslTrustStore;
@Value("${server.tomcat.ssl.trust-store-password:}")
private String sslTrustStorePassword;
@Value("${server.tomcat.ssl.certificate-fs-store-location:}")
private String certificateFileSystemStoreLocation;
@Autowired
private ResourceLoader resourceLoader;
@Bean
public EmbeddedServletContainerCustomizer addConnectorCustomizers() {
return container -> {
if (container instanceof TomcatEmbeddedServletContainerFactory) {
TomcatEmbeddedServletContainerFactory factory = (TomcatEmbeddedServletContainerFactory)container;
factory.addConnectorCustomizers(connector -> {
connector.setPort(port);
if (enableSsl) {
Http11NioProtocol protocol = (Http11NioProtocol)connector.getProtocolHandler();
connector.setScheme("https");
connector.setSecure(true);
protocol.setSSLEnabled(true);
try {
FileSystemResource keyStoreFile = new FileSystemResource(
getResource(sslKeyStore, sslKeyPassword));
FileSystemResource trustStoreFile = new FileSystemResource(
getResource(sslTrustStore, sslTrustStorePassword));
System.setProperty("javax.net.ssl.trustStore", trustStoreFile.getFile().getAbsolutePath());
System.setProperty("javax.net.ssl.trustStorePassword", sslTrustStorePassword);
protocol.setKeyAlias(sslKeyAlias);
protocol.setKeyPass(sslKeyPassword);
protocol.setKeystoreFile(keyStoreFile.getFile().getAbsolutePath());
protocol.setKeystorePass(sslKeyPassword);
protocol.setTruststoreFile(trustStoreFile.getFile().getAbsolutePath());
protocol.setTruststorePass(sslTrustStorePassword);
}
catch (Exception exception) {
throw new RuntimeException("Error setting up the SSL configuration [" +
exception.getMessage() + "]");
}
}
});
}
};
}
private File getResource(String keyStorePath, String keyStorePassword) throws KeyStoreException,
NoSuchAlgorithmException, CertificateException, IOException {
Resource resource = resourceLoader.getResource(keyStorePath);
KeyStore keyStore = KeyStore.getInstance(sslKeyStoreType);
keyStore.load(resource.getInputStream(), keyStorePassword.toCharArray());
File dir = new File(certificateFileSystemStoreLocation);
if (!dir.exists())
dir.mkdirs();
File file = new File(dir.getPath() + "/" + resource.getFilename());
try (FileOutputStream fos = new FileOutputStream(file)) {
keyStore.store(fos, keyStorePassword.toCharArray());
}
return file;
}
}

3 comments:

  1. Hi Philip, thanks for this article, it is really useful!
    I am confused about the location of the key store file. Does that not live in the war file?

    ReplyDelete
    Replies
    1. Hi Jacques, thank you for your feedback. The keystore file is included inside the WAR file. I have updated the article with additional information. Please let me know if its still unclear.

      Delete
    2. Thanks Phil, all clear now.

      Delete