From 67bd48d00c429e84d48d60f845919d9f9c45577f Mon Sep 17 00:00:00 2001 From: Dimitris ZARRAS Date: Mon, 20 Nov 2023 15:33:05 +0200 Subject: [PATCH] Provide a single WebClient bean application wide that is either secure or insecure. --- .../ec/eudi/pidissuer/PidIssuerApplication.kt | 100 ++++++++++++------ .../pid/EncodePidInCborWithMicroService.kt | 20 +--- .../out/pid/GetPidDataFromAuthServer.kt | 44 +------- .../adapter/out/webclient/WebClients.kt | 54 ---------- 4 files changed, 78 insertions(+), 140 deletions(-) delete mode 100644 src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/webclient/WebClients.kt diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/PidIssuerApplication.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/PidIssuerApplication.kt index 1060a02b..97724e9d 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/PidIssuerApplication.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/PidIssuerApplication.kt @@ -30,7 +30,6 @@ import eu.europa.ec.eudi.pidissuer.adapter.out.jose.EncryptCredentialResponseWit import eu.europa.ec.eudi.pidissuer.adapter.out.persistence.InMemoryCNonceRepository import eu.europa.ec.eudi.pidissuer.adapter.out.persistence.InMemoryDeferredCredentialRepository import eu.europa.ec.eudi.pidissuer.adapter.out.pid.* -import eu.europa.ec.eudi.pidissuer.adapter.out.webclient.WebClients import eu.europa.ec.eudi.pidissuer.domain.* import eu.europa.ec.eudi.pidissuer.port.input.GetCredentialIssuerMetaData import eu.europa.ec.eudi.pidissuer.port.input.GetDeferredCredential @@ -40,8 +39,11 @@ import eu.europa.ec.eudi.pidissuer.port.out.asDeferred import eu.europa.ec.eudi.pidissuer.port.out.persistence.GenerateCNonce import eu.europa.ec.eudi.pidissuer.port.out.persistence.GenerateTransactionId import eu.europa.ec.eudi.sdjwt.HashAlgorithm +import io.netty.handler.ssl.SslContextBuilder +import io.netty.handler.ssl.util.InsecureTrustManagerFactory import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json +import org.slf4j.LoggerFactory import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties import org.springframework.boot.runApplication @@ -53,6 +55,7 @@ import org.springframework.core.env.Environment import org.springframework.core.env.getProperty import org.springframework.core.env.getRequiredProperty import org.springframework.http.HttpStatus +import org.springframework.http.client.reactive.ReactorClientHttpConnector import org.springframework.http.codec.ServerCodecConfigurer import org.springframework.http.codec.json.KotlinSerializationJsonDecoder import org.springframework.http.codec.json.KotlinSerializationJsonEncoder @@ -63,31 +66,67 @@ import org.springframework.security.oauth2.server.resource.introspection.SpringR import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint import org.springframework.web.reactive.config.EnableWebFlux import org.springframework.web.reactive.config.WebFluxConfigurer +import org.springframework.web.reactive.function.client.WebClient +import reactor.netty.http.client.HttpClient import java.time.Clock import java.time.Duration +private val log = LoggerFactory.getLogger(PidIssuerApplication::class.java) + +/** + * [WebClient] instances for usage within the application. + */ +private object WebClients { + + /** + * A [WebClient] with [Json] serialization enabled. + */ + val Default: WebClient by lazy { + val json = Json { ignoreUnknownKeys = true } + WebClient + .builder() + .codecs { + it.defaultCodecs().kotlinSerializationJsonDecoder(KotlinSerializationJsonDecoder(json)) + it.defaultCodecs().kotlinSerializationJsonEncoder(KotlinSerializationJsonEncoder(json)) + it.defaultCodecs().enableLoggingRequestDetails(true) + } + .build() + } + + /** + * A [WebClient] with [Json] serialization enabled that trusts *all* certificates. + */ + val Insecure: WebClient by lazy { + log.warn("Using insecure WebClient trusting all certificates") + val sslContext = SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .build() + val httpClient = HttpClient.create().secure { it.sslContext(sslContext) } + Default.mutate() + .clientConnector(ReactorClientHttpConnector(httpClient)) + .build() + } +} + fun beans(clock: Clock) = beans { // // Adapters (out ports) // bean { clock } - profile("!insecure") { - bean { - GetPidDataFromAuthServer( - env.readRequiredUrl("issuer.authorizationServer.userinfo"), - env.getRequiredProperty("issuer.pid.issuingCountry").let(::IsoCountry), - clock, - ) + bean { + if ("insecure" in env.activeProfiles) { + WebClients.Insecure + } else { + WebClients.Default } } - profile("insecure") { - bean { - GetPidDataFromAuthServer.insecure( - env.readRequiredUrl("issuer.authorizationServer.userinfo"), - env.getRequiredProperty("issuer.pid.issuingCountry").let(::IsoCountry), - clock, - ) - } + bean { + GetPidDataFromAuthServer( + env.readRequiredUrl("issuer.authorizationServer.userinfo"), + env.getRequiredProperty("issuer.pid.issuingCountry").let(::IsoCountry), + clock, + ref(), + ) } // // Encryption of credential response @@ -122,7 +161,7 @@ fun beans(clock: Clock) = beans { val issuerPublicUrl = env.readRequiredUrl("issuer.publicUrl") bean { - EncodePidInCborWithMicroService(env.readRequiredUrl("issuer.pid.mso_mdoc.encoderUrl")) + EncodePidInCborWithMicroService(env.readRequiredUrl("issuer.pid.mso_mdoc.encoderUrl"), ref()) } CredentialIssuerMetaData( @@ -210,20 +249,6 @@ fun beans(clock: Clock) = beans { // // Security // - profile("insecure") { - bean { - val properties = ref() - - SpringReactiveOpaqueTokenIntrospector( - properties.opaquetoken.introspectionUri, - WebClients.insecure { - defaultHeaders { - it.setBasicAuth(properties.opaquetoken.clientId, properties.opaquetoken.clientSecret) - } - }, - ) - } - } bean { /* * This is a Spring naming convention @@ -262,7 +287,18 @@ fun beans(clock: Clock) = beans { } oauth2ResourceServer { - opaqueToken {} + opaqueToken { + val properties = ref() + introspector = SpringReactiveOpaqueTokenIntrospector( + properties.opaquetoken.introspectionUri, + ref() + .mutate() + .defaultHeaders { + it.setBasicAuth(properties.opaquetoken.clientId, properties.opaquetoken.clientSecret) + } + .build(), + ) + } } } } diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/pid/EncodePidInCborWithMicroService.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/pid/EncodePidInCborWithMicroService.kt index c974f35b..832fa191 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/pid/EncodePidInCborWithMicroService.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/pid/EncodePidInCborWithMicroService.kt @@ -25,8 +25,6 @@ import org.bouncycastle.util.io.pem.PemObject import org.bouncycastle.util.io.pem.PemWriter import org.slf4j.LoggerFactory import org.springframework.http.MediaType -import org.springframework.http.codec.json.KotlinSerializationJsonDecoder -import org.springframework.http.codec.json.KotlinSerializationJsonEncoder import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.awaitBody import java.io.StringWriter @@ -35,24 +33,15 @@ import kotlin.io.encoding.ExperimentalEncodingApi private val log = LoggerFactory.getLogger(EncodePidInCborWithMicroService::class.java) -class EncodePidInCborWithMicroService(private val creatorUrl: HttpsUrl) : EncodePidInCbor { +class EncodePidInCborWithMicroService( + private val creatorUrl: HttpsUrl, + private val webClient: WebClient, +) : EncodePidInCbor { init { log.info("Initialized using: $creatorUrl") } - private val webClient: WebClient by lazy { - val json = Json { ignoreUnknownKeys = true } - WebClient - .builder() - .codecs { configurer -> - configurer.defaultCodecs().kotlinSerializationJsonDecoder(KotlinSerializationJsonDecoder(json)) - configurer.defaultCodecs().kotlinSerializationJsonEncoder(KotlinSerializationJsonEncoder(json)) - configurer.defaultCodecs().enableLoggingRequestDetails(true) - }.baseUrl(creatorUrl.externalForm) - .build() - } - override suspend fun invoke( pid: Pid, pidMetaData: PidMetaData, @@ -61,6 +50,7 @@ class EncodePidInCborWithMicroService(private val creatorUrl: HttpsUrl) : Encode log.info("Requesting PID in mso_mdoc for ${pid.familyName} ...") val request = createMsoMdocReq(pid, pidMetaData, holderKey) webClient.post() + .uri(creatorUrl.externalForm) .accept(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON) .bodyValue(request) diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/pid/GetPidDataFromAuthServer.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/pid/GetPidDataFromAuthServer.kt index 1b130850..eb507591 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/pid/GetPidDataFromAuthServer.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/pid/GetPidDataFromAuthServer.kt @@ -17,7 +17,6 @@ package eu.europa.ec.eudi.pidissuer.adapter.out.pid import eu.europa.ec.eudi.pidissuer.adapter.out.oauth.OidcAddressClaim import eu.europa.ec.eudi.pidissuer.adapter.out.oauth.OidcAssurancePlaceOfBirth -import eu.europa.ec.eudi.pidissuer.adapter.out.webclient.WebClients import eu.europa.ec.eudi.pidissuer.domain.HttpsUrl import kotlinx.serialization.Required import kotlinx.serialization.SerialName @@ -33,7 +32,8 @@ import java.time.LocalDate private val log = LoggerFactory.getLogger(GetPidDataFromAuthServer::class.java) -class GetPidDataFromAuthServer private constructor( +class GetPidDataFromAuthServer( + private val authorizationServerUserInfoEndPoint: HttpsUrl, private val issuerCountry: IsoCountry, private val clock: Clock, private val webClient: WebClient, @@ -52,7 +52,9 @@ class GetPidDataFromAuthServer private constructor( } private suspend fun userInfo(accessToken: String): UserInfo = - webClient.get().accept(MediaType.APPLICATION_JSON) + webClient.get() + .uri(authorizationServerUserInfoEndPoint.externalForm) + .accept(MediaType.APPLICATION_JSON) .headers { headers -> headers.setBearerAuth(accessToken) } .retrieve() .awaitBody() @@ -92,42 +94,6 @@ class GetPidDataFromAuthServer private constructor( return pid to pidMetaData } - - companion object { - - /** - * Creates a new [GetPidDataFromAuthServer] using the provided data. - */ - operator fun invoke( - authorizationServerUserInfoEndPoint: HttpsUrl, - issuerCountry: IsoCountry, - clock: Clock, - ): GetPidDataFromAuthServer = - GetPidDataFromAuthServer( - issuerCountry, - clock, - WebClients.default { - baseUrl(authorizationServerUserInfoEndPoint.externalForm) - }, - ) - - /** - * Creates a new *insecure* [GetPidDataFromAuthServer] that trusts all certificates. - */ - fun insecure( - authorizationServerUserInfoEndPoint: HttpsUrl, - issuerCountry: IsoCountry, - clock: Clock, - ): GetPidDataFromAuthServer { - return GetPidDataFromAuthServer( - issuerCountry, - clock, - WebClients.insecure { - baseUrl(authorizationServerUserInfoEndPoint.externalForm) - }, - ) - } - } } @Serializable diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/webclient/WebClients.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/webclient/WebClients.kt deleted file mode 100644 index 4e4154a1..00000000 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/webclient/WebClients.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2023 European Commission - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package eu.europa.ec.eudi.pidissuer.adapter.out.webclient - -import io.netty.handler.ssl.SslContextBuilder -import io.netty.handler.ssl.util.InsecureTrustManagerFactory -import org.slf4j.LoggerFactory -import org.springframework.http.client.reactive.ReactorClientHttpConnector -import org.springframework.web.reactive.function.client.WebClient -import reactor.netty.http.client.HttpClient - -private val log = LoggerFactory.getLogger(WebClients::class.java) - -/** - * Factories for [WebClient]. - */ -internal object WebClients { - - /** - * Creates a new [WebClient]. - */ - fun default(customizer: WebClient.Builder.() -> Unit = {}): WebClient = - WebClient.builder() - .apply(customizer) - .build() - - /** - * Creates an *insecure* [WebClient] that trusts all certificates. - */ - fun insecure(customizer: WebClient.Builder.() -> Unit = {}): WebClient { - log.warn("Using insecure WebClient trusting all certificates") - val sslContext = SslContextBuilder.forClient() - .trustManager(InsecureTrustManagerFactory.INSTANCE) - .build() - val httpClient = HttpClient.create().secure { it.sslContext(sslContext) } - return WebClient.builder() - .clientConnector(ReactorClientHttpConnector(httpClient)) - .apply(customizer) - .build() - } -}