Skip to content

Commit

Permalink
fix(ssl): support SSL/TLS from debugger client
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
tommytroen committed Jan 5, 2023
1 parent 33ec99c commit a9304a1
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 28 deletions.
13 changes: 13 additions & 0 deletions docker-compose-ssl.yaml
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions src/main/kotlin/no/nav/security/mock/oauth2/debugger/Client.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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))
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<OAuth2TokenCallback> = LinkedBlockingQueue()
private val refreshTokenManager = RefreshTokenManager()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -55,6 +56,7 @@ interface OAuth2HttpServer : AutoCloseable {

fun port(): Int
fun url(path: String): HttpUrl
fun sslConfig(): Ssl?
}

class MockWebServerWrapper@JvmOverloads constructor(
Expand All @@ -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,
Expand Down Expand Up @@ -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<FullHttpRequest>() {
@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
Expand Down
39 changes: 24 additions & 15 deletions src/main/kotlin/no/nav/security/mock/oauth2/http/Ssl.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
Expand All @@ -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,
Expand Down Expand Up @@ -109,10 +110,18 @@ class SslKeystore @JvmOverloads constructor(
}

private fun X509v3CertificateBuilder.addExtensions(cn: String, publicKey: PublicKey) = apply {
val san: MutableList<GeneralName> = 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))
}
Expand Down
7 changes: 7 additions & 0 deletions src/test/resources/config-ssl.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"interactiveLogin": true,
"httpServer": {
"type": "NettyWrapper",
"ssl": {}
}
}

0 comments on commit a9304a1

Please sign in to comment.