Skip to content

Commit 2a4fe2c

Browse files
author
Francesco Serra
committed
NOJIRA add http4s-0.22 signer
1 parent 62a8187 commit 2a4fe2c

File tree

9 files changed

+207
-5
lines changed

9 files changed

+207
-5
lines changed

build.sbt

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ lazy val `mauth-signer-sttp` = scalaModuleProject("mauth-signer-sttp")
106106
Dependencies.test(scalaMock, scalaTest, wiremock, sttpAkkaHttpBackend).map(withExclusions)
107107
)
108108

109-
lazy val `mauth-signer-http4s` = scalaModuleProject("mauth-signer-http4s")
109+
lazy val `mauth-signer-http4s-023` = scalaModuleProject("mauth-signer-http4s-023")
110110
.dependsOn(`mauth-signer`, `mauth-signer-scala-core`, `mauth-test-utils` % "test")
111111
.settings(
112112
basicSettings,
@@ -119,6 +119,18 @@ lazy val `mauth-signer-http4s` = scalaModuleProject("mauth-signer-http4s")
119119
Dependencies.test(munitCatsEffect, http4sDsl)
120120
)
121121

122+
lazy val `mauth-signer-http4s-022` = scalaModuleProject("mauth-signer-http4s-022")
123+
.dependsOn(`mauth-signer`, `mauth-signer-scala-core`, `mauth-test-utils` % "test")
124+
.settings(
125+
basicSettings,
126+
publishSettings,
127+
testFrameworks += new TestFramework("munit.Framework"),
128+
libraryDependencies ++=
129+
Dependencies.provided(http4sClient022) ++
130+
Dependencies.compile(enumeratum) ++
131+
Dependencies.test(munitCatsEffect2, http4sDsl022)
132+
)
133+
122134
// A separate module to sign and send sttp request using akka-http backend
123135
// This keeps mauth-signer-sttp free of dependencies like akka and cats-effect in turn helps reduce dependency footprint
124136
// of our client libraries (which will only need to depend on mauth-signer-sttp)
@@ -167,7 +179,7 @@ lazy val `mauth-authenticator-akka-http` = scalaModuleProject("mauth-authenticat
167179
)
168180

169181
lazy val `mauth-authenticator-http4s` = (project in file("modules/mauth-authenticator-http4s")) // don't need to cross-compile
170-
.dependsOn(`mauth-signer-http4s`, `mauth-authenticator-scala` % "test->test;compile->compile", `mauth-test-utils` % "test")
182+
.dependsOn(`mauth-signer-http4s-023`, `mauth-authenticator-scala` % "test->test;compile->compile", `mauth-test-utils` % "test")
171183
.settings(
172184
basicSettings,
173185
moduleName := "mauth-authenticator-http4s",
@@ -195,7 +207,8 @@ lazy val `mauth-jvm-clients` = (project in file("."))
195207
`mauth-signer`,
196208
`mauth-signer-akka-http`,
197209
`mauth-signer-scala-core`,
198-
`mauth-signer-http4s`,
210+
`mauth-signer-http4s-023`,
211+
`mauth-signer-http4s-022`,
199212
`mauth-signer-sttp`,
200213
`mauth-signer-apachehttp`,
201214
`mauth-sender-sttp-akka-http`,
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.mdsol.mauth.http4s022.client
2+
3+
import cats.MonadThrow
4+
import com.mdsol.mauth.models.SignedRequest
5+
import org.http4s.headers.`Content-Type`
6+
import org.http4s.{headers, Header, Headers, Method, Request, Uri}
7+
import org.typelevel.ci.CIString
8+
import cats.syntax.all._
9+
10+
import scala.annotation.nowarn
11+
import scala.collection.immutable
12+
13+
object Implicits {
14+
15+
implicit class NewSignedRequestOps(val signedRequest: SignedRequest) extends AnyVal {
16+
17+
/** Create a http4s request from a [[models.SignedRequest]]
18+
*/
19+
def toHttp4sRequest[F[_]: MonadThrow]: F[Request[F]] = {
20+
val contentType: Option[`Content-Type`] = extractContentTypeFromHeaders(signedRequest.req.headers)
21+
val headersWithoutContentType: Map[String, String] = removeContentTypeFromHeaders(signedRequest.req.headers)
22+
23+
val allHeaders: immutable.Seq[Header.Raw] = (headersWithoutContentType ++ signedRequest.mauthHeaders).toList
24+
.map { case (name, value) =>
25+
Header.Raw(CIString(name), value)
26+
}
27+
28+
for {
29+
uri <- Uri.fromString(signedRequest.req.uri.toString).liftTo[F]
30+
method <- Method.fromString(signedRequest.req.httpMethod).liftTo[F]
31+
} yield Request[F](
32+
method = method,
33+
uri = uri,
34+
body = fs2.Stream.emits(signedRequest.req.body),
35+
headers = Headers(allHeaders)
36+
).withContentTypeOption(contentType)
37+
}
38+
39+
private def extractContentTypeFromHeaders(requestHeaders: Map[String, String]): Option[`Content-Type`] =
40+
requestHeaders
41+
.get(headers.`Content-Type`.toString)
42+
.flatMap(str => `Content-Type`.parse(str).toOption)
43+
44+
@nowarn("msg=.*Unused import.*") // compat import only needed for 2.12
45+
private def removeContentTypeFromHeaders(requestHeaders: Map[String, String]): Map[String, String] = {
46+
import scala.collection.compat._
47+
requestHeaders.view.filterKeys(_ != headers.`Content-Type`.toString).toMap
48+
}
49+
}
50+
51+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.mdsol.mauth.http4s022.client
2+
3+
import cats.effect._
4+
import cats.syntax.all._
5+
import com.mdsol.mauth.RequestSigner
6+
import com.mdsol.mauth.models.UnsignedRequest
7+
import org.http4s.Request
8+
import org.http4s.client.Client
9+
10+
import java.net.URI
11+
12+
object MAuthSigner {
13+
def apply[F[_]: Sync](signer: RequestSigner)(client: Client[F]): Client[F] =
14+
Client { req =>
15+
for {
16+
req <- Resource.eval(req.as[Array[Byte]].flatMap { byteArray =>
17+
val signedRequest = signer.signRequest(
18+
UnsignedRequest(
19+
req.method.name,
20+
URI.create(req.uri.renderString),
21+
byteArray,
22+
req.headers.headers.view.map(h => h.name.toString -> h.value).toMap
23+
)
24+
)
25+
Request(
26+
method = req.method,
27+
uri = req.uri,
28+
headers = req.headers.put(signedRequest.mauthHeaders.toList),
29+
body = req.body
30+
).pure[F]
31+
})
32+
res <- client.run(req)
33+
} yield res
34+
}
35+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package com.mdsol.mauth.http4s022.client
2+
3+
import cats.effect.IO
4+
import cats.syntax.all._
5+
import com.mdsol.mauth.models.UnsignedRequest
6+
import com.mdsol.mauth.{MAuthRequestSigner, MAuthVersion}
7+
import munit.CatsEffectSuite
8+
import org.http4s.client.Client
9+
import org.http4s.{Headers, HttpRoutes, Request, Response, Status, Uri}
10+
import org.http4s.dsl.io._
11+
import com.mdsol.mauth.test.utils.TestFixtures._
12+
import com.mdsol.mauth.util.EpochTimeProvider
13+
14+
import java.net.URI
15+
import java.util.UUID
16+
17+
class MAuthSignerMiddlewareSuite extends CatsEffectSuite {
18+
19+
private val CONST_EPOCH_TIME_PROVIDER: EpochTimeProvider = new EpochTimeProvider() { override def inSeconds(): Long = EXPECTED_TIME_HEADER_1.toLong }
20+
21+
private val signerV2: MAuthRequestSigner = new MAuthRequestSigner(
22+
UUID.fromString(APP_UUID_1),
23+
PRIVATE_KEY_1,
24+
CONST_EPOCH_TIME_PROVIDER,
25+
java.util.Arrays.asList[MAuthVersion](MAuthVersion.MWSV2)
26+
)
27+
28+
val signerV1: MAuthRequestSigner = new MAuthRequestSigner(
29+
UUID.fromString(APP_UUID_1),
30+
PRIVATE_KEY_1,
31+
CONST_EPOCH_TIME_PROVIDER,
32+
java.util.Arrays.asList[MAuthVersion](MAuthVersion.MWS)
33+
)
34+
35+
private def route(headers: Map[String, String]) = HttpRoutes
36+
.of[IO] { case req @ POST -> Root / "v1" / "test" =>
37+
if (headers.forall(h => req.headers.headers.map(h => h.name.toString -> h.value).contains(h)))
38+
Response[IO](Status.Ok).pure[IO]
39+
else
40+
Response[IO](Status.InternalServerError).pure[IO]
41+
}
42+
.orNotFound
43+
44+
test("correctly send a customized content-type header for v2") {
45+
46+
val simpleNewUnsignedRequest =
47+
UnsignedRequest
48+
.fromStringBodyUtf8(
49+
httpMethod = "POST",
50+
uri = new URI(s"/v1/test"),
51+
body = "",
52+
headers = Map("Content-Type" -> "application/json")
53+
)
54+
55+
val signedReq = signerV2.signRequest(simpleNewUnsignedRequest)
56+
57+
val client = Client.fromHttpApp(route(signedReq.mauthHeaders ++ simpleNewUnsignedRequest.headers))
58+
59+
val mAuthedClient = MAuthSigner(signerV2)(client)
60+
61+
mAuthedClient
62+
.status(
63+
Request[IO](
64+
method = POST,
65+
uri = Uri.unsafeFromString(s"/v1/test"),
66+
headers = Headers(signedReq.mauthHeaders.toList ++ List("Content-Type" -> "application/json"))
67+
)
68+
)
69+
.assertEquals(Status.Ok)
70+
}
71+
72+
test("correctly send a customized content-type header for v1") {
73+
74+
val simpleNewUnsignedRequest =
75+
UnsignedRequest
76+
.fromStringBodyUtf8(
77+
httpMethod = "POST",
78+
uri = new URI(s"/v1/test"),
79+
body = "",
80+
headers = Map("Content-Type" -> "application/json")
81+
)
82+
83+
val signedReq = signerV1.signRequest(simpleNewUnsignedRequest)
84+
85+
val client = Client.fromHttpApp(route(signedReq.mauthHeaders ++ simpleNewUnsignedRequest.headers))
86+
87+
val mAuthedClient = MAuthSigner(signerV1)(client)
88+
89+
mAuthedClient
90+
.status(
91+
Request[IO](
92+
method = POST,
93+
uri = Uri.unsafeFromString(s"/v1/test"),
94+
headers = Headers(signedReq.mauthHeaders.toList ++ List("Content-Type" -> "application/json"))
95+
)
96+
)
97+
.assertEquals(Status.Ok)
98+
}
99+
}

project/BuildSettings.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ object BuildSettings {
99
val scala213 = "2.13.14"
1010

1111
lazy val basicSettings = Seq(
12-
homepage := Some(new URL("https://github.com/mdsol/mauth-jvm-clients")),
12+
homepage := Some(new URI("https://github.com/mdsol/mauth-jvm-clients").toURL),
1313
organization := "com.mdsol",
14-
organizationHomepage := Some(new URL("http://mdsol.com")),
14+
organizationHomepage := Some(new URI("http://mdsol.com").toURL),
1515
description := "MAuth clients",
1616
scalaVersion := scala213,
1717
resolvers += Resolver.mavenLocal,

project/Dependencies.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ object Dependencies extends DependencyUtils {
1212
val log4cats = "2.5.0"
1313
val circe = "0.14.6"
1414
val circeGenericExtras = "0.14.3"
15+
val http4s022 = "0.22.15"
1516
}
1617

1718
val akkaHttp: ModuleID = "com.typesafe.akka" %% "akka-http" % Version.akkaHttp
@@ -35,7 +36,9 @@ object Dependencies extends DependencyUtils {
3536
val scalaLibCompat: ModuleID = "org.scala-lang.modules" %% "scala-collection-compat" % "2.8.1"
3637
val caffeine: ModuleID = "com.github.ben-manes.caffeine" % "caffeine" % "3.1.5"
3738
val http4sDsl: ModuleID = "org.http4s" %% "http4s-dsl" % Version.http4s
39+
val http4sDsl022: ModuleID = "org.http4s" %% "http4s-dsl" % Version.http4s022
3840
val http4sClient: ModuleID = "org.http4s" %% "http4s-client" % Version.http4s
41+
val http4sClient022: ModuleID = "org.http4s" %% "http4s-client" % Version.http4s022
3942
val enumeratum: ModuleID = "com.beachape" %% "enumeratum" % Version.enumeratum
4043
val log4cats: ModuleID = "org.typelevel" %% "log4cats-slf4j" % Version.log4cats
4144

@@ -57,6 +60,7 @@ object Dependencies extends DependencyUtils {
5760
val scalaTest: ModuleID = "org.scalatest" %% "scalatest" % "3.2.14"
5861
val wiremock: ModuleID = "com.github.tomakehurst" % "wiremock" % "2.27.2"
5962
val munitCatsEffect: ModuleID = "org.typelevel" %% "munit-cats-effect-3" % "1.0.7"
63+
val munitCatsEffect2: ModuleID = "org.typelevel" %% "munit-cats-effect-2" % "1.0.7"
6064
val log4catsNoop: ModuleID = "org.typelevel" %% "log4cats-noop" % Version.log4cats
6165
val scalaCacheCaffeine: ModuleID = "com.github.cb372" %% "scalacache-caffeine" % "1.0.0-M6"
6266

0 commit comments

Comments
 (0)