This is an update of a now out-of-date earlier walkthrough.

See also the related notes in Javalin 5 Secure Connections.

A recent upgrade to Javalin version 5 includes an upgrade of the underlying embedded Jetty web server to version 11.

My Javalin application uses a custom class to configure Javalin, via JavalinConfig:

config.jetty.server(DemoJetty::create);

But that DemoJetty class has changed quite a lot (partly due to the upgrade, partly due to some changes I wanted to make, and partly due to some fixes I added.)

The main change is to add logic to redirect all insecure (http) traffic to a secure (https) connection.

I also reorganized the code to make it clearer what is going on, and deliberately excluded some cipher suites which are reported as being weak.

The code uses a Java keystore - see demo.keystore.path. But you can use a PEM-formatted certificate also (not shown here).

DemoProperties is an enum to hold values read from a standard properties file.

The code:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import java.util.Properties;
import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
import org.eclipse.jetty.http2.HTTP2Cipher;
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.ssl.SslContextFactory;

/**
 * Configuration of the embedded Jetty server is controlled here, including
 * activation of HTTPS and HTTP/2.
 */
public class DemoJetty {

    private static final Server server = new Server();
    private static final Properties props = DemoProperties.INSTANCE.getProps();

    public static Server create() {

        // apply the connectors:
        server.setConnectors(new Connector[]{
            createHttpConnector(),
            createHttpsConnector()
        });
        // redirect all http to https:
        return redirectHttpToHttps(server);
    }

    private static ServerConnector createHttpConnector() {
        //
        // HTTP (Insecure) Connector
        //
        HttpConfiguration httpConfig = new HttpConfiguration();
        httpConfig.setSendServerVersion(false);
        httpConfig.addCustomizer(createSecureRequestCustomizer());
        // for redirecting http to https:
        httpConfig.setSecureScheme("https");
        int httpsPort = Integer.parseInt(props.getProperty("demo.https.port"));
        httpConfig.setSecurePort(httpsPort);
        // HTTP/1.1:
        HttpConnectionFactory httpCF = new HttpConnectionFactory(httpConfig);
        // create the connector:
        ServerConnector httpConnector = new ServerConnector(server, httpCF);
        // set the connector's port:
        httpConnector.setPort(Integer.parseInt(props.getProperty("demo.http.port")));

        return httpConnector;
    }

    private static ServerConnector createHttpsConnector() {
        //
        // HTTPS (Secure) Connector
        //
        HttpConfiguration httpConfig = new HttpConfiguration();
        httpConfig.setSendServerVersion(false);
        httpConfig.addCustomizer(createSecureRequestCustomizer());
        // HTTP/1.1:
        HttpConnectionFactory httpCF = new HttpConnectionFactory(httpConfig);
        // HTTP/2 connections:
        HTTP2ServerConnectionFactory http2CF = new HTTP2ServerConnectionFactory(httpConfig);
        // ALPN - a TLS extension:
        ALPNServerConnectionFactory alpnCF = new ALPNServerConnectionFactory();
        alpnCF.setDefaultProtocol(httpCF.getProtocol()); // fallback protocol
        // add SSL/TLS:
        SslConnectionFactory tlsHttp2CF = new SslConnectionFactory(getSslContextFactory(), alpnCF.getProtocol());
        // create the connector:
        ServerConnector httpsConnector = new ServerConnector(server, tlsHttp2CF, alpnCF, http2CF, httpCF);
        // set the connector's port:
        int httpsPort = Integer.parseInt(props.getProperty("demo.https.port"));
        httpsConnector.setPort(httpsPort);

        return httpsConnector;
    }

    private static SecureRequestCustomizer createSecureRequestCustomizer() {
        // For the SecureRequestCustomizer, Jetty defaults are:
        //  - isSniRequired()          -> false
        //  - isSniHostCheck()         -> true
        //  - getStsMaxAge()           -> -1 (no max age)
        //  - isStsIncludeSubDomains() -> false
        // If you want to customize it, do something like this:
        //SecureRequestCustomizer secureRequestCustomizer = new SecureRequestCustomizer();
        //secureRequestCustomizer.setSniHostCheck(true);
        return new SecureRequestCustomizer(); // just use the defaults.
    }

    private static SslContextFactory.Server getSslContextFactory() {
        SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
        sslContextFactory.setKeyStorePath(props.getProperty("demo.keystore.path"));
        sslContextFactory.setKeyStorePassword(props.getProperty("demo.keystore.pass"));
        sslContextFactory.setCipherComparator(HTTP2Cipher.COMPARATOR);
        // These are weak, according to Jetty and https://www.ssllabs.com/ssltest/analyze.html:
        sslContextFactory.setExcludeCipherSuites(
                "^TLS_RSA_.*$",
                "^.*_RSA_.*_(MD5|SHA|SHA1)$",
                "^.*_DHE_RSA_.*$",
                "TLS_DHE_DSS_WITH_AES_128_CBC_SHA",
                "TLS_DHE_DSS_WITH_AES_256_CBC_SHA",
                "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA",
                "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA",
                "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
                "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
                "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384",
                "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256"
        );

        return sslContextFactory;
    }

    private static Server redirectHttpToHttps(Server server) {
        //
        // https://serverfault.com/questions/367660/how-to-have-jetty-redirect-http-to-https
        // https://stackoverflow.com/questions/24991508/jetty-9-2-1-redirect-http-to-https
        //
        // create a constraint: transport guarantee = CONFIDENTIAL
        ConstraintSecurityHandler security = new ConstraintSecurityHandler();
        Constraint constraint = new Constraint();
        constraint.setDataConstraint(Constraint.DC_CONFIDENTIAL);
        //make the constraint apply to all uri paths
        ConstraintMapping mapping = new ConstraintMapping();
        mapping.setPathSpec("/*");
        mapping.setConstraint(constraint);
        security.addConstraintMapping(mapping);
        // add the constraint:
        server.setHandler(security);
        return server;
    }

}

Additional Notes

The order in which protocols are listed is important, in this line:

1
ServerConnector httpsConnector = new ServerConnector(server, tlsHttp2CF, alpnCF, http2CF, httpCF);

This is described in the Jetty documentation:

The fact that the HTTP/2 protocol comes before the HTTP/1.1 protocol indicates that HTTP/2 is the preferred protocol for the server.

Note also that the default protocol set in the ALPN ConnectionFactory, which is used in case ALPN is not supported by the client, is HTTP/1.1 — if the client does not support ALPN is probably an old client so HTTP/1.1 is the safest choice.

The same documentation walks through various configuration examples, some of which are used in the above code:

  • Clear-Text HTTP/1.1
  • Encrypted HTTP/1.1 (https)
  • Clear-Text HTTP/2
  • Encrypted HTTP/2
  • HTTP/3
  • Load balancer

You can shorten the list of excluded cipher suites, as shown above, by using regular expressions.

Below is the original list I was using, before I used regexes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
"TLS_DHE_DSS_WITH_AES_128_CBC_SHA",
"TLS_DHE_DSS_WITH_AES_256_CBC_SHA",
"TLS_DHE_RSA_WITH_AES_128_CBC_SHA",
"TLS_DHE_RSA_WITH_AES_128_CBC_SHA256",
"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_DHE_RSA_WITH_AES_256_CBC_SHA",
"TLS_DHE_RSA_WITH_AES_256_CBC_SHA256",
"TLS_DHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA",
"TLS_ECDH_RSA_WITH_AES_128_CBC_SHA",
"TLS_ECDH_RSA_WITH_AES_256_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384",
"TLS_RSA_WITH_AES_128_CBC_SHA",
"TLS_RSA_WITH_AES_128_CBC_SHA256",
"TLS_RSA_WITH_AES_128_GCM_SHA256",
"TLS_RSA_WITH_AES_256_CBC_SHA",
"TLS_RSA_WITH_AES_256_CBC_SHA256",
"TLS_RSA_WITH_AES_256_GCM_SHA384"

The list of available cipher suites, resulting from the exclusions list (as reported by SSL Labs testing):

TLS 1.3 (suites in server-preferred order):

1
2
3
TLS_AES_256_GCM_SHA384
TLS_AES_128_GCM_SHA256
TLS_CHACHA20_POLY1305_SHA256

TLS 1.2 (suites in server-preferred order):

1
2
3
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256