Make Feign client to take truststore from custom property
Asked Answered
C

3

3

Feign client in our app is communicating with a self-signed server. We are able to make Feign client use the custom truststore using the property javax.net.ssl.trustStore system property. But because my app also communicates with standard CA certified sites, the default truststore shouldn't be overridden.

How can I use the custom truststore without using javax.net.ssl.trustStore system property? Or else how can I have my Feign client use the truststore from a property other than standard javax.net.ssl.trustStore system property?

Corticosterone answered 13/4, 2018 at 6:39 Comment(0)
C
3

I ended up handcrafting my own instance of SSLSocketFactory that I pass to my Feign client, using the below code,

/**
 * Gets the {@link SSLSocketFactory} instance for the client communication
 * using the given truststore file and password.
 * 
 * Since the instance is used as client, this is instantiated with empty
 * keystore and the truststore represented by the given truststore file.
 * 
 * 
 * @param theTrustStoreFile
 *            The complete file path of the truststore.
 * @return {@link SSLSocketFactory} instance that internally uses the given
 *         truststore.
 * @throws Exception
 *             When there is an error in the creating the
 *             {@link SSLSocketFactory} instance.
 */
public static SSLSocketFactory getClientSSLSocketFactory(File theTrustStoreFile)
        throws Exception
{
    // This supports TLSv1.2
    SSLContext sslContext = SSLContext.getInstance("TLS");

    KeyStore kStore = KeyStore.getInstance(KeyStore.getDefaultType());

    FileInputStream file = getFileInputStream(theTrustStoreFile);
    kStore.load(file, null);

    TrustManagerFactory tmf = TrustManagerFactory
            .getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init(kStore);

    sslContext.init(new KeyManager[] {}, tmf.getTrustManagers(), null);

    return sslContext.getSocketFactory();
}

/**
 * Reads the file into {@link FileInputStream} instance.
 * 
 * @param file
 *            The file to be read.
 * @return {@link FileInputStream} that represents the file content/
 * @throws Exception
 *             When there is any error in reading the file.
 */
private static FileInputStream getFileInputStream(final File file) throws Exception
{
    return AccessController.doPrivileged(new PrivilegedExceptionAction<FileInputStream>()
    {
        @Override
        public FileInputStream run() throws Exception
        {
            try
            {
                if (file.exists())
                {
                    return new FileInputStream(file);
                } else
                {
                    return null;
                }
            } catch (FileNotFoundException e)
            {
                // couldn't find it, oh well.
                return null;
            }
        }
    });
}

And when I instantiate my client, I do it like,

Feign.builder().client(getClientSSLSocketFactory(trustFile),null)...

This gist contains sample code and its usage.

Corticosterone answered 16/4, 2018 at 9:5 Comment(0)
W
4

This is how i used FeignClient with keystore and truststore

FeignClient Configuration

@Configuration
public class TestClientConfig {

    @Bean
    public Client feignClient() {
        Client trustSSLSockets = new Client.Default(getSSLSocketFactory(), new NoopHostnameVerifier());
        return trustSSLSockets;
    }

    private SSLSocketFactory getSSLSocketFactory() {
        try {
            TrustStrategy acceptingTrustStrategy = new TrustStrategy() {
                @Override
                public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                    //Do your validations
                    return true;
                }
            };
            String allPassword = "123456";
            SSLContext sslContext = SSLContextBuilder
                    .create()
                    // .loadKeyMaterial(ResourceUtils.getFile("classpath:keystore.p12"), allPassword.toCharArray(), allPassword.toCharArray())
                    .loadKeyMaterial(ResourceUtils.getFile("classpath:keystore.jks"), allPassword.toCharArray(), allPassword.toCharArray())
                    .loadTrustMaterial(ResourceUtils.getFile("classpath:truststore.jks"), allPassword.toCharArray())
                    .build();
            return sslContext.getSocketFactory();
        } catch (Exception exception) {
            throw new RuntimeException(exception);
        }
    }
}

Interface

@FeignClient(name = "Test", url = "https://localhost:8443",configuration=TestClientConfig.class)
public interface TestClient {

    @RequestMapping(method = RequestMethod.GET,value = "/hello")
    String getHello();
}
Wroth answered 5/12, 2019 at 13:6 Comment(1)
You are not doing anything with the acceptingTrustStrategy you created. Did you intend to pass it to the sslContext?Ungley
C
3

I ended up handcrafting my own instance of SSLSocketFactory that I pass to my Feign client, using the below code,

/**
 * Gets the {@link SSLSocketFactory} instance for the client communication
 * using the given truststore file and password.
 * 
 * Since the instance is used as client, this is instantiated with empty
 * keystore and the truststore represented by the given truststore file.
 * 
 * 
 * @param theTrustStoreFile
 *            The complete file path of the truststore.
 * @return {@link SSLSocketFactory} instance that internally uses the given
 *         truststore.
 * @throws Exception
 *             When there is an error in the creating the
 *             {@link SSLSocketFactory} instance.
 */
public static SSLSocketFactory getClientSSLSocketFactory(File theTrustStoreFile)
        throws Exception
{
    // This supports TLSv1.2
    SSLContext sslContext = SSLContext.getInstance("TLS");

    KeyStore kStore = KeyStore.getInstance(KeyStore.getDefaultType());

    FileInputStream file = getFileInputStream(theTrustStoreFile);
    kStore.load(file, null);

    TrustManagerFactory tmf = TrustManagerFactory
            .getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init(kStore);

    sslContext.init(new KeyManager[] {}, tmf.getTrustManagers(), null);

    return sslContext.getSocketFactory();
}

/**
 * Reads the file into {@link FileInputStream} instance.
 * 
 * @param file
 *            The file to be read.
 * @return {@link FileInputStream} that represents the file content/
 * @throws Exception
 *             When there is any error in reading the file.
 */
private static FileInputStream getFileInputStream(final File file) throws Exception
{
    return AccessController.doPrivileged(new PrivilegedExceptionAction<FileInputStream>()
    {
        @Override
        public FileInputStream run() throws Exception
        {
            try
            {
                if (file.exists())
                {
                    return new FileInputStream(file);
                } else
                {
                    return null;
                }
            } catch (FileNotFoundException e)
            {
                // couldn't find it, oh well.
                return null;
            }
        }
    });
}

And when I instantiate my client, I do it like,

Feign.builder().client(getClientSSLSocketFactory(trustFile),null)...

This gist contains sample code and its usage.

Corticosterone answered 16/4, 2018 at 9:5 Comment(0)
S
3

Thanks to the combination of Kannan's and Niraj's answer I managed to configure my feign client to use a customSSLSocketFactory.

My use case: I have an API that requires SSL cert to be added to my local machine on JAVA cacerts in order to fetched it. However, I am unable to add this cert after deployment at the cloud server. Hence, I needed to configure my Feign client to take in a custom trust store from my application itself.

Here's my version of it

  1. Create your own truststore with the certificate added keytool -import -file <filepath-of-certificate> -alias <certificate-alias-name> -keystore <truststore-alias-name> system will prompt for a password, set your own password and remember it Truststore doesn't need any extensions for this case

  2. Copy this truststore file to resource folder of your spring boot app

  3. In application.properties or application.yml file set the classpath and password for your truststore. I'm using application.yml so the example as follows

e.g.

example-service:
  server: https://example.api.com
  trustStore: classpath:<Your truststore name>
  trustStorePassword: yourpassword

You can also skip step 3 and directly pass in the truststore and password below

  1. In CustomFeignConfig class you can directly pass in the truststore as a resource and directly pass in the truststore inputstream to sslsocketfactory. Note that the Bean notation is essential to override the default feign config. I only require a truststore for my case, so I pass in a empty new KeyManager[]{} on sslContext.init()
public class CustomFeignConfig {

    @Bean
    public Client feignClient(@Value("${example-service.trustStore}") Resource trustStoreResource,
                              @Value("${example-service.trustStorePassword}") String trustStorePassword) throws Exception {
        try {
            return new Client.Default(
                    sslSocketFactory(trustStoreResource.getInputStream(), trustStorePassword),
                    null);
        } catch (Exception e) {
            throw new Exception("Error in initializing feign client", e);
        }
    }

    private static SSLSocketFactory sslSocketFactory(InputStream trustStoreStream, String trustStorePassword)
            throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException, KeyManagementException {
        SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
        TrustManagerFactory tmf = createTrustManager(trustStoreStream, trustStorePassword);
        sslContext.init(new KeyManager[]{}, tmf.getTrustManagers(), null);
        return sslContext.getSocketFactory();
    }

    private static TrustManagerFactory createTrustManager(InputStream trustStoreStream, String trustStorePassword)
            throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
        trustStore.load(trustStoreStream, trustStorePassword.toCharArray());
        TrustManagerFactory tmf = TrustManagerFactory
                .getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(trustStore);
        return tmf;
    }
}    
  1. Apply this custom feign config only to the api that requires it
@FeignClient(name = "example-service", url = "${example-service.server}", configuration = CustomFeignConfig.class)
public interface MyFeignClient {}
Sitar answered 14/6, 2021 at 3:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.