Add https client authentication
Change-Id: Ib04eaa8e534e2ac83fb4c11a169f110a5f3a580d
This commit is contained in:
parent
0323749ff4
commit
79e7530390
7 changed files with 145 additions and 14 deletions
|
@ -24,6 +24,9 @@
|
||||||
<xs:element name="keystorePath" type="xs:string" />
|
<xs:element name="keystorePath" type="xs:string" />
|
||||||
<xs:element name="keystorePassword" type="xs:string" />
|
<xs:element name="keystorePassword" type="xs:string" />
|
||||||
<xs:element name="keyPassword" type="xs:string" />
|
<xs:element name="keyPassword" type="xs:string" />
|
||||||
|
<xs:element name="truststorePath" type="xs:string" />
|
||||||
|
<xs:element name="truststorePassword" type="xs:string" />
|
||||||
|
<xs:element name="requireClientAuth" type="xs:boolean" />
|
||||||
</xs:sequence>
|
</xs:sequence>
|
||||||
</xs:complexType>
|
</xs:complexType>
|
||||||
</xs:schema>
|
</xs:schema>
|
||||||
|
|
|
@ -16,8 +16,10 @@ import pl.touk.mockserver.client.Util
|
||||||
import pl.touk.mockserver.server.HttpMockServer
|
import pl.touk.mockserver.server.HttpMockServer
|
||||||
import spock.lang.Shared
|
import spock.lang.Shared
|
||||||
import spock.lang.Specification
|
import spock.lang.Specification
|
||||||
|
import spock.lang.Unroll
|
||||||
|
|
||||||
import javax.net.ssl.SSLContext
|
import javax.net.ssl.SSLContext
|
||||||
|
import javax.net.ssl.SSLHandshakeException
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
|
|
||||||
class MockServerHttpsTest extends Specification {
|
class MockServerHttpsTest extends Specification {
|
||||||
|
@ -27,14 +29,20 @@ class MockServerHttpsTest extends Specification {
|
||||||
HttpMockServer httpMockServer
|
HttpMockServer httpMockServer
|
||||||
|
|
||||||
@Shared
|
@Shared
|
||||||
SSLContext sslContext = SSLContexts.custom()
|
SSLContext noClientAuthSslContext = SSLContexts.custom()
|
||||||
.loadTrustMaterial(trustStore())
|
.loadTrustMaterial(trustStore())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@Shared
|
@Shared
|
||||||
CloseableHttpClient client = HttpClients.custom()
|
SSLContext trustedCertificateSslContext = SSLContexts.custom()
|
||||||
.setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER)
|
.loadKeyMaterial(trustedCertificateKeystore(), 'changeit'.toCharArray())
|
||||||
.setSslcontext(sslContext)
|
.loadTrustMaterial(trustStore())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
@Shared
|
||||||
|
SSLContext untrustedCertificateSslContext = SSLContexts.custom()
|
||||||
|
.loadKeyMaterial(untrustedCertificateKeystore(), 'changeit'.toCharArray())
|
||||||
|
.loadTrustMaterial(trustStore())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
def setup() {
|
def setup() {
|
||||||
|
@ -64,17 +72,90 @@ class MockServerHttpsTest extends Specification {
|
||||||
when:
|
when:
|
||||||
HttpPost restPost = new HttpPost('https://localhost:10443/testEndpoint')
|
HttpPost restPost = new HttpPost('https://localhost:10443/testEndpoint')
|
||||||
restPost.entity = new StringEntity('<request/>', ContentType.create("text/xml", "UTF-8"))
|
restPost.entity = new StringEntity('<request/>', ContentType.create("text/xml", "UTF-8"))
|
||||||
CloseableHttpResponse response = client.execute(restPost)
|
CloseableHttpResponse response = client(noClientAuthSslContext).execute(restPost)
|
||||||
then:
|
then:
|
||||||
GPathResult restPostResponse = Util.extractXmlResponse(response)
|
GPathResult restPostResponse = Util.extractXmlResponse(response)
|
||||||
restPostResponse.name() == 'goodResponse-request'
|
restPostResponse.name() == 'goodResponse-request'
|
||||||
and:
|
}
|
||||||
remoteMockServer.removeMock('testHttps')?.size() == 1
|
|
||||||
|
def 'should handle HTTPS server with client auth' () {
|
||||||
|
expect:
|
||||||
|
remoteMockServer.addMock(new AddMock(
|
||||||
|
name: 'testHttps',
|
||||||
|
path: 'testEndpoint',
|
||||||
|
port: 10443,
|
||||||
|
predicate: '''{req -> req.xml.name() == 'request'}''',
|
||||||
|
response: '''{req -> "<goodResponse-${req.xml.name()}/>"}''',
|
||||||
|
https: new Https(
|
||||||
|
keyPassword: 'changeit',
|
||||||
|
keystorePassword: 'changeit',
|
||||||
|
keystorePath: MockServerHttpsTest.classLoader.getResource('keystore.jks').path,
|
||||||
|
truststorePath: MockServerHttpsTest.classLoader.getResource('truststore.jks').path,
|
||||||
|
truststorePassword: 'changeit',
|
||||||
|
requireClientAuth: true
|
||||||
|
),
|
||||||
|
soap: false
|
||||||
|
))
|
||||||
|
when:
|
||||||
|
HttpPost restPost = new HttpPost('https://localhost:10443/testEndpoint')
|
||||||
|
restPost.entity = new StringEntity('<request/>', ContentType.create("text/xml", "UTF-8"))
|
||||||
|
CloseableHttpResponse response = client(trustedCertificateSslContext).execute(restPost)
|
||||||
|
then:
|
||||||
|
GPathResult restPostResponse = Util.extractXmlResponse(response)
|
||||||
|
restPostResponse.name() == 'goodResponse-request'
|
||||||
|
}
|
||||||
|
|
||||||
|
@Unroll
|
||||||
|
def 'should handle HTTPS server with wrong client auth' () {
|
||||||
|
expect:
|
||||||
|
remoteMockServer.addMock(new AddMock(
|
||||||
|
name: 'testHttps',
|
||||||
|
path: 'testEndpoint',
|
||||||
|
port: 10443,
|
||||||
|
predicate: '''{req -> req.xml.name() == 'request'}''',
|
||||||
|
response: '''{req -> "<goodResponse-${req.xml.name()}/>"}''',
|
||||||
|
https: new Https(
|
||||||
|
keyPassword: 'changeit',
|
||||||
|
keystorePassword: 'changeit',
|
||||||
|
keystorePath: MockServerHttpsTest.classLoader.getResource('keystore.jks').path,
|
||||||
|
truststorePath: MockServerHttpsTest.classLoader.getResource('truststore.jks').path,
|
||||||
|
truststorePassword: 'changeit',
|
||||||
|
requireClientAuth: true
|
||||||
|
),
|
||||||
|
soap: false
|
||||||
|
))
|
||||||
|
when:
|
||||||
|
HttpPost restPost = new HttpPost('https://localhost:10443/testEndpoint')
|
||||||
|
restPost.entity = new StringEntity('<request/>', ContentType.create("text/xml", "UTF-8"))
|
||||||
|
client(sslContext).execute(restPost)
|
||||||
|
then:
|
||||||
|
thrown(SSLHandshakeException)
|
||||||
|
where:
|
||||||
|
sslContext << [noClientAuthSslContext, untrustedCertificateSslContext]
|
||||||
|
}
|
||||||
|
|
||||||
|
private CloseableHttpClient client(SSLContext sslContext) {
|
||||||
|
return HttpClients.custom()
|
||||||
|
.setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER)
|
||||||
|
.setSslcontext(sslContext)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeyStore trustedCertificateKeystore() {
|
||||||
|
return loadKeystore('trusted.jks')
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeyStore untrustedCertificateKeystore() {
|
||||||
|
return loadKeystore('untrusted.jks')
|
||||||
}
|
}
|
||||||
|
|
||||||
private KeyStore trustStore() {
|
private KeyStore trustStore() {
|
||||||
KeyStore truststore = KeyStore.getInstance(KeyStore.defaultType)
|
return loadKeystore('truststore.jks')
|
||||||
truststore.load(new FileInputStream(MockServerHttpsTest.classLoader.getResource('truststore.jks').path), "changeit".toCharArray());
|
}
|
||||||
|
|
||||||
|
private KeyStore loadKeystore(String fileName) {
|
||||||
|
KeyStore truststore = KeyStore.getInstance(KeyStore.defaultType)
|
||||||
|
truststore.load(new FileInputStream(MockServerHttpsTest.classLoader.getResource(fileName).path), "changeit".toCharArray());
|
||||||
return truststore
|
return truststore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
BIN
mockserver-tests/src/test/resources/trusted.jks
Normal file
BIN
mockserver-tests/src/test/resources/trusted.jks
Normal file
Binary file not shown.
BIN
mockserver-tests/src/test/resources/truststore.jks
Normal file
BIN
mockserver-tests/src/test/resources/truststore.jks
Normal file
Binary file not shown.
BIN
mockserver-tests/src/test/resources/untrusted.jks
Normal file
BIN
mockserver-tests/src/test/resources/untrusted.jks
Normal file
Binary file not shown.
|
@ -2,15 +2,16 @@ package pl.touk.mockserver.server
|
||||||
|
|
||||||
import com.sun.net.httpserver.HttpHandler
|
import com.sun.net.httpserver.HttpHandler
|
||||||
import com.sun.net.httpserver.HttpServer
|
import com.sun.net.httpserver.HttpServer
|
||||||
import com.sun.net.httpserver.HttpsConfigurator
|
|
||||||
import com.sun.net.httpserver.HttpsServer
|
import com.sun.net.httpserver.HttpsServer
|
||||||
import groovy.transform.PackageScope
|
import groovy.transform.PackageScope
|
||||||
import groovy.util.logging.Slf4j
|
import groovy.util.logging.Slf4j
|
||||||
import pl.touk.mockserver.api.common.Https
|
import pl.touk.mockserver.api.common.Https
|
||||||
|
|
||||||
|
import javax.net.ssl.KeyManager
|
||||||
import javax.net.ssl.KeyManagerFactory
|
import javax.net.ssl.KeyManagerFactory
|
||||||
import javax.net.ssl.SSLContext
|
import javax.net.ssl.SSLContext
|
||||||
import javax.net.ssl.TrustManager
|
import javax.net.ssl.TrustManager
|
||||||
|
import javax.net.ssl.TrustManagerFactory
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.concurrent.Executor
|
import java.util.concurrent.Executor
|
||||||
|
@ -36,7 +37,7 @@ class HttpServerWrapper {
|
||||||
private HttpServer buildServer(InetSocketAddress addr, Https https) {
|
private HttpServer buildServer(InetSocketAddress addr, Https https) {
|
||||||
if (https) {
|
if (https) {
|
||||||
HttpsServer httpsServer = HttpsServer.create(addr, 0)
|
HttpsServer httpsServer = HttpsServer.create(addr, 0)
|
||||||
httpsServer.httpsConfigurator = new HttpsConfigurator(buildSslContext(https))
|
httpsServer.httpsConfigurator = new HttpsConfig(buildSslContext(https), https)
|
||||||
return httpsServer
|
return httpsServer
|
||||||
} else {
|
} else {
|
||||||
return HttpServer.create(addr, 0)
|
return HttpServer.create(addr, 0)
|
||||||
|
@ -44,14 +45,32 @@ class HttpServerWrapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
private SSLContext buildSslContext(Https https) {
|
private SSLContext buildSslContext(Https https) {
|
||||||
|
KeyManager[] keyManagers = buildKeyManager(https)
|
||||||
|
TrustManager[] trustManagers = buildTrustManager(https)
|
||||||
|
|
||||||
|
SSLContext ssl = SSLContext.getInstance('TLSv1')
|
||||||
|
ssl.init(keyManagers, trustManagers, new SecureRandom())
|
||||||
|
return ssl
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeyManager[] buildKeyManager(Https https) {
|
||||||
KeyStore keyStore = KeyStore.getInstance(KeyStore.defaultType)
|
KeyStore keyStore = KeyStore.getInstance(KeyStore.defaultType)
|
||||||
keyStore.load(new FileInputStream(https.keystorePath), https.keystorePassword.toCharArray())
|
keyStore.load(new FileInputStream(https.keystorePath), https.keystorePassword.toCharArray())
|
||||||
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.defaultAlgorithm)
|
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.defaultAlgorithm)
|
||||||
kmf.init(keyStore, https.keyPassword.toCharArray())
|
kmf.init(keyStore, https.keyPassword.toCharArray())
|
||||||
|
return kmf.keyManagers
|
||||||
|
}
|
||||||
|
|
||||||
SSLContext ssl = SSLContext.getInstance('TLSv1')
|
private TrustManager[] buildTrustManager(Https https) {
|
||||||
ssl.init(kmf.keyManagers, [] as TrustManager[], new SecureRandom())
|
if (https.requireClientAuth) {
|
||||||
return ssl
|
KeyStore trustStore = KeyStore.getInstance(KeyStore.defaultType)
|
||||||
|
trustStore.load(new FileInputStream(https.truststorePath), https.truststorePassword.toCharArray())
|
||||||
|
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.defaultAlgorithm)
|
||||||
|
tmf.init(trustStore)
|
||||||
|
return tmf.trustManagers
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void createContext(String context, HttpHandler handler) {
|
void createContext(String context, HttpHandler handler) {
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
package pl.touk.mockserver.server
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.HttpsConfigurator
|
||||||
|
import com.sun.net.httpserver.HttpsParameters
|
||||||
|
import groovy.transform.CompileStatic
|
||||||
|
import pl.touk.mockserver.api.common.Https
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLContext
|
||||||
|
import javax.net.ssl.SSLParameters
|
||||||
|
|
||||||
|
@CompileStatic
|
||||||
|
class HttpsConfig extends HttpsConfigurator {
|
||||||
|
private final Https https
|
||||||
|
|
||||||
|
HttpsConfig(SSLContext sslContext, Https https) {
|
||||||
|
super(sslContext)
|
||||||
|
this.https = https
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void configure(HttpsParameters httpsParameters) {
|
||||||
|
SSLContext sslContext = getSSLContext()
|
||||||
|
SSLParameters sslParameters = sslContext.defaultSSLParameters
|
||||||
|
sslParameters.needClientAuth = https.requireClientAuth
|
||||||
|
httpsParameters.needClientAuth = https.requireClientAuth
|
||||||
|
httpsParameters.SSLParameters = sslParameters
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue