Skip to content

Commit

Permalink
Merge pull request #163 from mdsol/NOJIRA-add_signer_for_http4s_022
Browse files Browse the repository at this point in the history
NOJIRA add http4s-0.22 signer
  • Loading branch information
mayman authored Aug 27, 2024
2 parents cb262c7 + 2a4fe2c commit b3f8538
Show file tree
Hide file tree
Showing 9 changed files with 207 additions and 5 deletions.
19 changes: 16 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ lazy val `mauth-signer-sttp` = scalaModuleProject("mauth-signer-sttp")
Dependencies.test(scalaMock, scalaTest, wiremock, sttpAkkaHttpBackend).map(withExclusions)
)

lazy val `mauth-signer-http4s` = scalaModuleProject("mauth-signer-http4s")
lazy val `mauth-signer-http4s-023` = scalaModuleProject("mauth-signer-http4s-023")
.dependsOn(`mauth-signer`, `mauth-signer-scala-core`, `mauth-test-utils` % "test")
.settings(
basicSettings,
Expand All @@ -119,6 +119,18 @@ lazy val `mauth-signer-http4s` = scalaModuleProject("mauth-signer-http4s")
Dependencies.test(munitCatsEffect, http4sDsl)
)

lazy val `mauth-signer-http4s-022` = scalaModuleProject("mauth-signer-http4s-022")
.dependsOn(`mauth-signer`, `mauth-signer-scala-core`, `mauth-test-utils` % "test")
.settings(
basicSettings,
publishSettings,
testFrameworks += new TestFramework("munit.Framework"),
libraryDependencies ++=
Dependencies.provided(http4sClient022) ++
Dependencies.compile(enumeratum) ++
Dependencies.test(munitCatsEffect2, http4sDsl022)
)

// A separate module to sign and send sttp request using akka-http backend
// This keeps mauth-signer-sttp free of dependencies like akka and cats-effect in turn helps reduce dependency footprint
// of our client libraries (which will only need to depend on mauth-signer-sttp)
Expand Down Expand Up @@ -167,7 +179,7 @@ lazy val `mauth-authenticator-akka-http` = scalaModuleProject("mauth-authenticat
)

lazy val `mauth-authenticator-http4s` = (project in file("modules/mauth-authenticator-http4s")) // don't need to cross-compile
.dependsOn(`mauth-signer-http4s`, `mauth-authenticator-scala` % "test->test;compile->compile", `mauth-test-utils` % "test")
.dependsOn(`mauth-signer-http4s-023`, `mauth-authenticator-scala` % "test->test;compile->compile", `mauth-test-utils` % "test")
.settings(
basicSettings,
moduleName := "mauth-authenticator-http4s",
Expand Down Expand Up @@ -195,7 +207,8 @@ lazy val `mauth-jvm-clients` = (project in file("."))
`mauth-signer`,
`mauth-signer-akka-http`,
`mauth-signer-scala-core`,
`mauth-signer-http4s`,
`mauth-signer-http4s-023`,
`mauth-signer-http4s-022`,
`mauth-signer-sttp`,
`mauth-signer-apachehttp`,
`mauth-sender-sttp-akka-http`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.mdsol.mauth.http4s022.client

import cats.MonadThrow
import com.mdsol.mauth.models.SignedRequest
import org.http4s.headers.`Content-Type`
import org.http4s.{headers, Header, Headers, Method, Request, Uri}
import org.typelevel.ci.CIString
import cats.syntax.all._

import scala.annotation.nowarn
import scala.collection.immutable

object Implicits {

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

/** Create a http4s request from a [[models.SignedRequest]]
*/
def toHttp4sRequest[F[_]: MonadThrow]: F[Request[F]] = {
val contentType: Option[`Content-Type`] = extractContentTypeFromHeaders(signedRequest.req.headers)
val headersWithoutContentType: Map[String, String] = removeContentTypeFromHeaders(signedRequest.req.headers)

val allHeaders: immutable.Seq[Header.Raw] = (headersWithoutContentType ++ signedRequest.mauthHeaders).toList
.map { case (name, value) =>
Header.Raw(CIString(name), value)
}

for {
uri <- Uri.fromString(signedRequest.req.uri.toString).liftTo[F]
method <- Method.fromString(signedRequest.req.httpMethod).liftTo[F]
} yield Request[F](
method = method,
uri = uri,
body = fs2.Stream.emits(signedRequest.req.body),
headers = Headers(allHeaders)
).withContentTypeOption(contentType)
}

private def extractContentTypeFromHeaders(requestHeaders: Map[String, String]): Option[`Content-Type`] =
requestHeaders
.get(headers.`Content-Type`.toString)
.flatMap(str => `Content-Type`.parse(str).toOption)

@nowarn("msg=.*Unused import.*") // compat import only needed for 2.12
private def removeContentTypeFromHeaders(requestHeaders: Map[String, String]): Map[String, String] = {
import scala.collection.compat._
requestHeaders.view.filterKeys(_ != headers.`Content-Type`.toString).toMap
}
}

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

import cats.effect._
import cats.syntax.all._
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[_]: Sync](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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.mdsol.mauth.http4s022.client

import cats.effect.IO
import cats.syntax.all._
import com.mdsol.mauth.models.UnsignedRequest
import com.mdsol.mauth.{MAuthRequestSigner, MAuthVersion}
import munit.CatsEffectSuite
import org.http4s.client.Client
import org.http4s.{Headers, HttpRoutes, Request, Response, Status, Uri}
import org.http4s.dsl.io._
import com.mdsol.mauth.test.utils.TestFixtures._
import com.mdsol.mauth.util.EpochTimeProvider

import java.net.URI
import java.util.UUID

class MAuthSignerMiddlewareSuite extends CatsEffectSuite {

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

private 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)
)

private def route(headers: Map[String, String]) = HttpRoutes
.of[IO] { case req @ POST -> Root / "v1" / "test" =>
if (headers.forall(h => req.headers.headers.map(h => h.name.toString -> h.value).contains(h)))
Response[IO](Status.Ok).pure[IO]
else
Response[IO](Status.InternalServerError).pure[IO]
}
.orNotFound

test("correctly send a customized content-type header for v2") {

val simpleNewUnsignedRequest =
UnsignedRequest
.fromStringBodyUtf8(
httpMethod = "POST",
uri = new URI(s"/v1/test"),
body = "",
headers = Map("Content-Type" -> "application/json")
)

val signedReq = signerV2.signRequest(simpleNewUnsignedRequest)

val client = Client.fromHttpApp(route(signedReq.mauthHeaders ++ simpleNewUnsignedRequest.headers))

val mAuthedClient = MAuthSigner(signerV2)(client)

mAuthedClient
.status(
Request[IO](
method = POST,
uri = Uri.unsafeFromString(s"/v1/test"),
headers = Headers(signedReq.mauthHeaders.toList ++ List("Content-Type" -> "application/json"))
)
)
.assertEquals(Status.Ok)
}

test("correctly send a customized content-type header for v1") {

val simpleNewUnsignedRequest =
UnsignedRequest
.fromStringBodyUtf8(
httpMethod = "POST",
uri = new URI(s"/v1/test"),
body = "",
headers = Map("Content-Type" -> "application/json")
)

val signedReq = signerV1.signRequest(simpleNewUnsignedRequest)

val client = Client.fromHttpApp(route(signedReq.mauthHeaders ++ simpleNewUnsignedRequest.headers))

val mAuthedClient = MAuthSigner(signerV1)(client)

mAuthedClient
.status(
Request[IO](
method = POST,
uri = Uri.unsafeFromString(s"/v1/test"),
headers = Headers(signedReq.mauthHeaders.toList ++ List("Content-Type" -> "application/json"))
)
)
.assertEquals(Status.Ok)
}
}
4 changes: 2 additions & 2 deletions project/BuildSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ object BuildSettings {
val scala213 = "2.13.14"

lazy val basicSettings = Seq(
homepage := Some(new URL("https://github.com/mdsol/mauth-jvm-clients")),
homepage := Some(new URI("https://github.com/mdsol/mauth-jvm-clients").toURL),
organization := "com.mdsol",
organizationHomepage := Some(new URL("http://mdsol.com")),
organizationHomepage := Some(new URI("http://mdsol.com").toURL),
description := "MAuth clients",
scalaVersion := scala213,
resolvers += Resolver.mavenLocal,
Expand Down
4 changes: 4 additions & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ object Dependencies extends DependencyUtils {
val log4cats = "2.7.0"
val circe = "0.14.9"
val circeGenericExtras = "0.14.3"
val http4s022 = "0.22.15"
}

val akkaHttp: ModuleID = "com.typesafe.akka" %% "akka-http" % Version.akkaHttp
Expand All @@ -36,7 +37,9 @@ object Dependencies extends DependencyUtils {
val scalaLibCompat: ModuleID = "org.scala-lang.modules" %% "scala-collection-compat" % "2.12.0"
val caffeine: ModuleID = "com.github.ben-manes.caffeine" % "caffeine" % "3.1.8"
val http4sDsl: ModuleID = "org.http4s" %% "http4s-dsl" % Version.http4s
val http4sDsl022: ModuleID = "org.http4s" %% "http4s-dsl" % Version.http4s022
val http4sClient: ModuleID = "org.http4s" %% "http4s-client" % Version.http4s
val http4sClient022: ModuleID = "org.http4s" %% "http4s-client" % Version.http4s022
val enumeratum: ModuleID = "com.beachape" %% "enumeratum" % Version.enumeratum
val log4cats: ModuleID = "org.typelevel" %% "log4cats-slf4j" % Version.log4cats

Expand All @@ -58,6 +61,7 @@ object Dependencies extends DependencyUtils {
val scalaTest: ModuleID = "org.scalatest" %% "scalatest" % "3.2.19"
val wiremock: ModuleID = "com.github.tomakehurst" % "wiremock" % "2.27.2"
val munitCatsEffect: ModuleID = "org.typelevel" %% "munit-cats-effect-3" % "1.0.7"
val munitCatsEffect2: ModuleID = "org.typelevel" %% "munit-cats-effect-2" % "1.0.7"
val log4catsNoop: ModuleID = "org.typelevel" %% "log4cats-noop" % Version.log4cats
val scalaCacheCaffeine: ModuleID = "com.github.cb372" %% "scalacache-caffeine" % "1.0.0-M6"

Expand Down

0 comments on commit b3f8538

Please sign in to comment.