Skip to content

Commit

Permalink
Merge pull request #156 from mdsol/tech/fix_request_cache
Browse files Browse the repository at this point in the history
Tech: Fix Cache to reduce requests.
  • Loading branch information
jatcwang authored Jul 17, 2023
2 parents ce4e4fd + df32f8a commit f049d9e
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 93 deletions.
4 changes: 2 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,8 @@ lazy val `mauth-authenticator-akka-http` = scalaModuleProject("mauth-authenticat
.settings(
publishSettings,
libraryDependencies ++=
Dependencies.provided(akkaHttp, akkaStream) ++
Dependencies.compile(jacksonDataBind, scalaCacheCaffeine).map(withExclusions) ++
Dependencies.provided(akkaHttp, akkaStream, akkaHttpCache) ++
Dependencies.compile(jacksonDataBind).map(withExclusions) ++
Dependencies.test(scalaTest, scalaMock, wiremock) ++
Dependencies.test(akkaHttpTestKit *).map(withExclusions)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,25 @@
package com.mdsol.mauth.akka.http

import akka.actor.ActorSystem
import akka.http.caching.scaladsl.CachingSettings
import akka.http.caching.LfuCache
import akka.http.scaladsl.model.{HttpResponse, StatusCodes}
import akka.http.scaladsl.unmarshalling.Unmarshal
import akka.stream.Materializer
import cats.effect.IO
import cats.effect.unsafe.implicits.global
import com.fasterxml.jackson.databind.ObjectMapper
import com.github.benmanes.caffeine.cache.Caffeine
import com.mdsol.mauth.http.HttpClient
import com.mdsol.mauth.http.Implicits._
import com.mdsol.mauth.models.UnsignedRequest
import com.mdsol.mauth.scaladsl.utils.ClientPublicKeyProvider
import com.mdsol.mauth.util.MAuthKeysHelper
import com.mdsol.mauth.{AuthenticatorConfiguration, MAuthRequestSigner}
import com.typesafe.scalalogging.StrictLogging
import scalacache.caffeine.CaffeineCache
import scalacache.memoization._
import scalacache.{Cache, Entry}

import java.net.URI
import java.security.PublicKey
import java.util.UUID
import scala.concurrent.duration._
import scala.concurrent.duration.DurationLong
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}

Expand All @@ -33,49 +30,54 @@ class MauthPublicKeyProvider(configuration: AuthenticatorConfiguration, signer:
) extends ClientPublicKeyProvider[Future]
with StrictLogging {

private val cCache = Caffeine.newBuilder().build[String, Entry[Option[PublicKey]]]()
implicit val caffeineCache: Cache[IO, String, Option[PublicKey]] = CaffeineCache[IO, String, Option[PublicKey]](underlying = cCache)
protected val mapper = new ObjectMapper

private val defaultCachingSettings = CachingSettings(system)
private val lfuCacheSettings = defaultCachingSettings.lfuCacheSettings.withTimeToLive(configuration.getTimeToLive.seconds)
private val cache = LfuCache.apply[UUID, Option[PublicKey]](defaultCachingSettings.withLfuCacheSettings(lfuCacheSettings))

/** Returns the associated public key for a given application UUID.
*
* @param appUUID , UUID of the application for which we want to retrieve its public key.
* @return { @link PublicKey} registered in MAuth for the application with given appUUID.
*/
override def getPublicKey(appUUID: UUID): Future[Option[PublicKey]] =
getPublicKeyIO(appUUID).unsafeToFuture()
cache.getOrLoad(
appUUID,
_ => {
val signedRequest =
signer.signRequest(UnsignedRequest.noBody("GET", new URI(configuration.getBaseUrl + getRequestUrlPath(appUUID)), headers = Map.empty))
retrievePublicKey()(HttpClient.call(signedRequest.toAkkaHttpRequest))

def getPublicKeyIO(appUUID: UUID): IO[Option[PublicKey]] = memoizeF(Some(configuration.getTimeToLive.seconds)) {
val signedRequest = signer.signRequest(UnsignedRequest.noBody("GET", new URI(configuration.getBaseUrl + getRequestUrlPath(appUUID)), headers = Map.empty))
retrievePublicKey()(IO.fromFuture(IO(HttpClient.call(signedRequest.toAkkaHttpRequest))))
}
}
)

protected def retrievePublicKey()(mauthPublicKeyFetcher: => IO[HttpResponse]): IO[Option[PublicKey]] = {
def getPublicKeyIO(appUUID: UUID): IO[Option[PublicKey]] = IO.fromFuture(IO(getPublicKey(appUUID)))

protected def retrievePublicKey()(mauthPublicKeyFetcher: => Future[HttpResponse]): Future[Option[PublicKey]] = {
mauthPublicKeyFetcher
.flatMap { response =>
IO.fromFuture(
IO(
Unmarshal(response.entity)
.to[String]
)
).map { body =>
if (response.status == StatusCodes.OK) {
Try(MAuthKeysHelper.getPublicKeyFromString(mapper.readTree(body).findValue("public_key_str").asText)) match {
case Success(publicKey) => Some(publicKey)
case Failure(error) =>
logger.error("Converting string to Public Key failed", error)
None
Unmarshal(response.entity)
.to[String]
.map { body =>
if (response.status == StatusCodes.OK) {
Try(MAuthKeysHelper.getPublicKeyFromString(mapper.readTree(body).findValue("public_key_str").asText)) match {
case Success(publicKey) => Some(publicKey)
case Failure(error) =>
logger.error("Converting string to Public Key failed", error)
None
}
} else {
logger.error(s"Unexpected response returned by server -- status: ${response.status} response: $body")
None
}
} else {
logger.error(s"Unexpected response returned by server -- status: ${response.status} response: $body")
}
.recover[Option[PublicKey]] { case error: Throwable =>
logger.error("Request to get MAuth public key couldn't be signed", error)
None
}
}.handleError { error: Throwable =>
logger.error("Request to get MAuth public key couldn't be signed", error)
None
}
}
.handleError { error: Throwable =>
.recover[Option[PublicKey]] { case error: Throwable =>
logger.error("Request to get MAuth public key couldn't be completed", error)
None
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,30 @@ package com.mdsol.mauth.akka.http
import java.net.URI
import java.security.Security
import akka.actor.ActorSystem
import cats.effect.unsafe.IORuntime
import com.mdsol.mauth.models.{SignedRequest, UnsignedRequest}
import com.mdsol.mauth.test.utils.{FakeMAuthServer, PortFinder}
import com.mdsol.mauth.{AuthenticatorConfiguration, MAuthRequest, MAuthRequestSigner}
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.scalamock.scalatest.MockFactory
import org.scalatest.concurrent.{IntegrationPatience, ScalaFutures}
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}

import scala.concurrent.ExecutionContext.Implicits.global
import cats.implicits._
import org.scalatest.wordspec.AnyWordSpec

class MauthPublicKeyProviderSpec
extends AnyFlatSpec
extends AnyWordSpec
with BeforeAndAfterAll
with BeforeAndAfterEach
with ScalaFutures
with IntegrationPatience
with Matchers
with MockFactory {

implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global
implicit val ioRuntime: IORuntime = cats.effect.unsafe.implicits.global

implicit val system: ActorSystem = ActorSystem()
private val EXPECTED_TIME_HEADER_VALUE = "1444672125"
private val EXPECTED_AUTHENTICATION_HEADER_VALUE = "MWS 92a1869e-c80d-4f06-8775-6c4ebb0758e0:lTMYNWPaG4..."
Expand Down Expand Up @@ -53,48 +56,82 @@ class MauthPublicKeyProviderSpec

private def getMAuthConfiguration = new AuthenticatorConfiguration(MAUTH_BASE_URL, MAUTH_URL_PATH, SECURITY_TOKENS_PATH)

"MauthPublicKeyProvider" should "retrieve PublicKey from MAuth Server" in {
FakeMAuthServer.return200()
val mockedSigner = mock[MAuthRequestSigner]
val unsignedRequest = UnsignedRequest(
"GET",
URI.create(MAUTH_BASE_URL + getRequestUrlPath(FakeMAuthServer.EXISTING_CLIENT_APP_UUID.toString)),
body = Array.empty,
headers = Map.empty
)
val mockedResponse = SignedRequest(
unsignedRequest,
mauthHeaders = Map(
"not_testing_signer_behaviour" -> "So any header is ok"
"MauthPublicKeyProvider" should {
"retrieve PublicKey from MAuth Server" in {
FakeMAuthServer.return200()
val mockedSigner = mock[MAuthRequestSigner]
val unsignedRequest = UnsignedRequest(
"GET",
URI.create(MAUTH_BASE_URL + getRequestUrlPath(FakeMAuthServer.EXISTING_CLIENT_APP_UUID.toString)),
body = Array.empty,
headers = Map.empty
)
val mockedResponse = SignedRequest(
unsignedRequest,
mauthHeaders = Map(
"not_testing_signer_behaviour" -> "So any header is ok"
)
)
)
(mockedSigner.signRequest(_: UnsignedRequest)).expects(*).returns(mockedResponse)
(mockedSigner.signRequest(_: UnsignedRequest)).expects(*).returns(mockedResponse)

whenReady(new MauthPublicKeyProvider(getMAuthConfiguration, mockedSigner).getPublicKey(FakeMAuthServer.EXISTING_CLIENT_APP_UUID)) { result =>
result.toString should not be empty
whenReady(new MauthPublicKeyProvider(getMAuthConfiguration, mockedSigner).getPublicKey(FakeMAuthServer.EXISTING_CLIENT_APP_UUID)) { result =>
result.toString should not be empty
}
}
}
"only make one call to mAuth when multiple calls happen in parallel and the cache is empty" in {
FakeMAuthServer.return200()
val mockedSigner = mock[MAuthRequestSigner]
val unsignedRequest = UnsignedRequest(
"GET",
URI.create(MAUTH_BASE_URL + getRequestUrlPath(FakeMAuthServer.EXISTING_CLIENT_APP_UUID.toString)),
body = Array.empty,
headers = Map.empty
)
val mockedResponse = SignedRequest(
unsignedRequest,
mauthHeaders = Map(
"not_testing_signer_behaviour" -> "So any header is ok"
)
)
(mockedSigner.signRequest(_: UnsignedRequest)).expects(*).returns(mockedResponse).anyNumberOfTimes() //No?

it should "fail on invalid response from MAuth Server" in {
FakeMAuthServer.return401()
val mockedSigner = mock[MAuthRequestSigner]
val unsignedRequest = UnsignedRequest(
"GET",
URI.create(MAUTH_BASE_URL + getRequestUrlPath(FakeMAuthServer.NON_EXISTING_CLIENT_APP_UUID.toString)),
body = Array.empty,
headers = Map.empty
)
val mockedResponse = SignedRequest(
unsignedRequest,
mauthHeaders = Map(
"not_testing_signer_behaviour" -> "So any header is ok"
val provider = new MauthPublicKeyProvider(getMAuthConfiguration, mockedSigner)
FakeMAuthServer.verifyNumberOfRequests(1)

whenReady(
List
.fill(100)(provider.getPublicKey(FakeMAuthServer.EXISTING_CLIENT_APP_UUID))
.sequence
) {
_.map { result =>
result.toString should not be empty
}
}

FakeMAuthServer.verifyNumberOfRequests(2)

}
"fail on invalid response from MAuth Server" in {
FakeMAuthServer.return401()
val mockedSigner = mock[MAuthRequestSigner]
val unsignedRequest = UnsignedRequest(
"GET",
URI.create(MAUTH_BASE_URL + getRequestUrlPath(FakeMAuthServer.NON_EXISTING_CLIENT_APP_UUID.toString)),
body = Array.empty,
headers = Map.empty
)
val mockedResponse = SignedRequest(
unsignedRequest,
mauthHeaders = Map(
"not_testing_signer_behaviour" -> "So any header is ok"
)
)
)
(mockedSigner.signRequest(_: UnsignedRequest)).expects(*).returns(mockedResponse)
(mockedSigner.signRequest(_: UnsignedRequest)).expects(*).returns(mockedResponse)

whenReady(new MauthPublicKeyProvider(getMAuthConfiguration, mockedSigner).getPublicKey(FakeMAuthServer.EXISTING_CLIENT_APP_UUID)) {
case Some(_) => fail("returned a public key, expected None")
case None =>
whenReady(new MauthPublicKeyProvider(getMAuthConfiguration, mockedSigner).getPublicKey(FakeMAuthServer.EXISTING_CLIENT_APP_UUID)) {
case Some(_) => fail("returned a public key, expected None")
case None =>
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
package com.mdsol.mauth.http4s

import cats.ApplicativeThrow
import cats.effect.{Concurrent, Sync}
import cats.effect.{Async, Outcome, Sync}
import com.mdsol.mauth.http4s.client.Implicits.NewSignedRequestOps
import com.mdsol.mauth.models.UnsignedRequest
import com.mdsol.mauth.scaladsl.utils.ClientPublicKeyProvider
import com.mdsol.mauth.util.MAuthKeysHelper
import com.mdsol.mauth.{AuthenticatorConfiguration, MAuthRequestSigner}
import org.http4s.client.Client
import org.http4s.{Response, Status}
import scalacache.memoization.memoizeF
import scalacache.{Cache, Entry}
import scalacache.caffeine.CaffeineCache

Expand All @@ -23,23 +22,39 @@ import com.mdsol.mauth.http4s.MauthPublicKeyProvider.SecurityToken
import io.circe.{Decoder, HCursor}
import org.http4s.circe.CirceEntityDecoder._
import org.typelevel.log4cats.Logger
import cats.effect.implicits._

class MauthPublicKeyProvider[F[_]: Concurrent: Logger](configuration: AuthenticatorConfiguration, signer: MAuthRequestSigner, val client: Client[F])(implicit
val cache: Cache[F, String, Option[PublicKey]]
class MauthPublicKeyProvider[F[_]: Async: Logger](configuration: AuthenticatorConfiguration, signer: MAuthRequestSigner, val client: Client[F])(implicit
val cache: Cache[F, UUID, F[Option[PublicKey]]]
) extends ClientPublicKeyProvider[F] {

/** Returns the associated public key for a given application UUID.
*
* @param appUUID , UUID of the application for which we want to retrieve its public key.
* @return { @link PublicKey} registered in MAuth for the application with given appUUID.
*/
override def getPublicKey(appUUID: UUID): F[Option[PublicKey]] = memoizeF(Some(configuration.getTimeToLive.seconds)) {
val uri = new URI(configuration.getBaseUrl + getRequestUrlPath(appUUID))
val signedRequest = signer.signRequest(UnsignedRequest.noBody("GET", uri, headers = Map.empty))
signedRequest
.toHttp4sRequest[F]
.flatMap(req => client.run(req).use(retrievePublicKey))
}
override def getPublicKey(appUUID: UUID): F[Option[PublicKey]] = cache
.cachingF(appUUID)(Some(configuration.getTimeToLive.seconds)) {
Sync[F]
.defer {
val uri = new URI(configuration.getBaseUrl + getRequestUrlPath(appUUID))
val signedRequest = signer.signRequest(UnsignedRequest.noBody("GET", uri, headers = Map.empty))
signedRequest
.toHttp4sRequest[F]
.flatMap(req => client.run(req).use(retrievePublicKey))
}
.guaranteeCase {
case Outcome.Succeeded(res) =>
res.flatMap {
case Some(_) => Async[F].unit
case None => cache.remove(appUUID)
}
case _ => cache.remove(appUUID)
}
.memoize
}
.flatten

private def retrievePublicKey(mauthPublicKeyFetcher: Response[F]): F[Option[PublicKey]] = {
mauthPublicKeyFetcher.status match {
case Status.Ok =>
Expand Down Expand Up @@ -82,9 +97,9 @@ object MauthPublicKeyProvider {

// this provides a default implementation of the cache to be used with the public key provider, and frees the user to
// inject their own cache
implicit def defaultCache[F[_]: Sync]: Cache[F, String, Option[PublicKey]] =
CaffeineCache[F, String, Option[PublicKey]](
Caffeine.newBuilder().build[String, Entry[Option[PublicKey]]]()
implicit def defaultCache[F[_]: Sync]: Cache[F, UUID, Option[PublicKey]] =
CaffeineCache[F, UUID, Option[PublicKey]](
Caffeine.newBuilder().build[UUID, Entry[Option[PublicKey]]]()
)

}
Loading

0 comments on commit f049d9e

Please sign in to comment.