Skip to content

Commit

Permalink
Merge pull request #143 from mdsol/add-http4s-client-middleware
Browse files Browse the repository at this point in the history
Add http4s client middleware
  • Loading branch information
fserra-mdsol authored Aug 16, 2022
2 parents c2556d5 + cc0752d commit 05686d2
Show file tree
Hide file tree
Showing 14 changed files with 297 additions and 76 deletions.
41 changes: 34 additions & 7 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,22 @@ lazy val `mauth-signer-apachehttp` = javaModuleProject("mauth-signer-apachehttp"
Dependencies.test(scalaMock, scalaTest).map(withExclusions)
)

lazy val `mauth-signer-akka-http` = scalaModuleProject("mauth-signer-akka-http")
lazy val `mauth-signer-scala-core` = scalaModuleProject("mauth-signer-scala-core")
.dependsOn(`mauth-signer`, `mauth-test-utils` % "test")
.settings(
noPublishSettings,
moduleName := "mauth-signer-scala-core",
testFrameworks += new TestFramework("munit.Framework"),
libraryDependencies ++=
Dependencies.compile(akkaHttp, akkaStream).map(withExclusions) ++
Dependencies.compile(scalaLogging, scalaLibCompat).map(withExclusions) ++
//Dependencies.example(akkaHttp, akkaStream).map(withExclusions) ++
Dependencies.test(scalaMock, scalaTest, wiremock).map(withExclusions),
mimaPreviousArtifacts := Set.empty
)

lazy val `mauth-signer-akka-http` = scalaModuleProject("mauth-signer-akka-http")
.dependsOn(`mauth-signer`, `mauth-signer-scala-core`, `mauth-test-utils` % "test")
.configs(ExampleTests)
.settings(
exampleSettings,
Expand All @@ -88,13 +102,26 @@ lazy val `mauth-signer-akka-http` = scalaModuleProject("mauth-signer-akka-http")
)

lazy val `mauth-signer-sttp` = scalaModuleProject("mauth-signer-sttp")
.dependsOn(`mauth-signer`, `mauth-test-utils` % "test")
.dependsOn(`mauth-signer`, `mauth-signer-scala-core`, `mauth-test-utils` % "test")
.settings(
publishSettings,
libraryDependencies ++=
Dependencies.compile(scalaLibCompat, sttp, scalaLogging).map(withExclusions) ++
Dependencies.test(scalaMock, scalaTest, wiremock, sttpAkkaHttpBackend).map(withExclusions),
// TODO remove once published
Dependencies.test(scalaMock, scalaTest, wiremock, sttpAkkaHttpBackend).map(withExclusions)
)

lazy val `mauth-signer-http4s` = scalaModuleProject("mauth-signer-http4s")
.dependsOn(`mauth-signer`, `mauth-signer-scala-core`, `mauth-test-utils` % "test")
.settings(
basicSettings,
moduleName := "mauth-authenticator-http4s",
publishSettings,
testFrameworks += new TestFramework("munit.Framework"),
libraryDependencies ++=
Dependencies.provided(http4sClient) ++
Dependencies.compile(enumeratum) ++
Dependencies.compile(log4cats) ++
Dependencies.test(munitCatsEffect, http4sDsl),
mimaPreviousArtifacts := Set.empty
)

Expand Down Expand Up @@ -152,11 +179,10 @@ lazy val `mauth-authenticator-http4s` = (project in file("modules/mauth-authenti
publishSettings,
testFrameworks += new TestFramework("munit.Framework"),
libraryDependencies ++=
Dependencies.provided(http4s) ++
Dependencies.provided(http4sDsl) ++
Dependencies.compile(enumeratum) ++
Dependencies.compile(log4cats) ++
Dependencies.test(munitCatsEffect),
mimaPreviousArtifacts := Set.empty
Dependencies.test(munitCatsEffect)
)

lazy val `mauth-jvm-clients` = (project in file("."))
Expand All @@ -168,6 +194,7 @@ lazy val `mauth-jvm-clients` = (project in file("."))
`mauth-common`,
`mauth-signer`,
`mauth-signer-akka-http`,
`mauth-signer-http4s`,
`mauth-signer-sttp`,
`mauth-signer-apachehttp`,
`mauth-sender-sttp-akka-http`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.mdsol.mauth.akka.http

import java.net.URI
import java.security.Security

import akka.actor.ActorSystem
import com.mdsol.mauth.models.{SignedRequest, UnsignedRequest}
import com.mdsol.mauth.test.utils.{FakeMAuthServer, PortFinder}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ object HeaderVersion extends Enum[HeaderVersion] {

object MAuthMiddleware {
import HeaderVersion._
def apply[G[_]: Sync, F[_]](requestValidationTimeout: Duration, fk: F ~> G)(http: Http[G, F])(implicit
authenticator: Authenticator,
def apply[G[_]: Sync, F[_]](requestValidationTimeout: Duration, authenticator: Authenticator, fk: F ~> G)(http: Http[G, F])(implicit
ec: ExecutionContext,
F: Async[F]
): Http[G, F] =
Expand Down Expand Up @@ -107,13 +106,11 @@ object MAuthMiddleware {
}
}

def httpRoutes[F[_]: Async](requestValidationTimeout: Duration)(httpRoutes: HttpRoutes[F])(implicit
authenticator: Authenticator,
def httpRoutes[F[_]: Async](requestValidationTimeout: Duration, authenticator: Authenticator)(httpRoutes: HttpRoutes[F])(implicit
ec: ExecutionContext
): HttpRoutes[F] = apply(requestValidationTimeout, OptionT.liftK[F])(httpRoutes)
): HttpRoutes[F] = apply(requestValidationTimeout, authenticator, OptionT.liftK[F])(httpRoutes)

def httpApp[F[_]: Async](requestValidationTimeout: Duration)(httpRoutes: HttpApp[F])(implicit
authenticator: Authenticator,
def httpApp[F[_]: Async](requestValidationTimeout: Duration, authenticator: Authenticator)(httpRoutes: HttpApp[F])(implicit
ec: ExecutionContext
): HttpApp[F] = apply(requestValidationTimeout, FunctionK.id[F])(httpRoutes)
): HttpApp[F] = apply(requestValidationTimeout, authenticator, FunctionK.id[F])(httpRoutes)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import org.http4s.Method._

import java.security.{PublicKey, Security}
import java.util.UUID
import scala.concurrent.{ExecutionContext, Future}
import scala.concurrent.Future
import scala.concurrent.duration._

class MAuthMiddlewareSuite extends CatsEffectSuite {
Expand Down Expand Up @@ -75,11 +75,11 @@ class MAuthMiddlewareSuite extends CatsEffectSuite {

private implicit val authenticator: RequestAuthenticator = new RequestAuthenticator(client, epochTimeProvider)

private val service = MAuthMiddleware.httpRoutes[IO](requestValidationTimeout)(route).orNotFound
private val service = MAuthMiddleware.httpRoutes[IO](requestValidationTimeout, authenticator)(route).orNotFound

val authenticatorV2: RequestAuthenticator = new RequestAuthenticator(client, epochTimeProvider, v2OnlyAuthenticate = true)
val serviceV2 =
MAuthMiddleware.httpRoutes[IO](requestValidationTimeout)(route)(implicitly[Async[IO]], authenticatorV2, implicitly[ExecutionContext]).orNotFound
MAuthMiddleware.httpRoutes[IO](requestValidationTimeout, authenticatorV2)(route).orNotFound

test("allow successfully authenticated request") {
val res = service(
Expand Down Expand Up @@ -147,7 +147,7 @@ class MAuthMiddlewareSuite extends CatsEffectSuite {

val localAuthenticator: RequestAuthenticator = new RequestAuthenticator(localClient, epochTimeProvider)
val localService =
MAuthMiddleware.httpRoutes[IO](requestValidationTimeout)(route)(implicitly[Async[IO]], localAuthenticator, implicitly[ExecutionContext]).orNotFound
MAuthMiddleware.httpRoutes[IO](requestValidationTimeout, localAuthenticator)(route).orNotFound

val res = localService(
Request[IO](GET, uri"/").withHeaders(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.mdsol.mauth

import java.net.URI

import akka.actor.ActorSystem
import com.mdsol.mauth.http.HttpClient
import com.mdsol.mauth.http.Implicits._
Expand All @@ -26,7 +25,7 @@ object MauthRequestSignerExample {
val httpMethod = "GET"
val uri = URI.create("https://api.mdsol.com/v1/countries")

val signedRequest = MAuthRequestSigner(configuration).signRequest(models.UnsignedRequest(httpMethod, uri, body = Array.empty, headers = Map.empty))
val signedRequest = MAuthRequestSigner(configuration).signRequest(UnsignedRequest(httpMethod, uri, body = Array.empty, headers = Map.empty))
Await.result(
HttpClient.call(signedRequest.toAkkaHttpRequest).map(response => println(s"response code: ${response._1.value}, response: ${response._3.toString}")),
10.seconds
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.mdsol.mauth.http

import akka.http.scaladsl.model.headers.RawHeader
import akka.http.scaladsl.model.{HttpEntity, _}
import com.mdsol.mauth.SignedRequest
import akka.http.scaladsl.model._
import com.mdsol.mauth.http.HttpVerbOps._
import com.mdsol.mauth.SignedRequest
import com.mdsol.mauth.models.{SignedRequest => NewSignedRequest}

import scala.annotation.nowarn
Expand All @@ -29,7 +29,7 @@ object Implicits {

implicit class NewSignedRequestOps(val signedRequest: NewSignedRequest) extends AnyVal {

/** Create an akka-http request from a [[com.mdsol.mauth.models.SignedRequest]]
/** Create an akka-http request from a [[models.SignedRequest]]
*/
def toAkkaHttpRequest: HttpRequest = {
val contentType: Option[String] = extractContentTypeFromHeaders(signedRequest.req.headers)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.mdsol.mauth

import java.net.URI
import java.security.Security
import java.util.UUID

import akka.actor.ActorSystem
import akka.http.scaladsl.model._
import com.github.tomakehurst.wiremock.WireMockServer
import com.github.tomakehurst.wiremock.client.WireMock.{aResponse, equalTo, post, urlPathEqualTo}
import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig
import com.mdsol.mauth.http.HttpClient
import com.mdsol.mauth.http.Implicits._
import com.mdsol.mauth.models.{UnsignedRequest => NewUnsignedRequest}
import com.mdsol.mauth.test.utils.TestFixtures._
import com.mdsol.mauth.util.EpochTimeProvider
import org.apache.http.HttpStatus
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.scalatest.BeforeAndAfterAll
import org.scalatest.concurrent.PatienceConfiguration.Timeout
import org.scalatest.concurrent.ScalaFutures
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

import scala.concurrent.duration._

class MAuthRequestAkkaSignerSpec extends AnyFlatSpec with Matchers with HttpClient with BeforeAndAfterAll with ScalaFutures {

implicit val system: ActorSystem = ActorSystem()
var service = new WireMockServer(wireMockConfig.dynamicPort)

Security.addProvider(new BouncyCastleProvider)

val CONST_EPOCH_TIME_PROVIDER: EpochTimeProvider = new EpochTimeProvider() { override def inSeconds(): Long = EXPECTED_TIME_HEADER_1.toLong }

val signer: MAuthRequestSigner = new MAuthRequestSigner(
UUID.fromString(APP_UUID_1),
PRIVATE_KEY_1,
CONST_EPOCH_TIME_PROVIDER,
SignerConfiguration.ALL_SIGN_VERSIONS
)

val signerV2: MAuthRequestSigner = new MAuthRequestSigner(
UUID.fromString(APP_UUID_1),
PRIVATE_KEY_1,
CONST_EPOCH_TIME_PROVIDER,
java.util.Arrays.asList[MAuthVersion](MAuthVersion.MWSV2)
)

val signerV1: MAuthRequestSigner = new MAuthRequestSigner(
UUID.fromString(APP_UUID_1),
PRIVATE_KEY_1,
CONST_EPOCH_TIME_PROVIDER,
java.util.Arrays.asList[MAuthVersion](MAuthVersion.MWS)
)

override protected def beforeAll(): Unit =
service.start()

override protected def afterAll(): Unit =
service.stop()

val simpleUnsignedRequest: UnsignedRequest = UnsignedRequest(uri = URI_EMPTY_PATH)
val simpleNewUnsignedRequest: NewUnsignedRequest =
NewUnsignedRequest.fromStringBodyUtf8(httpMethod = "GET", uri = URI_EMPTY_PATH, body = "", headers = Map.empty)

val unsignedRequest: NewUnsignedRequest =
NewUnsignedRequest.fromStringBodyUtf8(httpMethod = "GET", uri = URI_EMPTY_PATH_WITH_PARAM, body = SIMPLE_REQUEST_BODY, headers = Map.empty)
"MAuthRequestSigner" should "add time header to a request for V1" in {
signer.signRequest(simpleUnsignedRequest).getOrElse(fail("signRequest unexpectedly failed")).timeHeader shouldBe EXPECTED_TIME_HEADER_1
}

it should "correctly send a customized content-type header" in {
service.stubFor(
post(urlPathEqualTo(s"/v1/test"))
.willReturn(aResponse().withStatus(HttpStatus.SC_OK))
.withHeader("Content-type", equalTo("application/json"))
)

val simpleNewUnsignedRequest =
NewUnsignedRequest
.fromStringBodyUtf8(
httpMethod = "POST",
uri = new URI(s"${service.baseUrl()}/v1/test"),
body = "",
headers = Map("Content-Type" -> ContentTypes.`application/json`.toString())
)

whenReady(HttpClient.call(signerV2.signRequest(simpleNewUnsignedRequest).toAkkaHttpRequest), timeout = Timeout(5.seconds)) { response =>
response.status shouldBe StatusCodes.OK
}
}

it should "correctly send the default content type (text/plain UTF-8) when content type not specified" in {
service.stubFor(
post(urlPathEqualTo(s"/v1/test"))
.willReturn(aResponse().withStatus(HttpStatus.SC_OK))
.withHeader("Content-type", equalTo(ContentTypes.`text/plain(UTF-8)`.toString()))
)

val simpleNewUnsignedRequest =
NewUnsignedRequest
.fromStringBodyUtf8(httpMethod = "POST", uri = new URI(s"${service.baseUrl()}/v1/test"), body = "", headers = Map())

whenReady(HttpClient.call(signerV2.signRequest(simpleNewUnsignedRequest).toAkkaHttpRequest), timeout = Timeout(5.seconds)) { response =>
response.status shouldBe StatusCodes.OK
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.mdsol.mauth.http4s.client

import cats.syntax.all._
import cats.effect.kernel.{Async, Resource}
import com.mdsol.mauth.RequestSigner
import com.mdsol.mauth.models.UnsignedRequest
import org.http4s.Request
import org.http4s.client.Client

import java.net.URI

object MAuthSigner {
def apply[F[_]: Async](signer: RequestSigner)(client: Client[F]): Client[F] =
Client { req =>
for {
req <- Resource.eval(req.as[Array[Byte]].flatMap { byteArray =>
val signedRequest = signer.signRequest(
UnsignedRequest(
req.method.name,
URI.create(req.uri.renderString),
byteArray,
req.headers.headers.view.map(h => h.name.toString -> h.value).toMap
)
)
Request(
method = req.method,
uri = req.uri,
headers = req.headers.put(signedRequest.mauthHeaders.toList),
body = req.body
).pure[F]
})
res <- client.run(req)
} yield res
}
}
Loading

0 comments on commit 05686d2

Please sign in to comment.