Implementing 2 way SSL using spring boot

Andrew Mc picture Andrew Mc · Nov 19, 2015 · Viewed 32.3k times · Source

I'm creating some restful web services and am using Spring-Boot to create an embedded tomcat container.

One of the requirements is that this implements 2 way SSL. I've been looking at the HttpSecurity object and can get it to only run the webservices over an SSL channel using this:-

@Override
protected void configure(HttpSecurity http) throws Exception {

    System.out.println("CONFIGURED");

    http
        // ...
        .requiresChannel()
            .anyRequest().requiresSecure();
}

What I can't seem to find is a way of making the webservice only accessible to applications providing a valid client cert.

I have only a basic knowledge of SSL so even a general pointer in the right direction would be appreciated.

The server this is being deployed onto will have a mix of applications - this is the only one that needs to be locked down with 2-way SSL. What I'm really looking for is a way of locking down a single application to only accept client certificates.

Answer

veebee picture veebee · Dec 22, 2016

I came across a similar problem, and thought I’d share the solution I came with.

First, you need to understand that the SSL certificate authentication will be handled on your web server’s side (cfr. dur’s explanation, with the “clientAuth=want” setting). Then, your web app must be configured in order to handle the provided (and allowed) certificate, map it to a user etc.

The slight difference I have with you is that I’m packaging my spring boot application into a WAR archive, which is then deployed on an existing Tomcat application server.

My Tomcat’s server.xml configuration file defines an HTTPS connector as follows:

<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
    maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
    keystoreFile="/opt/tomcat/conf/key-stores/ssl-keystore.jks"
    keystorePass=“some-complex-password“
    clientAuth="want" sslProtocol="TLS"
    truststoreFile="/opt/tomcat/conf/trust-stores/ssl-truststore.jks"
    truststorePass=“some-other-complex-password” />

Small comment to avoid any confusion: keystoreFile contains the certificate/private key pair used for SSL (only), while truststoreFile contains the allowed CA certificates for client SSL authentication (note that you could also add the client certificates directly into that trust-store).

If you're using an embedded tomcat container with your spring boot application, you should be able to configure these settings in your application’s properties file, using the following property key/values:

server.ssl.key-store=/opt/tomcat/conf/key-stores/ssl-keystore.jks
server.ssl.key-store-password=some-complex-password
server.ssl.trust-store=/opt/tomcat/conf/trust-stores/ssl-truststore.jks
server.ssl.trust-store-password=some-other-complex-password
server.ssl.client-auth=want

Then, on my web app, I declare a specific SSL configuration as follows:

@Configuration
@EnableWebSecurity
//In order to use @PreAuthorise() annotations later on...
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SSLAuthConfiguration extends WebSecurityConfigurerAdapter {

    @Value("${allowed.user}")
    private String ALLOWED_USER;

    @Value("${server.ssl.client.regex}")
    private String CN_REGEX;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure (final HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
                .authorizeRequests()
                .antMatchers("/url-path-to-protect").authenticated() //Specify the URL path(s) requiring authentication...
            .and()
                .x509() //... and that x509 authentication is enabled
                    .subjectPrincipalRegex(CN_REGEX)
                    .userDetailsService(userDetailsService);
    }

    @Autowired
    //Simplified case, where the application has only one user...
    public void configureGlobal (final AuthenticationManagerBuilder auth) throws Exception {
        //... whose username is defined in the application's properties.
        auth
            .inMemoryAuthentication()
                .withUser(ALLOWED_USER).password("").roles("SSL_USER");
    }

}

I then need to declare the UserDetailsService bean (e.g. in my Application’s main class):

@Value("${allowed.user}")
private String ALLOWED_USER;

@Bean
public UserDetailsService userDetailsService () {

    return new UserDetailsService() {

        @Override
        public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException {
            if (username.equals(ALLOWED_USER)) {
                final User user = new User(username, "", AuthorityUtils.createAuthorityList("ROLE_SSL_USER"));
                return user;
            }
            return null;
        }
    };
}

And that’s it! I can then add @PreAuthorize(“hasRole(‘ROLE_SSL_USER’)”) annotations to the methods that I want to secure.

To sum things up a bit, the authentication flow will be as follows:

  1. User provides SSL certificate ;
  2. Tomcat validates against its trust-store ;
  3. The custom WebSecurityConfigurerAdapter retrieves a “username” from the certificate’s CN ;
  4. The application authenticates the user associated to the retrieved username ;
  5. At method level, if annotated with @PreAuthorize("hasRole('SSL_USER')"), the application will check whether the user has the required role.