Tuesday 16 September 2014

Spring OAuth2RestTemplate and self-signed server certificate

It might happen to you that you ended up using spring-security-oauth2 on OAuth2 client side. Personally I would not recommend to use it as it brings much more complexity into task that is not that difficult. But every use-case is diferent.

If you also happen to integrate with site using self-signed certificate, you'll inevitably encounter following exception.

org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException: Error requesting access token.
 at org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport.retrieveToken(OAuth2AccessTokenSupport.java:144) ~[spring-security-oauth2-2.0.2.RELEASE.jar:na]
 at org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider.obtainAccessToken(AuthorizationCodeAccessTokenProvider.java:198) ~[spring-security-oauth2-2.0.2.RELEASE.jar:na]
 at org.springframework.security.oauth2.client.OAuth2RestTemplate.acquireAccessToken(OAuth2RestTemplate.java:221) ~[spring-security-oauth2-2.0.2.RELEASE.jar:na]
 at org.springframework.security.oauth2.client.OAuth2RestTemplate.getAccessToken(OAuth2RestTemplate.java:173) ~[spring-security-oauth2-2.0.2.RELEASE.jar:na]
 at org.springframework.security.oauth2.client.OAuth2RestTemplate.createRequest(OAuth2RestTemplate.java:105) ~[spring-security-oauth2-2.0.2.RELEASE.jar:na]
 at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:538) ~[spring-web-4.0.5.RELEASE.jar:4.0.5.RELEASE]
 at org.springframework.security.oauth2.client.OAuth2RestTemplate.doExecute(OAuth2RestTemplate.java:128) ~[spring-security-oauth2-2.0.2.RELEASE.jar:na]
 at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:518) ~[spring-web-4.0.5.RELEASE.jar:4.0.5.RELEASE]
 at org.springframework.web.client.RestTemplate.getForObject(RestTemplate.java:256) ~[spring-web-4.0.5.RELEASE.jar:4.0.5.RELEASE]
...
Caused by: org.springframework.web.client.ResourceAccessException: I/O error on POST request for "https://somewhere.something.info/oauth2/token":sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target; nested exception is javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
 at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:558) ~[spring-web-4.0.5.RELEASE.jar:4.0.5.RELEASE]
 at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:511) ~[spring-web-4.0.5.RELEASE.jar:4.0.5.RELEASE]
 at org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport.retrieveToken(OAuth2AccessTokenSupport.java:136) ~[spring-security-oauth2-2.0.2.RELEASE.jar:na]
 ... 86 common frames omitted
Caused by: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
 at sun.security.ssl.Alerts.getSSLException(Alerts.java:192) ~[na:1.7.0_55]
 at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1884) ~[na:1.7.0_55]
 at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:276) ~[na:1.7.0_55]
 at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:270) ~[na:1.7.0_55]
 at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1341) ~[na:1.7.0_55]
 at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:153) ~[na:1.7.0_55]
 at sun.security.ssl.Handshaker.processLoop(Handshaker.java:868) ~[na:1.7.0_55]
 at sun.security.ssl.Handshaker.process_record(Handshaker.java:804) ~[na:1.7.0_55]
 at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1016) ~[na:1.7.0_55]
 at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1312) ~[na:1.7.0_55]
 at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1339) ~[na:1.7.0_55]
 at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1323) ~[na:1.7.0_55]
 at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:563) ~[na:1.7.0_55]
 at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185) ~[na:1.7.0_55]
 at sun.net.www.protocol.https.HttpsURLConnectionImpl.connect(HttpsURLConnectionImpl.java:153) ~[na:1.7.0_55]
 at org.springframework.http.client.SimpleBufferingClientHttpRequest.executeInternal(SimpleBufferingClientHttpRequest.java:78) ~[spring-web-4.0.5.RELEASE.jar:4.0.5.RELEASE]
 at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48) ~[spring-web-4.0.5.RELEASE.jar:4.0.5.RELEASE]
 at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:52) ~[spring-web-4.0.5.RELEASE.jar:4.0.5.RELEASE]
 at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:542) ~[spring-web-4.0.5.RELEASE.jar:4.0.5.RELEASE]
 ... 88 common frames omitted
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
 at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:385) ~[na:1.7.0_55]
 at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:292) ~[na:1.7.0_55]
 at sun.security.validator.Validator.validate(Validator.java:260) ~[na:1.7.0_55]
 at sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:326) ~[na:1.7.0_55]
 at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:231) ~[na:1.7.0_55]
 at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:126) ~[na:1.7.0_55]
 at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1323) ~[na:1.7.0_55]
 ... 102 common frames omitted
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
 at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:196) ~[na:1.7.0_55]
 at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:268) ~[na:1.7.0_55]
 at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:380) ~[na:1.7.0_55]
 ... 108 common frames omitted

Exception is throw when https://somewhere.something.info/oauth2/token is used to trade OAuth2 code for access token. You probably know what to do. Time for stupid trick with (totaly unsecure) X509TrustManager! As OAuth2RestTemplate extends RestTemplate, it inherits public void setRequestFactory(ClientHttpRequestFactory requestFactory) method and it can be used to make the trick.

private static void disableCertificateChecks(OAuth2RestTemplate oauthTemplate) throws Exception {

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, new TrustManager[] { new DumbX509TrustManager() }, null);
        ClientHttpRequestFactory requestFactory = new SSLContextRequestFactory(sslContext);

        //This is for OAuth protected resources
        oauthTemplate.setRequestFactory(requestFactory);
}

Code for SSLContextRequestFactory and DumbX509TrustManager is gisted here

Now, if you try to run through test once again, you will still get same SunCertPathBuilderException again. Why?

Answer is in hidden in bowels of OAuth2AccessTokenSupport, which is base class of AuthorizationCodeAccessTokenProvider. To cut the story short, it is creating it's own RestTemplates for token endpoint operations.

Luckily again, overridable setRequestFactory(...) is also provided on OAuth2AccessTokenSupport. Repeating same trick again finaly gives us working, exception free solution:

    private static void disableCertificateChecks(OAuth2RestTemplate oauthTemplate) throws Exception {

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, new TrustManager[] { new NastyX509TrustManager() }, null);
        ClientHttpRequestFactory requestFactory = new SSLContextRequestFactory(sslContext);

        //This is for OAuth protected resources
        oauthTemplate.setRequestFactory(requestFactory);

        //AuthorizationCodeAccessTokenProvider creates it's own RestTemplate for token operations
        AuthorizationCodeAccessTokenProvider provider = new AuthorizationCodeAccessTokenProvider();
        provider.setRequestFactory(requestFactory);
        oauthTemplate.setAccessTokenProvider(provider);
    }

Remember, you've just created huge security hole in your application. Make doubly sure it is never used in production

Happy unsecure https REST OAuth2 calls!

PS: if you favour more advanced http transport layer then basic Java HttpURLConnection (and you should on server side) like HttpComponents 4.3, then simplest possible ClientHttpRequestFactory creation would be:

        SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustSelfSignedStrategy()).build();
        CloseableHttpClient httpClient = HttpClients.custom().setSslcontext(sslContext).build();
        ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);