From a9304a1e96b752c803435aaa63f3dc4c656cf36c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tommy=20Tr=C3=B8en?= Date: Thu, 5 Jan 2023 22:31:59 +0100 Subject: [PATCH] fix(ssl): support SSL/TLS from debugger client * ignore certificate_unknown exception from Netty as it doesn't seem to affect the server, and no known fixes has been found for localhost certs. --- docker-compose-ssl.yaml | 13 +++++++ .../security/mock/oauth2/debugger/Client.kt | 12 ++++++ .../oauth2/debugger/DebuggerRequestHandler.kt | 12 ++++-- .../mock/oauth2/http/OAuth2HttpRequest.kt | 9 ----- .../oauth2/http/OAuth2HttpRequestHandler.kt | 2 +- .../mock/oauth2/http/OAuth2HttpServer.kt | 18 +++++++++ .../no/nav/security/mock/oauth2/http/Ssl.kt | 39 ++++++++++++------- src/test/resources/config-ssl.json | 7 ++++ 8 files changed, 84 insertions(+), 28 deletions(-) create mode 100644 docker-compose-ssl.yaml create mode 100644 src/test/resources/config-ssl.json diff --git a/docker-compose-ssl.yaml b/docker-compose-ssl.yaml new file mode 100644 index 00000000..9f8813a5 --- /dev/null +++ b/docker-compose-ssl.yaml @@ -0,0 +1,13 @@ +version: "3.1" + +services: + mock-oauth2-server: + image: mock-oauth2-server:latest + ports: + - "8080:8080" + volumes: + - ./src/test/resources/config-ssl.json:/app/config.json + environment: + LOG_LEVEL: "debug" + SERVER_PORT: 8080 + JSON_CONFIG_PATH: /app/config.json diff --git a/src/main/kotlin/no/nav/security/mock/oauth2/debugger/Client.kt b/src/main/kotlin/no/nav/security/mock/oauth2/debugger/Client.kt index 7746d094..70a0b359 100644 --- a/src/main/kotlin/no/nav/security/mock/oauth2/debugger/Client.kt +++ b/src/main/kotlin/no/nav/security/mock/oauth2/debugger/Client.kt @@ -12,6 +12,10 @@ import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.internal.toHostHeader import java.net.URLEncoder import java.nio.charset.StandardCharsets +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509TrustManager +import no.nav.security.mock.oauth2.http.Ssl internal class TokenRequest( val url: HttpUrl, @@ -78,3 +82,11 @@ internal fun OkHttpClient.post(tokenRequest: TokenRequest): String = .post(tokenRequest.body.toRequestBody("application/x-www-form-urlencoded".toMediaType())) .build() ).execute().body?.string() ?: throw RuntimeException("could not get response body from url=${tokenRequest.url}") + +fun OkHttpClient.withSsl(ssl: Ssl, followRedirects: Boolean = false): OkHttpClient = + newBuilder().apply { + followRedirects(followRedirects) + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply { init(ssl.sslKeystore.keyStore) } + val sslContext = SSLContext.getInstance("TLS").apply { init(null, trustManagerFactory.trustManagers, null) } + sslSocketFactory(sslContext.socketFactory, trustManagerFactory.trustManagers[0] as X509TrustManager) + }.build() diff --git a/src/main/kotlin/no/nav/security/mock/oauth2/debugger/DebuggerRequestHandler.kt b/src/main/kotlin/no/nav/security/mock/oauth2/debugger/DebuggerRequestHandler.kt index 471705dc..5760300f 100644 --- a/src/main/kotlin/no/nav/security/mock/oauth2/debugger/DebuggerRequestHandler.kt +++ b/src/main/kotlin/no/nav/security/mock/oauth2/debugger/DebuggerRequestHandler.kt @@ -12,6 +12,7 @@ import no.nav.security.mock.oauth2.extensions.toDebuggerUrl import no.nav.security.mock.oauth2.http.ExceptionHandler import no.nav.security.mock.oauth2.http.OAuth2HttpResponse import no.nav.security.mock.oauth2.http.Route +import no.nav.security.mock.oauth2.http.Ssl import no.nav.security.mock.oauth2.http.html import no.nav.security.mock.oauth2.http.redirect import no.nav.security.mock.oauth2.http.routes @@ -26,10 +27,11 @@ private val client: OkHttpClient = OkHttpClient().newBuilder().build() class DebuggerRequestHandler( sessionManager: SessionManager = SessionManager(), + ssl: Ssl? = null, route: Route = routes { exceptionHandler(handle(sessionManager)) debuggerForm(sessionManager) - debuggerCallback(sessionManager) + debuggerCallback(sessionManager, ssl) } ) : Route by route @@ -72,7 +74,7 @@ private fun Route.Builder.debuggerForm(sessionManager: SessionManager) = apply { } } -private fun Route.Builder.debuggerCallback(sessionManager: SessionManager) = +private fun Route.Builder.debuggerCallback(sessionManager: SessionManager, ssl: Ssl? = null) = any(DEBUGGER_CALLBACK) { log.debug("handling ${it.method} request to debugger callback") val session = sessionManager.session(it) @@ -91,6 +93,10 @@ private fun Route.Builder.debuggerCallback(sessionManager: SessionManager) = "redirect_uri" to session["redirect_uri"].urlEncode() ) ) - val response = client.post(request) + val response = if (ssl != null) { + client.withSsl(ssl).post(request) + } else { + client.post(request) + } html(templateMapper.debuggerCallbackHtml(request.toString(), response)) } diff --git a/src/main/kotlin/no/nav/security/mock/oauth2/http/OAuth2HttpRequest.kt b/src/main/kotlin/no/nav/security/mock/oauth2/http/OAuth2HttpRequest.kt index 5462f70f..dab551a2 100644 --- a/src/main/kotlin/no/nav/security/mock/oauth2/http/OAuth2HttpRequest.kt +++ b/src/main/kotlin/no/nav/security/mock/oauth2/http/OAuth2HttpRequest.kt @@ -5,15 +5,6 @@ import com.nimbusds.oauth2.sdk.TokenRequest import com.nimbusds.oauth2.sdk.http.HTTPRequest import com.nimbusds.openid.connect.sdk.AuthenticationRequest import no.nav.security.mock.oauth2.extensions.clientAuthentication -import no.nav.security.mock.oauth2.extensions.isAuthorizationEndpointUrl -import no.nav.security.mock.oauth2.extensions.isDebuggerCallbackUrl -import no.nav.security.mock.oauth2.extensions.isDebuggerUrl -import no.nav.security.mock.oauth2.extensions.isEndSessionEndpointUrl -import no.nav.security.mock.oauth2.extensions.isIntrospectUrl -import no.nav.security.mock.oauth2.extensions.isJwksUrl -import no.nav.security.mock.oauth2.extensions.isTokenEndpointUrl -import no.nav.security.mock.oauth2.extensions.isUserInfoUrl -import no.nav.security.mock.oauth2.extensions.isWellKnownUrl import no.nav.security.mock.oauth2.extensions.keyValuesToMap import no.nav.security.mock.oauth2.extensions.requirePrivateKeyJwt import no.nav.security.mock.oauth2.extensions.toAuthorizationEndpointUrl diff --git a/src/main/kotlin/no/nav/security/mock/oauth2/http/OAuth2HttpRequestHandler.kt b/src/main/kotlin/no/nav/security/mock/oauth2/http/OAuth2HttpRequestHandler.kt index be867255..c5a8ab98 100644 --- a/src/main/kotlin/no/nav/security/mock/oauth2/http/OAuth2HttpRequestHandler.kt +++ b/src/main/kotlin/no/nav/security/mock/oauth2/http/OAuth2HttpRequestHandler.kt @@ -50,7 +50,7 @@ private val log = KotlinLogging.logger {} class OAuth2HttpRequestHandler(private val config: OAuth2Config) { private val loginRequestHandler = LoginRequestHandler(templateMapper, config) - private val debuggerRequestHandler = DebuggerRequestHandler() + private val debuggerRequestHandler = DebuggerRequestHandler(ssl = config.httpServer.sslConfig()) private val tokenCallbackQueue: BlockingQueue = LinkedBlockingQueue() private val refreshTokenManager = RefreshTokenManager() 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 5ce2b19f..2a6ebdbe 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 @@ -30,6 +30,7 @@ import java.net.InetAddress import java.net.InetSocketAddress import java.util.concurrent.BlockingQueue import java.util.concurrent.LinkedBlockingQueue +import javax.net.ssl.SSLHandshakeException import kotlin.properties.Delegates import mu.KotlinLogging import no.nav.security.mock.oauth2.extensions.asOAuth2HttpRequest @@ -55,6 +56,7 @@ interface OAuth2HttpServer : AutoCloseable { fun port(): Int fun url(path: String): HttpUrl + fun sslConfig(): Ssl? } class MockWebServerWrapper@JvmOverloads constructor( @@ -78,6 +80,7 @@ class MockWebServerWrapper@JvmOverloads constructor( override fun port(): Int = mockWebServer.port override fun url(path: String): HttpUrl = mockWebServer.url(path) + override fun sslConfig(): Ssl? = ssl internal class MockWebServerDispatcher( private val requestHandler: RequestHandler, @@ -159,9 +162,24 @@ class NettyWrapper @JvmOverloads constructor( .resolve(path)!! } + override fun sslConfig(): Ssl? = ssl + private fun Ssl.nettySslHandler(): SslHandler = SslHandler(sslEngine()) internal class RouterChannelHandler(val requestHandler: RequestHandler) : SimpleChannelInboundHandler() { + @Deprecated("Deprecated in ChannelInboundHandlerAdapter") + override fun exceptionCaught(ctx: ChannelHandlerContext, throwable: Throwable) { + val msg = throwable.message ?: "" + val ignoreError = "certificate_unknown" + + // have not been able to determine why this error is thrown or how to fix it, but it does not seem to affect the server + if (throwable.cause is SSLHandshakeException && msg.contains(ignoreError)) { + log.debug("received $ignoreError error from netty channel, ignoring") + } else { + log.error("error in netty channel handler", throwable) + ctx.close() + } + } override fun channelRead0(ctx: ChannelHandlerContext, request: FullHttpRequest) { val address = ctx.channel().remoteAddress() as InetSocketAddress 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 7c08e078..4c978c53 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,5 +1,18 @@ package no.nav.security.mock.oauth2.http +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 import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.AlgorithmIdentifier @@ -21,19 +34,6 @@ 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( val sslKeystore: SslKeystore = SslKeystore() @@ -57,7 +57,8 @@ class SslKeystore @JvmOverloads constructor( val keyPassword: String = "", val keyStore: KeyStore = generate("localhost", keyPassword) ) { - @JvmOverloads constructor( + @JvmOverloads + constructor( keyPassword: String, keystoreFile: File, keystoreType: KeyStoreType = KeyStoreType.PKCS12, @@ -109,10 +110,18 @@ class SslKeystore @JvmOverloads constructor( } private fun X509v3CertificateBuilder.addExtensions(cn: String, publicKey: PublicKey) = apply { + val san: MutableList = mutableListOf( + GeneralName(GeneralName.dNSName, cn) + ) + + if (cn == "localhost") { + san.add(GeneralName(GeneralName.iPAddress, "127.0.0.1")) + } + 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.subjectAlternativeName, false, GeneralNames(san.toTypedArray())) // GeneralNames(GeneralName(GeneralName.dNSName, san))) .addExtension(Extension.keyUsage, false, KeyUsage(KeyUsage.digitalSignature)) .addExtension(Extension.extendedKeyUsage, false, ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth)) } diff --git a/src/test/resources/config-ssl.json b/src/test/resources/config-ssl.json new file mode 100644 index 00000000..8990cac6 --- /dev/null +++ b/src/test/resources/config-ssl.json @@ -0,0 +1,7 @@ +{ + "interactiveLogin": true, + "httpServer": { + "type": "NettyWrapper", + "ssl": {} + } +}