Skip to content

Commit

Permalink
Merge pull request #166 from mdsol/tech/MCC-1289760
Browse files Browse the repository at this point in the history
[MCC-1289760]: add mauth request sender for http4s
  • Loading branch information
mayman authored Jan 8, 2025
2 parents 91a1e0c + cddadca commit 90a3e2a
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 33 deletions.
10 changes: 7 additions & 3 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,19 @@ env:

jobs:
test_and_release:
runs-on: ubuntu-latest
runs-on: [ubuntu-latest]
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: coursier/cache-action@v6
- uses: coursier/setup-action@v1
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
jvm: temurin:11
java-version: '21'
distribution: 'temurin'
cache: 'sbt'
- uses: sbt/setup-sbt@v1
- name: Lint
run: sbt "scalafmtSbtCheck;scalafmtCheckAll"
- name: Test
Expand Down
10 changes: 10 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,15 @@ lazy val `mauth-sender-sttp-akka-http` = scalaModuleProject("mauth-sender-sttp-a
Dependencies.test(scalaMock, scalaTest, wiremock, sttpAkkaHttpBackend).map(withExclusions)
)

lazy val `mauth-sender-sttp-http4s-http` = scalaModuleProject("mauth-sender-sttp-http4s-http")
.dependsOn(`mauth-signer-sttp`, `mauth-test-utils` % "test")
.settings(
publishSettings,
libraryDependencies ++=
Dependencies.compile(catsEffect, scalaLibCompat, sttp, sttpFs2, scalaLogging).map(withExclusions) ++
Dependencies.test(scalaMock, http4sEmberClient, scalaTest, wiremock, sttpHttp4sHttpBackend).map(withExclusions)
)

lazy val `mauth-authenticator` = javaModuleProject("mauth-authenticator")
.dependsOn(`mauth-common`)
.settings(
Expand Down Expand Up @@ -212,6 +221,7 @@ lazy val `mauth-jvm-clients` = (project in file("."))
`mauth-signer-sttp`,
`mauth-signer-apachehttp`,
`mauth-sender-sttp-akka-http`,
`mauth-sender-sttp-http4s-http`,
`mauth-test-utils`,
`mauth-authenticator-http4s`
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.mdsol.mauth

import cats.effect.IO
import sttp.client3.{Request, Response, SttpBackend}
import sttp.capabilities.fs2.Fs2Streams

class SttpHttp4sMAuthRequestSender(
signer: MAuthSttpSigner,
sttpBackend: SttpBackend[IO, Fs2Streams[IO]]
) extends SttpMAuthRequestSender[IO] {
override def send[T](request: Request[T, Any]): IO[Response[T]] =
sttpBackend.send(signer.signSttpRequest(request))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.mdsol.mauth

import cats.effect.IO
import cats.effect.kernel.Resource
import cats.effect.unsafe.implicits.global
import com.github.tomakehurst.wiremock.WireMockServer
import com.github.tomakehurst.wiremock.client.WireMock._
import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig
import com.github.tomakehurst.wiremock.verification.LoggedRequest
import com.mdsol.mauth.test.utils.TestFixtures
import com.mdsol.mauth.test.utils.TestFixtures._
import com.mdsol.mauth.util.EpochTimeProvider
import com.mdsol.mauth.util.MAuthKeysHelper.getPrivateKeyFromString
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.scalatest.Inside._
import org.scalatest.matchers.should.Matchers._
import org.scalatest.wordspec.AsyncWordSpec
import org.scalatest.{BeforeAndAfter, BeforeAndAfterAll}
import sttp.client3.basicRequest
import sttp.client3.http4s.Http4sBackend
import sttp.model.{MediaType, Uri}

import java.net.URI
import java.security.Security
import java.util.UUID
import scala.jdk.CollectionConverters._

class SttpHttp4sMAuthRequestSenderSpec extends AsyncWordSpec with BeforeAndAfter with BeforeAndAfterAll {

Security.addProvider(new BouncyCastleProvider)

val wiremockServer: WireMockServer = new WireMockServer(wireMockConfig.dynamicPort())

lazy val requestSender: Resource[IO, SttpHttp4sMAuthRequestSender] = for {
sttpBackend <- Http4sBackend.usingDefaultEmberClientBuilder[IO]()
} yield new SttpHttp4sMAuthRequestSender(v1v2Signer, sttpBackend)

"correctly send auth signatures and content-type header" in {
val req = basicRequest
.get(Uri(new URI(s"${wiremockServer.baseUrl()}$REQUEST_NORMALIZE_PATH")))
.body("")
.contentType(MediaType.ApplicationJson)

requestSender
.use { case sttpSender =>
sttpSender.send(req).map { _ =>
inside(getRecordedRequests()) { case List(r) =>
r.getHeader("content-type") shouldBe "application/json"
r.getHeader(TIME_HEADER_V1) shouldBe EPOCH_TIME
r.getHeader(AUTH_HEADER_V1) shouldBe s"MWS $APP_UUID_V2:$SIGNATURE_NORMALIZE_PATH_V1"

r.getHeader(TIME_HEADER_V2) shouldBe EPOCH_TIME
r.getHeader(AUTH_HEADER_V2) shouldBe s"MWSV2 $APP_UUID_V2:$SIGNATURE_NORMALIZE_PATH_V2;"
}
}
}
.unsafeToFuture()
}

"sends a default content type (text/plain UTF-8) when content type not specified" in {
val req = basicRequest.get(Uri(new URI(s"${wiremockServer.baseUrl()}/"))).body("hello")

requestSender
.use { case sttpSender =>
sttpSender.send(req).map { _ =>
inside(getRecordedRequests()) { case List(r) =>
r.getHeader("content-type") shouldBe "text/plain; charset=UTF-8"
}
}
}
.unsafeToFuture()
}

private def getRecordedRequests(): List[LoggedRequest] =
wiremockServer.getAllServeEvents.asScala.toList.map(_.getRequest)

lazy val v1v2Signer: MAuthSttpSigner = {
val epochTimeProvider: EpochTimeProvider = () => EPOCH_TIME.toLong
new MAuthSttpSignerImpl(
UUID.fromString(APP_UUID_V2),
getPrivateKeyFromString(TestFixtures.PRIVATE_KEY_2),
epochTimeProvider,
SignerConfiguration.ALL_SIGN_VERSIONS
)
}

before {
wiremockServer.stubFor(
get(anyUrl())
.willReturn(aResponse().withStatus(200))
)
}

after {
wiremockServer.resetAll()
}

override protected def beforeAll(): Unit = {
super.beforeAll()
wiremockServer.start()
}

override protected def afterAll(): Unit = {
super.afterAll()
wiremockServer.stop()
}
}
4 changes: 2 additions & 2 deletions project/BuildSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import sbt._

object BuildSettings {
val env: util.Map[String, String] = System.getenv()
val scala212 = "2.12.17"
val scala213 = "2.13.14"
val scala212 = "2.12.20"
val scala213 = "2.13.15"

lazy val basicSettings = Seq(
homepage := Some(new URI("https://github.com/mdsol/mauth-jvm-clients").toURL),
Expand Down
59 changes: 31 additions & 28 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,44 @@ object Dependencies extends DependencyUtils {
val akka: String = "2.6.20" // Do not update beyond 2.6.* due to license changes
val akkaHttp: String = "10.2.10" // Do not update beyond 10.2.* due to license changes
val logback = "1.5.6"
val sttp = "3.9.7"
val http4s = "0.23.25"
val sttp = "3.10.2"
val http4s = "0.23.29"
val enumeratum = "1.7.4"
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
val akkaHttpCache: ModuleID = "com.typesafe.akka" %% "akka-http-caching" % Version.akkaHttp
val akkaStream: ModuleID = "com.typesafe.akka" %% "akka-stream" % Version.akka
val apacheHttpClient: ModuleID = "org.apache.httpcomponents" % "httpclient" % "4.5.14"
val bouncyCastlePkix: ModuleID = "org.bouncycastle" % "bcpkix-jdk18on" % "1.78.1"
val commonsCodec: ModuleID = "commons-codec" % "commons-codec" % "1.17.0"
val commonsLang3: ModuleID = "org.apache.commons" % "commons-lang3" % "3.14.0"
val guava: ModuleID = "com.google.guava" % "guava" % "31.1-jre"
val jacksonDataBind: ModuleID = "com.fasterxml.jackson.core" % "jackson-databind" % "2.17.0"
val logbackClassic: ModuleID = "ch.qos.logback" % "logback-classic" % Version.logback
val logbackCore: ModuleID = "ch.qos.logback" % "logback-core" % Version.logback
val slf4jApi: ModuleID = "org.slf4j" % "slf4j-api" % "2.0.12"
val typeSafeConfig: ModuleID = "com.typesafe" % "config" % "1.4.3"
val scalaCacheCore: ModuleID = "com.github.cb372" %% "scalacache-core" % "1.0.0-M6"
val scalaLogging: ModuleID = "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5"
val catsEffect: ModuleID = "org.typelevel" %% "cats-effect" % "3.4.8"
val sttp: ModuleID = "com.softwaremill.sttp.client3" %% "core" % Version.sttp
val sttpAkkaHttpBackend: ModuleID = "com.softwaremill.sttp.client3" %% "akka-http-backend" % Version.sttp
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
val akkaHttp: ModuleID = "com.typesafe.akka" %% "akka-http" % Version.akkaHttp
val akkaHttpCache: ModuleID = "com.typesafe.akka" %% "akka-http-caching" % Version.akkaHttp
val akkaStream: ModuleID = "com.typesafe.akka" %% "akka-stream" % Version.akka
val apacheHttpClient: ModuleID = "org.apache.httpcomponents" % "httpclient" % "4.5.14"
val bouncyCastlePkix: ModuleID = "org.bouncycastle" % "bcpkix-jdk18on" % "1.78.1"
val commonsCodec: ModuleID = "commons-codec" % "commons-codec" % "1.17.0"
val commonsLang3: ModuleID = "org.apache.commons" % "commons-lang3" % "3.14.0"
val guava: ModuleID = "com.google.guava" % "guava" % "31.1-jre"
val jacksonDataBind: ModuleID = "com.fasterxml.jackson.core" % "jackson-databind" % "2.17.0"
val logbackClassic: ModuleID = "ch.qos.logback" % "logback-classic" % Version.logback
val logbackCore: ModuleID = "ch.qos.logback" % "logback-core" % Version.logback
val slf4jApi: ModuleID = "org.slf4j" % "slf4j-api" % "2.0.12"
val typeSafeConfig: ModuleID = "com.typesafe" % "config" % "1.4.3"
val scalaCacheCore: ModuleID = "com.github.cb372" %% "scalacache-core" % "1.0.0-M6"
val scalaLogging: ModuleID = "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5"
val catsEffect: ModuleID = "org.typelevel" %% "cats-effect" % "3.4.8"
val sttp: ModuleID = "com.softwaremill.sttp.client3" %% "core" % Version.sttp
val sttpAkkaHttpBackend: ModuleID = "com.softwaremill.sttp.client3" %% "akka-http-backend" % Version.sttp
val sttpHttp4sHttpBackend: ModuleID = "com.softwaremill.sttp.client3" %% "http4s-backend" % Version.sttp
val sttpFs2: ModuleID = "com.softwaremill.sttp.shared" %% "fs2" % "1.4.2"
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 http4sEmberClient: ModuleID = "org.http4s" %% "http4s-ember-client" % Version.http4s
val enumeratum: ModuleID = "com.beachape" %% "enumeratum" % Version.enumeratum
val log4cats: ModuleID = "org.typelevel" %% "log4cats-slf4j" % Version.log4cats

lazy val circeBasic: Seq[ModuleID] = Seq(
"io.circe" %% "circe-core" % Version.circe,
Expand Down

0 comments on commit 90a3e2a

Please sign in to comment.