Search code examples
spring-bootspring-saml

SSL peer failed hostname validation in Spring SAML


I'm trying to configure my Spring Boot project to use SAML authentication against a third party IDP. I've already achieved to make the configuration from vdenotaris work with the SSOCircle provider and now I want to switch it to the other provider.

The SAML metadata endpoint is HTTPS enabled and I've already created a keystore with both the certificate given in metadata (which is used for signing and encryption) and the one provided by the HTTP endpoint (based in this answer). Then, I save them in a cert file ($CERTIFICATE_FILE) and I use this script to generate my keystore:

keytool -delete -alias third-party -keystore $KEYSTORE_FILE -storepass $KEYSTORE_PASSWORD
keytool -import -alias third-party -file $CERTIFICATE_FILE -keystore $KEYSTORE_FILE -storepass $KEYSTORE_PASSWORD -noprompt
keytool -genkeypair -alias mycompany -keypass mycompanypass -keystore $KEYSTORE_FILE

Then, when using this keystore to retrieve the SAML metadata, I get this error:

org.opensaml.saml2.metadata.provider.MetadataProviderException: org.opensaml.saml2.metadata.provider.MetadataProviderException: Error retrieving metadata from https://third.party.provider/metadata
    at org.opensaml.saml2.metadata.provider.HTTPMetadataProvider.fetchMetadata(HTTPMetadataProvider.java:274)
    at org.opensaml.saml2.metadata.provider.AbstractReloadingMetadataProvider.refresh(AbstractReloadingMetadataProvider.java:267)
    at org.opensaml.saml2.metadata.provider.AbstractReloadingMetadataProvider.doInitialization(AbstractReloadingMetadataProvider.java:236)
    at org.opensaml.saml2.metadata.provider.AbstractMetadataProvider.initialize(AbstractMetadataProvider.java:407)
    at org.springframework.security.saml.metadata.ExtendedMetadataDelegate.initialize(ExtendedMetadataDelegate.java:167)
    at org.springframework.security.saml.metadata.MetadataManager.initializeProvider(MetadataManager.java:412)
    at org.springframework.security.saml.metadata.MetadataManager.refreshMetadata(MetadataManager.java:238)
    at org.springframework.security.saml.metadata.CachingMetadataManager.refreshMetadata(CachingMetadataManager.java:86)
    at org.springframework.security.saml.metadata.MetadataManager$RefreshTask.run(MetadataManager.java:1040)
    at java.util.TimerThread.mainLoop(Timer.java:555)
    at java.util.TimerThread.run(Timer.java:505)
Caused by: org.opensaml.saml2.metadata.provider.MetadataProviderException: Error retrieving metadata from https://third.party.provider/metadata
    at org.opensaml.saml2.metadata.provider.HTTPMetadataProvider.fetchMetadata(HTTPMetadataProvider.java:274)
    at org.opensaml.saml2.metadata.provider.AbstractReloadingMetadataProvider.refresh(AbstractReloadingMetadataProvider.java:255)
    ... 9 common frames omitted
Caused by: javax.net.ssl.SSLPeerUnverifiedException: SSL peer failed hostname validation for name: null
    at org.opensaml.ws.soap.client.http.TLSProtocolSocketFactory.verifyHostname(TLSProtocolSocketFactory.java:233)
    at org.opensaml.ws.soap.client.http.TLSProtocolSocketFactory.createSocket(TLSProtocolSocketFactory.java:186)
    at org.springframework.security.saml.trust.httpclient.TLSProtocolSocketFactory.createSocket(TLSProtocolSocketFactory.java:97)
    at org.apache.commons.httpclient.HttpConnection.open(HttpConnection.java:707)
    at org.apache.commons.httpclient.MultiThreadedHttpConnectionManager$HttpConnectionAdapter.open(MultiThreadedHttpConnectionManager.java:1361)
    at org.apache.commons.httpclient.HttpMethodDirector.executeWithRetry(HttpMethodDirector.java:387)
    at org.apache.commons.httpclient.HttpMethodDirector.executeMethod(HttpMethodDirector.java:171)
    at org.apache.commons.httpclient.HttpClient.executeMethod(HttpClient.java:397)
    at org.apache.commons.httpclient.HttpClient.executeMethod(HttpClient.java:323)
    at org.opensaml.saml2.metadata.provider.HTTPMetadataProvider.fetchMetadata(HTTPMetadataProvider.java:250)
    ... 10 common frames omitted

These ones are the relevant parts of my configuration based in the linked project by vdenotaris:

// Setup TLS Socket Factory
@Bean
public TLSProtocolConfigurer tlsProtocolConfigurer() {
    return new TLSProtocolConfigurer();
}

@Bean
public ProtocolSocketFactory socketFactory() {
    return new TLSProtocolSocketFactory(keyManager(), null, "allowAll");
}

@Bean
public Protocol socketFactoryProtocol() {
    return new Protocol("https", socketFactory(), 443);
}

@Bean
public MethodInvokingFactoryBean socketFactoryInitialization() {
    MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean();
    methodInvokingFactoryBean.setTargetClass(Protocol.class);
    methodInvokingFactoryBean.setTargetMethod("registerProtocol");
    Object[] args = { "https", socketFactoryProtocol() };
    methodInvokingFactoryBean.setArguments(args);
    return methodInvokingFactoryBean;
}

// Central storage of cryptographic keys
@Bean
public KeyManager keyManager() {
    DefaultResourceLoader loader = new DefaultResourceLoader();
    Resource storeFile = loader.getResource("classpath:/saml/mySamlKeystore.jks");
    String storePass = "storepass";
    Map<String, String> passwords = new HashMap<String, String>();
    passwords.put("mycompany", "mycompanypass");
    String defaultKey = "mycompany";
    return new JKSKeyManager(storeFile, storePass, passwords, defaultKey);
}

However, here I found some misconceptions. As far as I know, the TLSProtocolConfigurer creates a TLSProtocolSocketFactory itself, why is the sample project creating both beans then? According to the docs using TLSProtocolConfigurer should be enough, but how to create socketFactoryProtocol()?

I would be grateful to have some light in here.


Solution

  • I wasn't providing the password for the imported cert file:

    @Bean
    public KeyManager keyManager() {
        DefaultResourceLoader loader = new DefaultResourceLoader();
        Resource storeFile = loader.getResource("classpath:/saml/mySamlKeystore.jks");
        String storePass = "storepass";
        Map<String, String> passwords = new HashMap<String, String>();
        passwords.put("mycompany", "mycompanypass");
        passwords.put("third-party", "mycompanypass");
        String defaultKey = "mycompany";
        return new JKSKeyManager(storeFile, storePass, passwords, defaultKey);
    }
    

    With this, the application can read the certs from the keystore and trust them, without the need of installing them in the JDK cacerts.