From 7523d935a5e168ae0413f181ce76a1fba653514f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tommy=20Tr=C3=B8en?= Date: Thu, 27 May 2021 22:55:03 +0200 Subject: [PATCH 01/11] feature: add https support for NettyWrapper * enabled by providing Tls constructor param with external keystore --- .../mock/oauth2/http/OAuth2HttpServer.kt | 16 +++++++-- .../no/nav/security/mock/oauth2/http/Ssl.kt | 34 ++++++++++++++++++ .../oauth2/server/OAuth2HttpServerTest.kt | 26 ++++++++++++-- src/test/resources/localhost.p12 | Bin 0 -> 2349 bytes 4 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/no/nav/security/mock/oauth2/http/Ssl.kt create mode 100644 src/test/resources/localhost.p12 diff --git a/src/main/kotlin/no/nav/security/mock/oauth2/http/OAuth2HttpServer.kt b/src/main/kotlin/no/nav/security/mock/oauth2/http/OAuth2HttpServer.kt index f611032b..db86bf96 100644 --- a/src/main/kotlin/no/nav/security/mock/oauth2/http/OAuth2HttpServer.kt +++ b/src/main/kotlin/no/nav/security/mock/oauth2/http/OAuth2HttpServer.kt @@ -94,7 +94,9 @@ class MockWebServerWrapper : OAuth2HttpServer { } } -class NettyWrapper : OAuth2HttpServer { +class NettyWrapper @JvmOverloads constructor( + private val ssl: Ssl? = null +) : OAuth2HttpServer { private val masterGroup = NioEventLoopGroup() private val workerGroup = NioEventLoopGroup() private var closeFuture: ChannelFuture? = null @@ -109,6 +111,9 @@ class NettyWrapper : OAuth2HttpServer { .childHandler( object : ChannelInitializer() { public override fun initChannel(ch: SocketChannel) { + if (ssl != null) { + ch.pipeline().addFirst("ssl", ssl.nettySslHandler()) + } ch.pipeline().addLast("codec", HttpServerCodec()) ch.pipeline().addLast("keepAlive", HttpServerKeepAliveHandler()) ch.pipeline().addLast("aggregator", HttpObjectAggregator(Int.MAX_VALUE)) @@ -136,14 +141,21 @@ class NettyWrapper : OAuth2HttpServer { override fun port(): Int = if (port > 0) port else address.port override fun url(path: String): HttpUrl { + val scheme = if (ssl != null) { + "https" + } else { + "http" + } return HttpUrl.Builder() - .scheme("http") + .scheme(scheme) .host(address.address.canonicalHostName) .port(port()) .build() .resolve(path)!! } + private fun Ssl.nettySslHandler(): SslHandler = SslHandler(sslEngine()) + internal class RouterChannelHandler(val requestHandler: RequestHandler) : SimpleChannelInboundHandler() { override fun channelRead0(ctx: ChannelHandlerContext, request: FullHttpRequest) { diff --git a/src/main/kotlin/no/nav/security/mock/oauth2/http/Ssl.kt b/src/main/kotlin/no/nav/security/mock/oauth2/http/Ssl.kt new file mode 100644 index 00000000..59c8935a --- /dev/null +++ b/src/main/kotlin/no/nav/security/mock/oauth2/http/Ssl.kt @@ -0,0 +1,34 @@ +package no.nav.security.mock.oauth2.http + +import java.io.File +import java.security.KeyStore +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLEngine + +class Ssl @JvmOverloads constructor( + private val keystore: String, + private val keystorePassword: String = "", + private val keystoreType: String = "PKCS12", +) { + fun sslEngine(): SSLEngine = sslContext().createSSLEngine().apply { + useClientMode = false + needClientAuth = false + } + + internal fun sslContext(): SSLContext { + val keyStore = keyStore() + val keyManager = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()).apply { + init(keyStore, keystorePassword.toCharArray()) + } + return SSLContext.getInstance("TLS").apply { + init(keyManager.keyManagers, null, null) + } + } + + internal fun keyStore() = KeyStore.getInstance(keystoreType).apply { + File(keystore).inputStream().use { + load(it, keystorePassword.toCharArray()) + } + } +} diff --git a/src/test/kotlin/no/nav/security/mock/oauth2/server/OAuth2HttpServerTest.kt b/src/test/kotlin/no/nav/security/mock/oauth2/server/OAuth2HttpServerTest.kt index 1b18cd18..76bdde44 100644 --- a/src/test/kotlin/no/nav/security/mock/oauth2/server/OAuth2HttpServerTest.kt +++ b/src/test/kotlin/no/nav/security/mock/oauth2/server/OAuth2HttpServerTest.kt @@ -2,24 +2,40 @@ package no.nav.security.mock.oauth2.server import io.kotest.matchers.shouldBe import mu.KotlinLogging +import no.nav.security.mock.oauth2.http.Ssl import no.nav.security.mock.oauth2.http.MockWebServerWrapper import no.nav.security.mock.oauth2.http.NettyWrapper import no.nav.security.mock.oauth2.http.OAuth2HttpResponse import no.nav.security.mock.oauth2.http.OAuth2HttpServer import no.nav.security.mock.oauth2.http.RequestHandler import no.nav.security.mock.oauth2.http.redirect -import no.nav.security.mock.oauth2.testutils.client import no.nav.security.mock.oauth2.testutils.get import no.nav.security.mock.oauth2.testutils.post import okhttp3.Headers import okhttp3.OkHttpClient import org.junit.jupiter.api.Test +import java.security.KeyStore +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509TrustManager private val log = KotlinLogging.logger { } internal class OAuth2HttpServerTest { - val client: OkHttpClient = client() + private val httpsConfig = Ssl( + keystore = "src/test/resources/localhost.p12", + keystorePassword = "", + keystoreType = "PKCS12" + ) + + val client: OkHttpClient = OkHttpClient().newBuilder().apply { + followRedirects(false) + val keyStore: KeyStore = httpsConfig.keyStore() + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply { init(keyStore) } + val sslContext = SSLContext.getInstance("TLS").apply { init(null, trustManagerFactory.trustManagers, null) } + sslSocketFactory(sslContext.socketFactory, trustManagerFactory.trustManagers[0] as X509TrustManager) + }.build() val requestHandler: RequestHandler = { log.debug("received request on url=${it.url}") @@ -42,6 +58,12 @@ internal class OAuth2HttpServerTest { NettyWrapper().start(port = 1234, requestHandler).shouldServeRequests().stop() } + @Test + fun `Netty server should start and serve requests with HTTPS enabled`() { + NettyWrapper(httpsConfig).start(requestHandler).shouldServeRequests().stop() + NettyWrapper(httpsConfig).start(port = 1234, requestHandler).shouldServeRequests().stop() + } + @Test fun `MockWebServer should start and serve requests`() { MockWebServerWrapper().start(requestHandler).shouldServeRequests().stop() diff --git a/src/test/resources/localhost.p12 b/src/test/resources/localhost.p12 new file mode 100644 index 0000000000000000000000000000000000000000..ec2bd543173977b6788c6c03636b840a3fd2033a GIT binary patch literal 2349 zcmV+|3DWj3f(a=C0Ru3C2=4|7Duzgg_YDCD0ic2i-~@sQ+%SR&*f4?vj|K@UhDe6@ z4FLxRpn?N{FoFYo0s#Opf&+C12`Yw2hW8Bt2LUh~1_~;MNQUG*6-$!c2l79AKUe;8pM<_6iVXHBommH~kE^V1 zoK=Rj7_d^tOgmM;oz&3yIouonqzg`s4M9xuFH4m&m7)GWF1iISZ4NwpFrQWKu~HA@xa*D1gfi;A|9Phcikvu z*dMT1j2h3=(Wcy3Ff>B;FKDmtk%*-QY%|B88`OT;=3!V?UX~ALw3}p=%tKxxuZO)9^E-H1F;o=txNS5V$ z46JC=l#1(9iFj%fk39Ou)9MP-8q`GlR-aLza}xEg>WlKto_WlE_EH}LV{s$Wf9xEI z3H1&r&}p}{<5lGc!N@Ce{=vA>|4`9Nu+VfAi+`{;hm7$#4$#bFm>+Z^>@xA=MM&iC zL4&Id2WAl@Y4dpQ=Cv}(>BeXQ2O3rJA5&4>mYW&IrEo%f;F@N z(B0}YoGdq$NAtv!yhS-uwnAbtHRjA3mi+Lv)01oBwF`$NSE3{WFa)XqcZ-dRsoL=F zv}1H^zgfI@S#BnTD_CQ!;7x!dE2K)^?BP z27=kV+Kw=%+S5L_&M90VHVepL8# zMF=JJQnQ2sx|C9!FHWNR?H8X%yv?Y2g;jeH{iH#jc>js!#5^XJ9$>4clgoPd*wUWI zbZv9SH}%KXgyP}oSt`b{YxHW&aR6bU24cHeyWQ0K@%y4j%SA&JP+ZOai{WTq>`?Ac zl#39rRMoRlBmDI0P-wvLyF05$FoFd^1_>&LNQUjVwa& zGvLsov=M7cYlGBML|_lt7&1F5rVq~iOR|vhZ47ALd4$w;(Z1=0m-ea=xR-yAr5P`D zs%Mn)T#;|l9=|7O61k8P+x2F6fZ*99$u+#K0VbgJ7{1w6gD|HQJ8n3#Bu{I9Lp9y-EEM1ZC(j`+z)Ia zYFooiVlha#fMcb>{Xj#SqBO6%r)*JmCLJ^IYP>H1W+0up;_8K%Oi1e z9d`E2#>v=y*CLoAWOE=4k?tfa8;>czlpxh}jSzyyl|~n(V6zIYc?)0NluUu*Ea=!2 z#Az6QkaUyDE31crtz%W<(=(=d-i0uF(Ei?XY057p<2o?gAS2ajP&oNTmw(TIlq$KS z;~O4QiH~+gsgsdnJHHC1(&>GAsx^*cNvx-sg>Hm0rxnIDcO#({L{kk^_|VY%3W zq3S`;Dmpx<*;nppqEHVmtduLIr({@h-?A@;SMiTD7sEkwd z%>@m;hb}vnsVkU#4rv${uzg3rIbt^wl;JYB!W|C6kb!QmwrRYnk)37z4be-AH6Qvbzs@KiyOCVdqnL6*7NSzm=)t~>Uigk=_ z%JbDXP#eNf0GLOUutjOHW??`BP^vbbf$!VJ?>ab+u6}dQr%UQ{uH@>cE>X z%chcP=@D_Y%Y;zvF!sH)9!;Aq!qLuR7Xt9=)n32Si@E?ECmJ^H1xLUwODC|ZR$prB zFRDiAz;(tV!9Gw=b1~!Ti3|$+Z5>A)b~63+FdhgU^7vI$;f-^w76`K5Zq-82wVkNF z$SKT4%mss@LDhv#ISLO-0 zGXPZ9!nuWvSk{|uFqBc>vj8W1VuNL?acTzh-rD2VSj_%K7DPt=rDe|ApjaMFC?y*m zm@@q={tmdJ*g4mOqr0A^GR2R1TYlMw^2=ncSh)DV{-n|%Sn~ZDF<)Dk{NyZt*XD6Z5IY{j@bHIZ z3nSX}TOi{9{{X?;BD%y@>pM3LMvi!!*?+Yp*flXFFe3&DDuzgg_YDCF6)_eB6wn<1 z3Dc%%y%kA2++7RdvlAS6nJ_UhAutIB1uG5%0vZJX1Qd^{_xV5k40>DZ2{eIez%loO T97_ZUC1hKbcJl|c0s;sCPK#s` literal 0 HcmV?d00001 From 1fab8734f9210bf3f81c18c0513d0695073450eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tommy=20Tr=C3=B8en?= Date: Thu, 27 May 2021 22:55:03 +0200 Subject: [PATCH 02/11] feature: add https support for NettyWrapper * enabled by providing Tls constructor param with external keystore --- build.gradle.kts | 5 ++ .../mock/oauth2/testutils/Certificates.kt | 86 +++++++++++++++++++ src/test/resources/createCert.sh | 5 ++ 3 files changed, 96 insertions(+) create mode 100644 src/test/kotlin/no/nav/security/mock/oauth2/testutils/Certificates.kt create mode 100755 src/test/resources/createCert.sh diff --git a/build.gradle.kts b/build.gradle.kts index 93b7727e..b3babb19 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,9 @@ import java.time.Duration import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask + + + val assertjVersion = "3.19.0" val kotlinLoggingVersion = "2.0.6" val logbackVersion = "1.2.3" @@ -12,6 +15,7 @@ val junitJupiterVersion = "5.7.2" val kotlinVersion = "1.5.10" val freemarkerVersion = "2.3.31" val kotestVersion = "4.6.0" +val bouncyCastleVersion = "1.68" val mavenRepoBaseUrl = "https://oss.sonatype.org" val mainClassKt = "no.nav.security.mock.oauth2.StandaloneMockOAuth2ServerKt" @@ -61,6 +65,7 @@ dependencies { implementation("io.github.microutils:kotlin-logging:$kotlinLoggingVersion") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") implementation("org.freemarker:freemarker:$freemarkerVersion") + implementation("org.bouncycastle:bcpkix-jdk15on:$bouncyCastleVersion") testImplementation("org.assertj:assertj-core:$assertjVersion") testImplementation("org.junit.jupiter:junit-jupiter-api:$junitJupiterVersion") testImplementation("org.junit.jupiter:junit-jupiter-params:$junitJupiterVersion") diff --git a/src/test/kotlin/no/nav/security/mock/oauth2/testutils/Certificates.kt b/src/test/kotlin/no/nav/security/mock/oauth2/testutils/Certificates.kt new file mode 100644 index 00000000..29e6e6b0 --- /dev/null +++ b/src/test/kotlin/no/nav/security/mock/oauth2/testutils/Certificates.kt @@ -0,0 +1,86 @@ +package no.nav.security.mock.oauth2.testutils + +import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509.AlgorithmIdentifier +import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier +import org.bouncycastle.asn1.x509.BasicConstraints +import org.bouncycastle.asn1.x509.ExtendedKeyUsage +import org.bouncycastle.asn1.x509.Extension +import org.bouncycastle.asn1.x509.GeneralName +import org.bouncycastle.asn1.x509.GeneralNames +import org.bouncycastle.asn1.x509.KeyPurposeId +import org.bouncycastle.asn1.x509.KeyUsage +import org.bouncycastle.asn1.x509.SubjectKeyIdentifier +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import org.bouncycastle.cert.X509ExtensionUtils +import org.bouncycastle.cert.X509v3CertificateBuilder +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.openssl.jcajce.JcaPEMWriter +import org.bouncycastle.operator.ContentSigner +import org.bouncycastle.operator.bc.BcDigestCalculatorProvider +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import java.io.IOException +import java.io.StringWriter +import java.math.BigInteger +import java.security.KeyPair +import java.security.KeyPairGenerator +import java.security.PublicKey +import java.security.cert.X509Certificate +import java.time.Duration +import java.time.Instant +import java.util.Date + +class Certificates { + + fun KeyPair.toX509v3Certificate(cn: String, expiry: Duration = Duration.ofDays(365)): X509Certificate { + val now = Instant.now() + val x500Name = X500Name("CN=$cn") + val contentSigner: ContentSigner = JcaContentSignerBuilder("SHA256withRSA").build(this.private) + val certificateHolder = JcaX509v3CertificateBuilder( + x500Name, + BigInteger.valueOf(now.toEpochMilli()), + Date.from(now), + Date.from(now.plus(expiry)), + x500Name, + this.public + ).addExtensions(cn, this.public).build(contentSigner) + return JcaX509CertificateConverter().setProvider(BouncyCastleProvider()).getCertificate(certificateHolder) + } + + private fun X509v3CertificateBuilder.addExtensions(cn: String, publicKey: PublicKey) = apply { + addExtension(Extension.subjectKeyIdentifier, false, publicKey.createSubjectKeyId()) + .addExtension(Extension.authorityKeyIdentifier, false, publicKey.createAuthorityKeyId()) + .addExtension(Extension.basicConstraints, true, BasicConstraints(true)) + .addExtension(Extension.subjectAlternativeName, false, GeneralNames(GeneralName(GeneralName.dNSName, cn))) + .addExtension(Extension.keyUsage, false, KeyUsage(KeyUsage.digitalSignature)) + .addExtension(Extension.extendedKeyUsage, false, ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth)) + } + + fun main() { + val keyPair = KeyPairGenerator.getInstance("RSA") + .apply { initialize(2048) }.generateKeyPair() + + println(x509CertificateToPem(keyPair.toX509v3Certificate("yolo"))) + } + + @Throws(IOException::class) + fun x509CertificateToPem(cert: X509Certificate?): String? { + val writer = StringWriter() + val pemWriter = JcaPEMWriter(writer) + pemWriter.writeObject(cert) + pemWriter.flush() + pemWriter.close() + return writer.toString() + } + + private fun PublicKey.createSubjectKeyId(): SubjectKeyIdentifier = + X509ExtensionUtils(digestCalculator()).createSubjectKeyIdentifier(SubjectPublicKeyInfo.getInstance(encoded)) + + private fun PublicKey.createAuthorityKeyId(): AuthorityKeyIdentifier = + X509ExtensionUtils(digestCalculator()).createAuthorityKeyIdentifier(SubjectPublicKeyInfo.getInstance(encoded)) + + private fun digestCalculator() = BcDigestCalculatorProvider().get(AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1)) +} diff --git a/src/test/resources/createCert.sh b/src/test/resources/createCert.sh new file mode 100755 index 00000000..ca76f9bf --- /dev/null +++ b/src/test/resources/createCert.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +openssl req -x509 -out localhost.crt -keyout localhost.key \ + -newkey rsa:2048 -nodes -sha256 -days 3650\ + -subj '/CN=localhost' -extensions EXT -config <( printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth") From 0b652a759851e8fb0c8aced151a9dac7931099d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tommy=20Tr=C3=B8en?= Date: Fri, 4 Jun 2021 19:45:21 +0200 Subject: [PATCH 03/11] feat: generate cert and keystore if not provided in Ssl constructor * introduce BouncyCastle for cert generation --- .../no/nav/security/mock/oauth2/http/Ssl.kt | 112 ++++++++++++++++-- .../oauth2/server/OAuth2HttpServerTest.kt | 54 ++++++--- .../mock/oauth2/testutils/Certificates.kt | 86 -------------- 3 files changed, 139 insertions(+), 113 deletions(-) delete mode 100644 src/test/kotlin/no/nav/security/mock/oauth2/testutils/Certificates.kt diff --git a/src/main/kotlin/no/nav/security/mock/oauth2/http/Ssl.kt b/src/main/kotlin/no/nav/security/mock/oauth2/http/Ssl.kt index 59c8935a..d9d77336 100644 --- a/src/main/kotlin/no/nav/security/mock/oauth2/http/Ssl.kt +++ b/src/main/kotlin/no/nav/security/mock/oauth2/http/Ssl.kt @@ -1,34 +1,128 @@ package no.nav.security.mock.oauth2.http +import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509.AlgorithmIdentifier +import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier +import org.bouncycastle.asn1.x509.BasicConstraints +import org.bouncycastle.asn1.x509.ExtendedKeyUsage +import org.bouncycastle.asn1.x509.Extension +import org.bouncycastle.asn1.x509.GeneralName +import org.bouncycastle.asn1.x509.GeneralNames +import org.bouncycastle.asn1.x509.KeyPurposeId +import org.bouncycastle.asn1.x509.KeyUsage +import org.bouncycastle.asn1.x509.SubjectKeyIdentifier +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import org.bouncycastle.cert.X509ExtensionUtils +import org.bouncycastle.cert.X509v3CertificateBuilder +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.operator.ContentSigner +import org.bouncycastle.operator.bc.BcDigestCalculatorProvider +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder import java.io.File +import java.math.BigInteger +import java.security.KeyPair +import java.security.KeyPairGenerator import java.security.KeyStore +import java.security.PublicKey +import java.security.cert.X509Certificate +import java.time.Duration +import java.time.Instant +import java.util.Date import javax.net.ssl.KeyManagerFactory import javax.net.ssl.SSLContext import javax.net.ssl.SSLEngine class Ssl @JvmOverloads constructor( - private val keystore: String, - private val keystorePassword: String = "", - private val keystoreType: String = "PKCS12", + val sslKeystore: SslKeystore = SslKeystore() ) { fun sslEngine(): SSLEngine = sslContext().createSSLEngine().apply { useClientMode = false needClientAuth = false } - internal fun sslContext(): SSLContext { - val keyStore = keyStore() + private fun sslContext(): SSLContext { val keyManager = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()).apply { - init(keyStore, keystorePassword.toCharArray()) + init(sslKeystore.keyStore, sslKeystore.keyPassword.toCharArray()) } return SSLContext.getInstance("TLS").apply { init(keyManager.keyManagers, null, null) } } +} + +class SslKeystore @JvmOverloads constructor( + val keyPassword: String = "q", + val keyStore: KeyStore = generate("localhost", keyPassword) +) { + @JvmOverloads constructor( + keyPassword: String, + keystoreFile: File, + keystoreType: KeyStoreType = KeyStoreType.PKCS12, + keystorePassword: String = "", + ) : this(keyPassword, keyStore(keystoreFile, keystoreType, keystorePassword)) + + enum class KeyStoreType { + PKCS12, + JKS + } + + companion object { + private const val CERT_SIGNATURE_ALG = "SHA256withRSA" + private const val KEY_ALG = "RSA" + private const val KEY_SIZE = 2048 + + fun generate(hostname: String, keyPassword: String): KeyStore { + val keyPair = KeyPairGenerator.getInstance(KEY_ALG).apply { initialize(KEY_SIZE) }.generateKeyPair() + val cert = keyPair.toX509Certificate(hostname) + return KeyStore.getInstance(KeyStoreType.PKCS12.name).apply { + this.load(null) + this.setKeyEntry(hostname, keyPair.private, keyPassword.toCharArray(), arrayOf(cert)) + } + } - internal fun keyStore() = KeyStore.getInstance(keystoreType).apply { - File(keystore).inputStream().use { - load(it, keystorePassword.toCharArray()) + private fun keyStore( + keystoreFile: File, + keystoreType: KeyStoreType = KeyStoreType.PKCS12, + keystorePassword: String = "", + ) = KeyStore.getInstance(keystoreType.name).apply { + keystoreFile.inputStream().use { + load(it, keystorePassword.toCharArray()) + } } + + private fun KeyPair.toX509Certificate(cn: String, expiry: Duration = Duration.ofDays(365)): X509Certificate { + val now = Instant.now() + val x500Name = X500Name("CN=$cn") + val contentSigner: ContentSigner = JcaContentSignerBuilder(CERT_SIGNATURE_ALG).build(this.private) + val certificateHolder = JcaX509v3CertificateBuilder( + x500Name, + BigInteger.valueOf(now.toEpochMilli()), + Date.from(now), + Date.from(now.plus(expiry)), + x500Name, + this.public + ).addExtensions(cn, this.public).build(contentSigner) + return JcaX509CertificateConverter().setProvider(BouncyCastleProvider()).getCertificate(certificateHolder) + } + + private fun X509v3CertificateBuilder.addExtensions(cn: String, publicKey: PublicKey) = apply { + addExtension(Extension.subjectKeyIdentifier, false, publicKey.createSubjectKeyId()) + .addExtension(Extension.authorityKeyIdentifier, false, publicKey.createAuthorityKeyId()) + .addExtension(Extension.basicConstraints, true, BasicConstraints(true)) + .addExtension(Extension.subjectAlternativeName, false, GeneralNames(GeneralName(GeneralName.dNSName, cn))) + .addExtension(Extension.keyUsage, false, KeyUsage(KeyUsage.digitalSignature)) + .addExtension(Extension.extendedKeyUsage, false, ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth)) + } + + private fun PublicKey.createSubjectKeyId(): SubjectKeyIdentifier = + X509ExtensionUtils(digestCalculator()).createSubjectKeyIdentifier(SubjectPublicKeyInfo.getInstance(encoded)) + + private fun PublicKey.createAuthorityKeyId(): AuthorityKeyIdentifier = + X509ExtensionUtils(digestCalculator()).createAuthorityKeyIdentifier(SubjectPublicKeyInfo.getInstance(encoded)) + + private fun digestCalculator() = BcDigestCalculatorProvider().get(AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1)) } } diff --git a/src/test/kotlin/no/nav/security/mock/oauth2/server/OAuth2HttpServerTest.kt b/src/test/kotlin/no/nav/security/mock/oauth2/server/OAuth2HttpServerTest.kt index 76bdde44..1073a53c 100644 --- a/src/test/kotlin/no/nav/security/mock/oauth2/server/OAuth2HttpServerTest.kt +++ b/src/test/kotlin/no/nav/security/mock/oauth2/server/OAuth2HttpServerTest.kt @@ -2,18 +2,20 @@ package no.nav.security.mock.oauth2.server import io.kotest.matchers.shouldBe import mu.KotlinLogging -import no.nav.security.mock.oauth2.http.Ssl import no.nav.security.mock.oauth2.http.MockWebServerWrapper import no.nav.security.mock.oauth2.http.NettyWrapper import no.nav.security.mock.oauth2.http.OAuth2HttpResponse import no.nav.security.mock.oauth2.http.OAuth2HttpServer import no.nav.security.mock.oauth2.http.RequestHandler +import no.nav.security.mock.oauth2.http.Ssl +import no.nav.security.mock.oauth2.http.SslKeystore import no.nav.security.mock.oauth2.http.redirect import no.nav.security.mock.oauth2.testutils.get import no.nav.security.mock.oauth2.testutils.post import okhttp3.Headers import okhttp3.OkHttpClient import org.junit.jupiter.api.Test +import java.io.File import java.security.KeyStore import javax.net.ssl.SSLContext import javax.net.ssl.TrustManagerFactory @@ -23,19 +25,7 @@ private val log = KotlinLogging.logger { } internal class OAuth2HttpServerTest { - private val httpsConfig = Ssl( - keystore = "src/test/resources/localhost.p12", - keystorePassword = "", - keystoreType = "PKCS12" - ) - - val client: OkHttpClient = OkHttpClient().newBuilder().apply { - followRedirects(false) - val keyStore: KeyStore = httpsConfig.keyStore() - val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply { init(keyStore) } - val sslContext = SSLContext.getInstance("TLS").apply { init(null, trustManagerFactory.trustManagers, null) } - sslSocketFactory(sslContext.socketFactory, trustManagerFactory.trustManagers[0] as X509TrustManager) - }.build() + val httpClient = OkHttpClient().newBuilder().followRedirects(false).build() val requestHandler: RequestHandler = { log.debug("received request on url=${it.url}") @@ -59,9 +49,23 @@ internal class OAuth2HttpServerTest { } @Test - fun `Netty server should start and serve requests with HTTPS enabled`() { - NettyWrapper(httpsConfig).start(requestHandler).shouldServeRequests().stop() - NettyWrapper(httpsConfig).start(port = 1234, requestHandler).shouldServeRequests().stop() + fun `Netty server should start and serve requests with generated keystore and HTTPS enabled`() { + val ssl = Ssl() + NettyWrapper(ssl).start(requestHandler).shouldServeRequests(ssl).stop() + NettyWrapper(ssl).start(port = 1234, requestHandler).shouldServeRequests(ssl).stop() + } + + @Test + fun `Netty server should start and serve requests with provided keystore and HTTPS enabled`() { + val ssl = Ssl( + SslKeystore( + keyPassword = "", + keystoreFile = File("src/test/resources/localhost.p12"), + keystorePassword = "", + keystoreType = SslKeystore.KeyStoreType.PKCS12 + ) + ) + NettyWrapper(ssl).start(requestHandler).shouldServeRequests(ssl).stop() } @Test @@ -70,7 +74,13 @@ internal class OAuth2HttpServerTest { MockWebServerWrapper().start(port = 1234, requestHandler).shouldServeRequests().stop() } - private fun OAuth2HttpServer.shouldServeRequests() = apply { + private fun OAuth2HttpServer.shouldServeRequests(ssl: Ssl? = null) = apply { + val client = if (ssl != null) { + httpClient.withTrustStore(ssl.sslKeystore.keyStore) + } else { + httpClient + } + client.get( this.url("/header"), Headers.headersOf("header1", "headervalue1") @@ -86,6 +96,14 @@ internal class OAuth2HttpServerTest { } } + private fun OkHttpClient.withTrustStore(keyStore: KeyStore): OkHttpClient = + newBuilder().apply { + followRedirects(false) + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply { init(keyStore) } + val sslContext = SSLContext.getInstance("TLS").apply { init(null, trustManagerFactory.trustManagers, null) } + sslSocketFactory(sslContext.socketFactory, trustManagerFactory.trustManagers[0] as X509TrustManager) + }.build() + private fun ok(body: String) = OAuth2HttpResponse( status = 200, body = body diff --git a/src/test/kotlin/no/nav/security/mock/oauth2/testutils/Certificates.kt b/src/test/kotlin/no/nav/security/mock/oauth2/testutils/Certificates.kt deleted file mode 100644 index 29e6e6b0..00000000 --- a/src/test/kotlin/no/nav/security/mock/oauth2/testutils/Certificates.kt +++ /dev/null @@ -1,86 +0,0 @@ -package no.nav.security.mock.oauth2.testutils - -import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers -import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x509.AlgorithmIdentifier -import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier -import org.bouncycastle.asn1.x509.BasicConstraints -import org.bouncycastle.asn1.x509.ExtendedKeyUsage -import org.bouncycastle.asn1.x509.Extension -import org.bouncycastle.asn1.x509.GeneralName -import org.bouncycastle.asn1.x509.GeneralNames -import org.bouncycastle.asn1.x509.KeyPurposeId -import org.bouncycastle.asn1.x509.KeyUsage -import org.bouncycastle.asn1.x509.SubjectKeyIdentifier -import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo -import org.bouncycastle.cert.X509ExtensionUtils -import org.bouncycastle.cert.X509v3CertificateBuilder -import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter -import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder -import org.bouncycastle.jce.provider.BouncyCastleProvider -import org.bouncycastle.openssl.jcajce.JcaPEMWriter -import org.bouncycastle.operator.ContentSigner -import org.bouncycastle.operator.bc.BcDigestCalculatorProvider -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder -import java.io.IOException -import java.io.StringWriter -import java.math.BigInteger -import java.security.KeyPair -import java.security.KeyPairGenerator -import java.security.PublicKey -import java.security.cert.X509Certificate -import java.time.Duration -import java.time.Instant -import java.util.Date - -class Certificates { - - fun KeyPair.toX509v3Certificate(cn: String, expiry: Duration = Duration.ofDays(365)): X509Certificate { - val now = Instant.now() - val x500Name = X500Name("CN=$cn") - val contentSigner: ContentSigner = JcaContentSignerBuilder("SHA256withRSA").build(this.private) - val certificateHolder = JcaX509v3CertificateBuilder( - x500Name, - BigInteger.valueOf(now.toEpochMilli()), - Date.from(now), - Date.from(now.plus(expiry)), - x500Name, - this.public - ).addExtensions(cn, this.public).build(contentSigner) - return JcaX509CertificateConverter().setProvider(BouncyCastleProvider()).getCertificate(certificateHolder) - } - - private fun X509v3CertificateBuilder.addExtensions(cn: String, publicKey: PublicKey) = apply { - addExtension(Extension.subjectKeyIdentifier, false, publicKey.createSubjectKeyId()) - .addExtension(Extension.authorityKeyIdentifier, false, publicKey.createAuthorityKeyId()) - .addExtension(Extension.basicConstraints, true, BasicConstraints(true)) - .addExtension(Extension.subjectAlternativeName, false, GeneralNames(GeneralName(GeneralName.dNSName, cn))) - .addExtension(Extension.keyUsage, false, KeyUsage(KeyUsage.digitalSignature)) - .addExtension(Extension.extendedKeyUsage, false, ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth)) - } - - fun main() { - val keyPair = KeyPairGenerator.getInstance("RSA") - .apply { initialize(2048) }.generateKeyPair() - - println(x509CertificateToPem(keyPair.toX509v3Certificate("yolo"))) - } - - @Throws(IOException::class) - fun x509CertificateToPem(cert: X509Certificate?): String? { - val writer = StringWriter() - val pemWriter = JcaPEMWriter(writer) - pemWriter.writeObject(cert) - pemWriter.flush() - pemWriter.close() - return writer.toString() - } - - private fun PublicKey.createSubjectKeyId(): SubjectKeyIdentifier = - X509ExtensionUtils(digestCalculator()).createSubjectKeyIdentifier(SubjectPublicKeyInfo.getInstance(encoded)) - - private fun PublicKey.createAuthorityKeyId(): AuthorityKeyIdentifier = - X509ExtensionUtils(digestCalculator()).createAuthorityKeyIdentifier(SubjectPublicKeyInfo.getInstance(encoded)) - - private fun digestCalculator() = BcDigestCalculatorProvider().get(AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1)) -} From 390b0097cd627386847cdf0b84e6d120302c76ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tommy=20Tr=C3=B8en?= Date: Fri, 4 Jun 2021 21:00:28 +0200 Subject: [PATCH 04/11] refactor: make sslContext public as it is needed to retrieve sslSocketFactory * remove typo --- src/main/kotlin/no/nav/security/mock/oauth2/http/Ssl.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/no/nav/security/mock/oauth2/http/Ssl.kt b/src/main/kotlin/no/nav/security/mock/oauth2/http/Ssl.kt index d9d77336..f6499de2 100644 --- a/src/main/kotlin/no/nav/security/mock/oauth2/http/Ssl.kt +++ b/src/main/kotlin/no/nav/security/mock/oauth2/http/Ssl.kt @@ -43,7 +43,7 @@ class Ssl @JvmOverloads constructor( needClientAuth = false } - private fun sslContext(): SSLContext { + fun sslContext(): SSLContext { val keyManager = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()).apply { init(sslKeystore.keyStore, sslKeystore.keyPassword.toCharArray()) } @@ -54,7 +54,7 @@ class Ssl @JvmOverloads constructor( } class SslKeystore @JvmOverloads constructor( - val keyPassword: String = "q", + val keyPassword: String = "", val keyStore: KeyStore = generate("localhost", keyPassword) ) { @JvmOverloads constructor( From dae54f00aa41a324bda1f2a7c98f3812be48e750 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tommy=20Tr=C3=B8en?= Date: Fri, 4 Jun 2021 21:06:55 +0200 Subject: [PATCH 05/11] feature: enable https support for MockWebServerWrapper --- .../no/nav/security/mock/oauth2/http/OAuth2HttpServer.kt | 9 +++++++-- .../security/mock/oauth2/server/OAuth2HttpServerTest.kt | 7 +++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/no/nav/security/mock/oauth2/http/OAuth2HttpServer.kt b/src/main/kotlin/no/nav/security/mock/oauth2/http/OAuth2HttpServer.kt index db86bf96..2f0364cb 100644 --- a/src/main/kotlin/no/nav/security/mock/oauth2/http/OAuth2HttpServer.kt +++ b/src/main/kotlin/no/nav/security/mock/oauth2/http/OAuth2HttpServer.kt @@ -57,13 +57,18 @@ interface OAuth2HttpServer : AutoCloseable { fun url(path: String): HttpUrl } -class MockWebServerWrapper : OAuth2HttpServer { +class MockWebServerWrapper@JvmOverloads constructor( + private val ssl: Ssl? = null +) : OAuth2HttpServer { val mockWebServer: MockWebServer = MockWebServer() override fun start(inetAddress: InetAddress, port: Int, requestHandler: RequestHandler): OAuth2HttpServer = apply { mockWebServer.start(inetAddress, port) - log.debug("started server on address=$inetAddress and port=${mockWebServer.port}") mockWebServer.dispatcher = MockWebServerDispatcher(requestHandler) + if (ssl != null) { + mockWebServer.useHttps(ssl.sslContext().socketFactory, false) + } + log.debug("started server on address=$inetAddress and port=${mockWebServer.port}, httpsEnabled=${ssl != null}") } override fun stop(): OAuth2HttpServer = apply { diff --git a/src/test/kotlin/no/nav/security/mock/oauth2/server/OAuth2HttpServerTest.kt b/src/test/kotlin/no/nav/security/mock/oauth2/server/OAuth2HttpServerTest.kt index 1073a53c..79d618e5 100644 --- a/src/test/kotlin/no/nav/security/mock/oauth2/server/OAuth2HttpServerTest.kt +++ b/src/test/kotlin/no/nav/security/mock/oauth2/server/OAuth2HttpServerTest.kt @@ -74,6 +74,13 @@ internal class OAuth2HttpServerTest { MockWebServerWrapper().start(port = 1234, requestHandler).shouldServeRequests().stop() } + @Test + fun `MockWebServer should start and serve requests with generated keystore and HTTPS enabled`() { + val ssl = Ssl() + MockWebServerWrapper(ssl).start(requestHandler).shouldServeRequests(ssl).stop() + MockWebServerWrapper(ssl).start(port = 1234, requestHandler).shouldServeRequests(ssl).stop() + } + private fun OAuth2HttpServer.shouldServeRequests(ssl: Ssl? = null) = apply { val client = if (ssl != null) { httpClient.withTrustStore(ssl.sslKeystore.keyStore) From e307285df7a54bd96541badd1b0b27a77bbc41bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tommy=20Tr=C3=B8en?= Date: Sat, 5 Jun 2021 19:52:57 +0200 Subject: [PATCH 06/11] tests: add integration test for https --- .../oauth2/e2e/MockOAuth2ServerIntegrationTest.kt | 15 +++++++++++++++ .../mock/oauth2/server/OAuth2HttpServerTest.kt | 13 +------------ .../no/nav/security/mock/oauth2/testutils/Http.kt | 12 ++++++++++++ 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/test/kotlin/no/nav/security/mock/oauth2/e2e/MockOAuth2ServerIntegrationTest.kt b/src/test/kotlin/no/nav/security/mock/oauth2/e2e/MockOAuth2ServerIntegrationTest.kt index f1629925..2dc961e6 100644 --- a/src/test/kotlin/no/nav/security/mock/oauth2/e2e/MockOAuth2ServerIntegrationTest.kt +++ b/src/test/kotlin/no/nav/security/mock/oauth2/e2e/MockOAuth2ServerIntegrationTest.kt @@ -13,7 +13,9 @@ import io.kotest.matchers.string.shouldStartWith import no.nav.security.mock.oauth2.MockOAuth2Server import no.nav.security.mock.oauth2.OAuth2Config import no.nav.security.mock.oauth2.extensions.verifySignatureAndIssuer +import no.nav.security.mock.oauth2.http.MockWebServerWrapper import no.nav.security.mock.oauth2.http.OAuth2HttpResponse +import no.nav.security.mock.oauth2.http.Ssl import no.nav.security.mock.oauth2.http.WellKnown import no.nav.security.mock.oauth2.http.route import no.nav.security.mock.oauth2.testutils.audience @@ -26,6 +28,7 @@ import no.nav.security.mock.oauth2.testutils.post import no.nav.security.mock.oauth2.testutils.subject import no.nav.security.mock.oauth2.testutils.toTokenResponse import no.nav.security.mock.oauth2.testutils.tokenRequest +import no.nav.security.mock.oauth2.testutils.withTrustStore import no.nav.security.mock.oauth2.token.DefaultOAuth2TokenCallback import no.nav.security.mock.oauth2.withMockOAuth2Server import okhttp3.HttpUrl.Companion.toHttpUrl @@ -81,6 +84,18 @@ class MockOAuth2ServerIntegrationTest { } } + @Test + fun `welknown should include https addresses when MockWebServerWrapper is started with https enabled`() { + val ssl = Ssl() + val server = MockOAuth2Server( + OAuth2Config(httpServer = MockWebServerWrapper(ssl)) + ).apply { start() } + client.withTrustStore(ssl.sslKeystore.keyStore).get(server.wellKnownUrl("issuer1")).parse().asClue { + it urlsShouldStartWith "https" + } + server.shutdown() + } + @Test fun `token request with enqueued token callback should return claims from tokencallback (with exception of id_token and oidc rules)`() { val server = MockOAuth2Server().apply { start() } diff --git a/src/test/kotlin/no/nav/security/mock/oauth2/server/OAuth2HttpServerTest.kt b/src/test/kotlin/no/nav/security/mock/oauth2/server/OAuth2HttpServerTest.kt index 79d618e5..498d95d2 100644 --- a/src/test/kotlin/no/nav/security/mock/oauth2/server/OAuth2HttpServerTest.kt +++ b/src/test/kotlin/no/nav/security/mock/oauth2/server/OAuth2HttpServerTest.kt @@ -12,14 +12,11 @@ import no.nav.security.mock.oauth2.http.SslKeystore import no.nav.security.mock.oauth2.http.redirect import no.nav.security.mock.oauth2.testutils.get import no.nav.security.mock.oauth2.testutils.post +import no.nav.security.mock.oauth2.testutils.withTrustStore import okhttp3.Headers import okhttp3.OkHttpClient import org.junit.jupiter.api.Test import java.io.File -import java.security.KeyStore -import javax.net.ssl.SSLContext -import javax.net.ssl.TrustManagerFactory -import javax.net.ssl.X509TrustManager private val log = KotlinLogging.logger { } @@ -103,14 +100,6 @@ internal class OAuth2HttpServerTest { } } - private fun OkHttpClient.withTrustStore(keyStore: KeyStore): OkHttpClient = - newBuilder().apply { - followRedirects(false) - val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply { init(keyStore) } - val sslContext = SSLContext.getInstance("TLS").apply { init(null, trustManagerFactory.trustManagers, null) } - sslSocketFactory(sslContext.socketFactory, trustManagerFactory.trustManagers[0] as X509TrustManager) - }.build() - private fun ok(body: String) = OAuth2HttpResponse( status = 200, body = body diff --git a/src/test/kotlin/no/nav/security/mock/oauth2/testutils/Http.kt b/src/test/kotlin/no/nav/security/mock/oauth2/testutils/Http.kt index 6820cdf2..66249795 100644 --- a/src/test/kotlin/no/nav/security/mock/oauth2/testutils/Http.kt +++ b/src/test/kotlin/no/nav/security/mock/oauth2/testutils/Http.kt @@ -11,6 +11,10 @@ import okhttp3.HttpUrl import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response +import java.security.KeyStore +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509TrustManager fun Response.toTokenResponse(): ParsedTokenResponse = ParsedTokenResponse( this.code, @@ -31,6 +35,14 @@ fun client(followRedirects: Boolean = false): OkHttpClient = .followRedirects(followRedirects) .build() +fun OkHttpClient.withTrustStore(keyStore: KeyStore, followRedirects: Boolean = false): OkHttpClient = + newBuilder().apply { + followRedirects(followRedirects) + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply { init(keyStore) } + val sslContext = SSLContext.getInstance("TLS").apply { init(null, trustManagerFactory.trustManagers, null) } + sslSocketFactory(sslContext.socketFactory, trustManagerFactory.trustManagers[0] as X509TrustManager) + }.build() + fun OkHttpClient.tokenRequest(url: HttpUrl, parameters: Map): Response = tokenRequest(url, Headers.headersOf(), parameters) From 31110a2dbc906c07c66aa45c8f4cc1e9c2c7eb0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tommy=20Tr=C3=B8en?= Date: Sat, 5 Jun 2021 19:55:54 +0200 Subject: [PATCH 07/11] chore:remove unused file --- src/test/resources/createCert.sh | 5 ----- 1 file changed, 5 deletions(-) delete mode 100755 src/test/resources/createCert.sh diff --git a/src/test/resources/createCert.sh b/src/test/resources/createCert.sh deleted file mode 100755 index ca76f9bf..00000000 --- a/src/test/resources/createCert.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -openssl req -x509 -out localhost.crt -keyout localhost.key \ - -newkey rsa:2048 -nodes -sha256 -days 3650\ - -subj '/CN=localhost' -extensions EXT -config <( printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth") From ac5668d008bd22263d2a8f9bbca4f1302ae2102b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tommy=20Tr=C3=B8en?= Date: Tue, 8 Jun 2021 21:08:12 +0200 Subject: [PATCH 08/11] feat: support ssl with json config * allow to byo keystore or generate * make Ssl non-private in order to retrieve the keystore when testing against the generated server cert --- .../nav/security/mock/oauth2/OAuth2Config.kt | 36 +++++- .../mock/oauth2/http/OAuth2HttpServer.kt | 4 +- .../security/mock/oauth2/OAuth2ConfigTest.kt | 111 +++++++++++++++++- 3 files changed, 142 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/no/nav/security/mock/oauth2/OAuth2Config.kt b/src/main/kotlin/no/nav/security/mock/oauth2/OAuth2Config.kt index b5b7c73d..63a6666c 100644 --- a/src/main/kotlin/no/nav/security/mock/oauth2/OAuth2Config.kt +++ b/src/main/kotlin/no/nav/security/mock/oauth2/OAuth2Config.kt @@ -1,18 +1,21 @@ package no.nav.security.mock.oauth2 import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import no.nav.security.mock.oauth2.http.MockWebServerWrapper import no.nav.security.mock.oauth2.http.NettyWrapper import no.nav.security.mock.oauth2.http.OAuth2HttpServer +import no.nav.security.mock.oauth2.http.Ssl +import no.nav.security.mock.oauth2.http.SslKeystore import no.nav.security.mock.oauth2.token.OAuth2TokenCallback import no.nav.security.mock.oauth2.token.OAuth2TokenProvider import no.nav.security.mock.oauth2.token.RequestMappingTokenCallback +import java.io.File data class OAuth2Config @JvmOverloads constructor( val interactiveLogin: Boolean = false, @@ -29,11 +32,34 @@ data class OAuth2Config @JvmOverloads constructor( NettyWrapper } + data class ServerConfig( + val type: ServerType, + val ssl: SslConfig? = null + ) + + data class SslConfig( + val keyPassword: String = "", + val keystoreFile: File? = null, + val keystoreType: SslKeystore.KeyStoreType = SslKeystore.KeyStoreType.PKCS12, + val keystorePassword: String = "" + ) { + fun ssl() = Ssl(sslKeyStore()) + + private fun sslKeyStore() = + if (keystoreFile == null) SslKeystore() else SslKeystore(keyPassword, keystoreFile, keystoreType, keystorePassword) + } + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): OAuth2HttpServer { - return when (p.readValueAs(object : TypeReference() {})) { - ServerType.NettyWrapper -> NettyWrapper() - ServerType.MockWebServerWrapper -> MockWebServerWrapper() - else -> throw IllegalArgumentException("unsupported httpServer specified in config") + val node: JsonNode = p.readValueAsTree() + val serverConfig: ServerConfig = if (node.isObject) { + p.codec.treeToValue(node, ServerConfig::class.java) + } else { + ServerConfig(ServerType.valueOf(node.textValue())) + } + val ssl: Ssl? = serverConfig.ssl?.ssl() + return when (serverConfig.type) { + ServerType.NettyWrapper -> NettyWrapper(ssl) + ServerType.MockWebServerWrapper -> MockWebServerWrapper(ssl) } } } diff --git a/src/main/kotlin/no/nav/security/mock/oauth2/http/OAuth2HttpServer.kt b/src/main/kotlin/no/nav/security/mock/oauth2/http/OAuth2HttpServer.kt index 2f0364cb..25131489 100644 --- a/src/main/kotlin/no/nav/security/mock/oauth2/http/OAuth2HttpServer.kt +++ b/src/main/kotlin/no/nav/security/mock/oauth2/http/OAuth2HttpServer.kt @@ -58,7 +58,7 @@ interface OAuth2HttpServer : AutoCloseable { } class MockWebServerWrapper@JvmOverloads constructor( - private val ssl: Ssl? = null + val ssl: Ssl? = null ) : OAuth2HttpServer { val mockWebServer: MockWebServer = MockWebServer() @@ -100,7 +100,7 @@ class MockWebServerWrapper@JvmOverloads constructor( } class NettyWrapper @JvmOverloads constructor( - private val ssl: Ssl? = null + val ssl: Ssl? = null ) : OAuth2HttpServer { private val masterGroup = NioEventLoopGroup() private val workerGroup = NioEventLoopGroup() diff --git a/src/test/kotlin/no/nav/security/mock/oauth2/OAuth2ConfigTest.kt b/src/test/kotlin/no/nav/security/mock/oauth2/OAuth2ConfigTest.kt index 4a00c189..b3396a5c 100644 --- a/src/test/kotlin/no/nav/security/mock/oauth2/OAuth2ConfigTest.kt +++ b/src/test/kotlin/no/nav/security/mock/oauth2/OAuth2ConfigTest.kt @@ -1,12 +1,18 @@ package no.nav.security.mock.oauth2 -import com.fasterxml.jackson.databind.exc.InvalidFormatException +import com.fasterxml.jackson.databind.JsonMappingException import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.Matcher +import io.kotest.matchers.MatcherResult import io.kotest.matchers.collections.shouldContainAll import io.kotest.matchers.should import io.kotest.matchers.shouldBe import io.kotest.matchers.types.beInstanceOf import no.nav.security.mock.oauth2.FullConfig.configJson +import no.nav.security.mock.oauth2.HttpServerConfig.mockWebServerWithGeneratedKeystore +import no.nav.security.mock.oauth2.HttpServerConfig.mockWebServerWithProvidedKeystore +import no.nav.security.mock.oauth2.HttpServerConfig.nettyWithGeneratedKeystore +import no.nav.security.mock.oauth2.HttpServerConfig.nettyWithProvidedKeystore import no.nav.security.mock.oauth2.HttpServerConfig.withMockWebServerWrapper import no.nav.security.mock.oauth2.HttpServerConfig.withNettyHttpServer import no.nav.security.mock.oauth2.HttpServerConfig.withUnknownHttpServer @@ -14,6 +20,12 @@ import no.nav.security.mock.oauth2.http.MockWebServerWrapper import no.nav.security.mock.oauth2.http.NettyWrapper import org.intellij.lang.annotations.Language import org.junit.jupiter.api.Test +import java.io.File +import java.security.KeyStore +import java.security.cert.X509Certificate +import java.time.Duration +import java.time.Instant +import java.util.Date internal class OAuth2ConfigTest { @@ -21,7 +33,7 @@ internal class OAuth2ConfigTest { fun `create httpServer from json`() { OAuth2Config.fromJson(withNettyHttpServer).httpServer should beInstanceOf() OAuth2Config.fromJson(withMockWebServerWrapper).httpServer should beInstanceOf() - shouldThrow { + shouldThrow { OAuth2Config.fromJson(withUnknownHttpServer) } } @@ -36,6 +48,57 @@ internal class OAuth2ConfigTest { it.issuerId() }.toList() shouldContainAll listOf("issuer1", "issuer2") } + + @Test + fun `create NettyWrapper with https enabled and provided keystore`() { + val server = OAuth2Config.fromJson(nettyWithProvidedKeystore).httpServer as NettyWrapper + val actualKeyStore = server.ssl?.sslKeystore?.keyStore + val actualPrivateKey = actualKeyStore?.getKey("localhost", "".toCharArray()) + val expectedPrivateKey = privateKeyFromFile() + actualPrivateKey shouldBe expectedPrivateKey + } + + @Test + fun `create MockWebServerWrapper with https enabled and provided keystore`() { + val server = OAuth2Config.fromJson(mockWebServerWithProvidedKeystore).httpServer as MockWebServerWrapper + val actualKeyStore = server.ssl?.sslKeystore?.keyStore + val actualPrivateKey = actualKeyStore?.getKey("localhost", "".toCharArray()) + val expectedPrivateKey = privateKeyFromFile() + actualPrivateKey shouldBe expectedPrivateKey + } + + @Test + fun `create NettyWrapper with https enabled and generated keystore`() { + val server = OAuth2Config.fromJson(nettyWithGeneratedKeystore).httpServer as NettyWrapper + val actualKeyStore = server.ssl?.sslKeystore?.keyStore + val actualCert = actualKeyStore?.getCertificate("localhost") as X509Certificate + actualCert.notBefore should beAroundNow() + } + + @Test + fun `create MockWebServerWrapper with https enabled and generated keystore`() { + val server = OAuth2Config.fromJson(mockWebServerWithGeneratedKeystore).httpServer as MockWebServerWrapper + val actualKeyStore = server.ssl?.sslKeystore?.keyStore + val actualCert = actualKeyStore?.getCertificate("localhost") as X509Certificate + actualCert.notBefore should beAroundNow() + } + + private fun beAroundNow(skew: Duration = Duration.ofSeconds(2)) = object : Matcher { + override fun test(value: Date): MatcherResult { + val now = Instant.now() + val withSkew = value.toInstant().plus(skew) + return MatcherResult( + withSkew.isAfter(now), + "Date $withSkew should be after $now", + "Date $withSkew should not be after $now" + ) + } + } + + private fun privateKeyFromFile() = + KeyStore.getInstance("PKCS12").apply { + File("src/test/resources/localhost.p12").inputStream().use { load(it, "".toCharArray()) } + }.getKey("localhost", "".toCharArray()) } object FullConfig { @@ -101,4 +164,48 @@ object HttpServerConfig { "httpServer": "UnknownServer" } """.trimIndent() + + @Language("json") + val nettyWithProvidedKeystore = """ + { + "httpServer" : { + "type" : "NettyWrapper", + "ssl" : { + "keystoreFile" : "src/test/resources/localhost.p12" + } + } + } + """.trimIndent() + + @Language("json") + val nettyWithGeneratedKeystore = """ + { + "httpServer" : { + "type" : "NettyWrapper", + "ssl" : {} + } + } + """.trimIndent() + + @Language("json") + val mockWebServerWithGeneratedKeystore = """ + { + "httpServer" : { + "type" : "MockWebServerWrapper", + "ssl" : {} + } + } + """.trimIndent() + + @Language("json") + val mockWebServerWithProvidedKeystore = """ + { + "httpServer" : { + "type" : "MockWebServerWrapper", + "ssl" : { + "keystoreFile" : "src/test/resources/localhost.p12" + } + } + } + """.trimIndent() } From 1536e8729d5329d29cb7caa90fa059c8e88435fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tommy=20Tr=C3=B8en?= Date: Tue, 8 Jun 2021 21:08:59 +0200 Subject: [PATCH 09/11] doc: fix typos and start ssl doc --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 419f8dd4..4a27f964 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ As of version 0.3.3 the Docker image is published to the [GitHub Container Regis :exclamation: # mock-oauth2-server -A scriptable/customizable web server for testing HTTP clients using OAuth2/OpenID Connect or applications with a dependency to a running OAuth2 server (i.e. APIs requiring signed JWTs from a known issuer). The server also provides the neccessary endpoints for token validation (endpoint for JWKS) and ID Provider metadata discovery ("well-known" endpoints providing server metadata) +A scriptable/customizable web server for testing HTTP clients using OAuth2/OpenID Connect or applications with a dependency to a running OAuth2 server (i.e. APIs requiring signed JWTs from a known issuer). The server also provides the necessary endpoints for token validation (endpoint for JWKS) and ID Provider metadata discovery ("well-known" endpoints providing server metadata) **mock-oauth2-server** is written in Kotlin using the great [OkHttp MockWebServer](https://github.com/square/okhttp/tree/master/mockwebserver) as the underlying server library and can be used in unit/integration tests in both **Java** and **Kotlin** or in any language as a standalone server in e.g. docker-compose. @@ -35,7 +35,7 @@ The motivation behind this library is to provide a setup such that application d * Verify expected requests made to the server * Customizable through exposure of underlying [OkHttp MockWebServer](https://github.com/square/okhttp/tree/master/mockwebserver) * **Standalone support** - i.e. run as application in IDE, run inside your app, or as a Docker image (provided) -* **OAuth2 Client Debugger** - e.g. support for triggering OIDC Auth Code Flow and receiving callback in debugger app, view token reponse from server (intended for standalone support) +* **OAuth2 Client Debugger** - e.g. support for triggering OIDC Auth Code Flow and receiving callback in debugger app, view token response from server (intended for standalone support) @@ -334,6 +334,10 @@ services: The debugger is a OAuth2 client implementing the `authorization_code` flow with a UI for debugging (e.g. request parameters). Point your browser to [http://localhost:8080/default/debugger](http://localhost:8080/default/debugger) to check it out. +### Enabling HTTPS + +TODO + ## 👥 Contact This project is currently maintained by the organisation [@navikt](https://github.com/navikt). From 80f10a1a9ddeba9d7ed1777e9ba2561fced2e821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tommy=20Tr=C3=B8en?= Date: Tue, 8 Jun 2021 21:36:32 +0200 Subject: [PATCH 10/11] doc: document simple usage of HTTPS --- README.md | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a27f964..bcf1a7b1 100644 --- a/README.md +++ b/README.md @@ -336,7 +336,70 @@ Point your browser to [http://localhost:8080/default/debugger](http://localhost: ### Enabling HTTPS -TODO +In order to enable HTTPS you can either provide your own keystore or let the server generate one for you. + +#### Unit tests + +You need to supply the server with an SSL config, in order to do that you must specify your chosen server type in `OAuth2Config` and +pass in the SSL config to your server. + +*Generate keystore:* +```kotlin +val ssl = Ssl() +val server = MockOAuth2Server( + OAuth2Config(httpServer = MockWebServerWrapper(ssl)) +) +``` +*This will generate a SSL certificate for `localhost` and can be added to your client's truststore by getting the ssl config: +`ssl.sslKeystore.keyStore`* + +*Bring your own:* +```kotlin +val ssl = Ssl( + SslKeystore( + keyPassword = "", + keystoreFile = File("src/test/resources/localhost.p12"), + keystorePassword = "", + keystoreType = SslKeystore.KeyStoreType.PKCS12 + ) +) +val server = MockOAuth2Server( + OAuth2Config(httpServer = MockWebServerWrapper(ssl)) +) +``` + +#### Docker / Standalone mode - JSON_CONFIG + +In order to enable HTTPS for the server in Docker or standalone mode +you can either make the server generate the keystore or bring your own. + +*Generate keystore:* + +```json +{ + "httpServer" : { + "type" : "NettyWrapper", + "ssl" : {} + } +} +``` + +*Bring your own:* + +```json + +{ + "httpServer" : { + "type" : "NettyWrapper", + "ssl" : { + "keyPassword" : "", + "keystoreFile" : "src/test/resources/localhost.p12", + "keystoreType" : "PKCS12", + "keystorePassword" : "" + } + } +} +``` ## 👥 Contact From 81b626c1d2512875d797a54de000054f88f67fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tommy=20Tr=C3=B8en?= Date: Thu, 10 Jun 2021 21:05:05 +0200 Subject: [PATCH 11/11] chore: fix typo in test name after review --- .../security/mock/oauth2/e2e/MockOAuth2ServerIntegrationTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/kotlin/no/nav/security/mock/oauth2/e2e/MockOAuth2ServerIntegrationTest.kt b/src/test/kotlin/no/nav/security/mock/oauth2/e2e/MockOAuth2ServerIntegrationTest.kt index 2dc961e6..dea4fef7 100644 --- a/src/test/kotlin/no/nav/security/mock/oauth2/e2e/MockOAuth2ServerIntegrationTest.kt +++ b/src/test/kotlin/no/nav/security/mock/oauth2/e2e/MockOAuth2ServerIntegrationTest.kt @@ -85,7 +85,7 @@ class MockOAuth2ServerIntegrationTest { } @Test - fun `welknown should include https addresses when MockWebServerWrapper is started with https enabled`() { + fun `wellknown should include https addresses when MockWebServerWrapper is started with https enabled`() { val ssl = Ssl() val server = MockOAuth2Server( OAuth2Config(httpServer = MockWebServerWrapper(ssl))