From 32f6aac330df1eadc3f91d3fe7448c4edc0e802c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Axel=20Wikstr=C3=B6m?= Date: Fri, 24 Jan 2025 15:03:43 +0200 Subject: [PATCH] =?UTF-8?q?Uusi=20OAuth2=20client=20k=C3=A4ytt=C3=B6=C3=B6?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../henkilo/OppijanumeroRekisteriClient.scala | 17 ++-- .../koski/http/OtuvaOAuth2ClientFactory.scala | 81 +++++++++++++++++++ .../koski/http/VirkailijaOAuth2Client.scala | 66 --------------- 3 files changed, 89 insertions(+), 75 deletions(-) create mode 100644 src/main/scala/fi/oph/koski/http/OtuvaOAuth2ClientFactory.scala delete mode 100644 src/main/scala/fi/oph/koski/http/VirkailijaOAuth2Client.scala diff --git a/src/main/scala/fi/oph/koski/henkilo/OppijanumeroRekisteriClient.scala b/src/main/scala/fi/oph/koski/henkilo/OppijanumeroRekisteriClient.scala index b518c3ef429..ab0578bfcad 100644 --- a/src/main/scala/fi/oph/koski/henkilo/OppijanumeroRekisteriClient.scala +++ b/src/main/scala/fi/oph/koski/henkilo/OppijanumeroRekisteriClient.scala @@ -33,9 +33,15 @@ case class OppijanumeroRekisteriClient( def withRetryStrategy(strategy: OppijanumeroRekisteriClientRetryStrategy): OppijanumeroRekisteriClient = this.copy(retryStrategy = strategy) + private val virkailjaUrl = makeServiceConfig(config).virkailijaUrl + private val baseUrl = "/oppijanumerorekisteri-service" - private val oidServiceHttp = VirkailijaHttpClient(makeServiceConfig(config), baseUrl, true) + private val otuvaTokenEndpoint = config.getString("otuvaTokenEndpoint") + + private val oauth2clientFactory = new OtuvaOAuth2ClientFactory(OtuvaOAuth2Credentials.fromSecretsManager, otuvaTokenEndpoint) + + private val oidServiceHttp = oauth2clientFactory(virkailjaUrl, Http.retryingClient(baseUrl)) private val postRetryingOidServiceHttp = { // Osa POST-metodilla ONR:ään tehtävistä kyselyistä on oikeasti idempotentteja, @@ -43,14 +49,7 @@ case class OppijanumeroRekisteriClient( // esim. raportointoinkannan generointi jatkamaan, vaikka onr-yhteys hetken pätkisikin. val client = unsafeRetryingClient(baseUrl, retryStrategy.applyConfig, retryStrategy.backoffPolicy) - /*VirkailijaHttpClient( - makeServiceConfig(config), - baseUrl, - client, - preferGettingCredentialsFromSecretsManager = true - )*/ - // TODO - VirkailijaOAuth2Client() + oauth2clientFactory(virkailjaUrl, client) } private def makeServiceConfig(config: Config) = diff --git a/src/main/scala/fi/oph/koski/http/OtuvaOAuth2ClientFactory.scala b/src/main/scala/fi/oph/koski/http/OtuvaOAuth2ClientFactory.scala new file mode 100644 index 00000000000..ad87b3f40fe --- /dev/null +++ b/src/main/scala/fi/oph/koski/http/OtuvaOAuth2ClientFactory.scala @@ -0,0 +1,81 @@ +package fi.oph.koski.http + +import cats.effect.{IO, Resource} +import cats.effect.std.Hotswap +import fi.oph.koski.config.SecretsManager +import fi.oph.koski.log.NotLoggable +import fi.oph.scalaschema.Serializer.format +import org.http4s.{Header, Method, Request, Response, Uri, UrlForm} +import org.http4s.client.Client +import org.json4s.jackson.JsonMethods +import org.typelevel.ci.CIStringSyntax + +case class OtuvaOAuth2Credentials(id: String, secret: String) extends NotLoggable + +object OtuvaOAuth2Credentials { + def fromSecretsManager: OtuvaOAuth2Credentials = { + val cachedSecretsClient = new SecretsManager + val secretId = cachedSecretsClient.getSecretId("Otuva OAuth2 credentials", "OPINTOPOLKU_VIRKAILIJA_OAUTH2_SECRET_ID") + cachedSecretsClient.getStructuredSecret[OtuvaOAuth2Credentials](secretId) + } +} + +class OtuvaOAuth2ClientFactory( + credetials: OtuvaOAuth2Credentials, + otuvaTokenEndpoint: String, +) { + + private var token: Option[String] = None + private val otuvaTokenEndpointUri = Uri.fromString(otuvaTokenEndpoint).right.get + + def apply(serviceUrl: String, serviceClient: Client[IO]): Http = { + + def withOAuth2Token(req: Request[IO], hotswap: Hotswap[IO, Response[IO]]) = { + getToken().flatMap(requestWithToken(req, hotswap, retry = true)) + } + + def getToken(): IO[String] = { + token match { + case Some(s) => IO.pure(s) + case None => refreshToken() + } + } + + def requestWithToken(req: Request[IO], hotswap: Hotswap[IO, Response[IO]], retry: Boolean)(token: String): IO[Response[IO]] = { + val newReq = req.withHeaders(Header.Raw(ci"Authentication", "Bearer " + token)) + hotswap.swap(serviceClient.run(newReq)).flatMap { + // TODO: better expiry check + case r: Response[IO] if r.status.code != 200 => + refreshToken().flatMap(requestWithToken(req, hotswap, retry = false)) + case r: Response[IO] => IO.pure(r) + } + } + + def refreshToken(): IO[String] = { + val body = UrlForm("grant_type" -> "client_credentials", "client_id" -> credetials.id, "client_secret" -> credetials.secret) + val req = Request[IO](Method.POST, otuvaTokenEndpointUri).withEntity(body) + + serviceClient.run(req).use( + res => { + res.as[String].map { + s => + val v = JsonMethods.parse(s) + val t = (v \ "access_token").extract[String] + token = Some(t) + t + } + } + ) + } + + val client = Client[IO] { + req => + Hotswap.create[IO, Response[IO]].flatMap { hotswap => + Resource.eval(withOAuth2Token(req, hotswap)) + } + } + + Http(serviceUrl, client) + } + +} diff --git a/src/main/scala/fi/oph/koski/http/VirkailijaOAuth2Client.scala b/src/main/scala/fi/oph/koski/http/VirkailijaOAuth2Client.scala deleted file mode 100644 index 8b46dae8a1e..00000000000 --- a/src/main/scala/fi/oph/koski/http/VirkailijaOAuth2Client.scala +++ /dev/null @@ -1,66 +0,0 @@ -import cats.effect.{IO, Resource} -import cats.effect.std.Hotswap -import fi.oph.koski.log.NotLoggable -import fi.oph.scalaschema.Serializer.format -import org.http4s.{Header, Method, Request, Response, Uri, UrlForm} -import org.http4s.client.Client -import org.json4s.jackson.JsonMethods -import org.typelevel.ci.CIStringSyntax - -case class VirkailijaOAuth2Credentials(id: String, secret: String) extends NotLoggable - -class VirkailijaOAuth2Client(serviceClient: Client[IO], - credetials: VirkailijaOAuth2Credentials) { - - var token: Option[String] = None - // TODO get url from config - val otuvaAuthServerUrl = Uri.fromString("").right.get - - def withOAuth2Token(req: Request[IO], hotswap: Hotswap[IO, Response[IO]]) = { - getToken().flatMap(requestWithToken(req, hotswap, retry = true)) - } - - def getToken(): IO[String] = { - token match { - case Some(s) => IO.pure(s) - case None => refreshToken() - } - } - - def requestWithToken(req: Request[IO], hotswap: Hotswap[IO, Response[IO]], retry: Boolean)(token: String): IO[Response[IO]] = { - val newReq = req.withHeaders(Header.Raw(ci"Authentication", "Bearer " + token)) - hotswap.swap(serviceClient.run(newReq)).flatMap { - // TODO: better expiry check - case r: Response[IO] if r.status.code != 200 => - refreshToken().flatMap(requestWithToken(req, hotswap, retry = false)) - case r: Response[IO] => IO.pure(r) - } - } - - def refreshToken(): IO[String] = { - val body = UrlForm("grant_type" -> "client_credentials", "client_id" -> credetials.id, "client_secret" -> credetials.secret) - val req = Request[IO](Method.POST, otuvaAuthServerUrl).withEntity(body) - - serviceClient.run(req).use( - res => { - res.as[String].map { - s => - val v = JsonMethods.parse(s) - val t = (v \ "access_token").extract[String] - token = Some(t) - t - } - } - ) - } - - def apply(): Client[IO] = { - Client { - req => - Hotswap.create[IO, Response[IO]].flatMap { hotswap => - Resource.eval(withOAuth2Token(req, hotswap)) - } - } - } - -}