Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tech/try scala 3 cross compile #149

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def scalaModuleProject(modName: String): Project = {
.settings(
basicSettings,
moduleName := modName,
crossScalaVersions := Seq(scala212, scala213)
crossScalaVersions := Seq(scala212, scala213, scala31)
)
}

Expand Down Expand Up @@ -174,16 +174,18 @@ 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-authenticator-scala`, `mauth-test-utils` % "test")
.dependsOn(`mauth-signer-http4s`, `mauth-authenticator-scala`, `mauth-test-utils` % "test")
.settings(
basicSettings,
moduleName := "mauth-authenticator-http4s",
publishSettings,
testFrameworks += new TestFramework("munit.Framework"),
libraryDependencies ++=
Dependencies.provided(http4sDsl) ++
Dependencies.provided(http4sClient) ++
Dependencies.compile(enumeratum) ++
Dependencies.compile(log4cats) ++
Dependencies.compile(jacksonDataBind, scalaCache) ++
Dependencies.test(munitCatsEffect)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.mdsol.mauth.http.{`X-MWS-Authentication`, `X-MWS-Time`, HttpVerbOps}
import com.mdsol.mauth.scaladsl.Authenticator
import com.typesafe.scalalogging.StrictLogging

import scala.concurrent.Future
import scala.concurrent.duration.{Duration, FiniteDuration}
import scala.util.control.NonFatal
import scala.util.{Success, Try}
Expand Down Expand Up @@ -42,45 +43,43 @@ trait MAuthDirectives extends StrictLogging {
* @param requestValidationTimeout request validation timeout duration, defaults to 10 seconds
* @return Directive to authenticate the request
*/
def authenticate(implicit authenticator: Authenticator, timeout: FiniteDuration, requestValidationTimeout: Duration): Directive0 = {
extractExecutionContext.flatMap { implicit ec =>
extractLatestAuthenticationHeaders(authenticator.isV2OnlyAuthenticate).flatMap { mauthHeaderValues: MauthHeaderValues =>
toStrictEntity(timeout) &
extractRequest.flatMap { req =>
val isAuthed: Directive[Unit] = req.entity match {
case entity: HttpEntity.Strict =>
val mAuthRequest: MAuthRequest = new MAuthRequest(
mauthHeaderValues.authenticator,
entity.data.toArray[Byte],
HttpVerbOps.httpVerb(req.method),
mauthHeaderValues.time.toString,
req.uri.path.toString,
getQueryString(req)
)
if (!authenticator.isV2OnlyAuthenticate) {
// store V1 headers for fallback to V1 authentication if V2 failed
val xmwsAuthenticationHeader = extractRequestHeader(req, MAuthRequest.X_MWS_AUTHENTICATION_HEADER_NAME)
val xmwsTimeHeader = extractRequestHeader(req, MAuthRequest.X_MWS_TIME_HEADER_NAME)
if (xmwsAuthenticationHeader.nonEmpty && xmwsTimeHeader.nonEmpty) {
mAuthRequest.setXmwsSignature(xmwsAuthenticationHeader)
mAuthRequest.setXmwsTime(xmwsTimeHeader)
}
def authenticate(implicit authenticator: Authenticator[Future], timeout: FiniteDuration, requestValidationTimeout: Duration): Directive0 = {
extractLatestAuthenticationHeaders(authenticator.isV2OnlyAuthenticate).flatMap { (mauthHeaderValues: MauthHeaderValues) =>
toStrictEntity(timeout) &
extractRequest.flatMap { req =>
val isAuthed: Directive[Unit] = req.entity match {
case entity: HttpEntity.Strict =>
val mAuthRequest: MAuthRequest = new MAuthRequest(
mauthHeaderValues.authenticator,
entity.data.toArray[Byte],
HttpVerbOps.httpVerb(req.method),
mauthHeaderValues.time.toString,
req.uri.path.toString,
getQueryString(req)
)
if (!authenticator.isV2OnlyAuthenticate) {
// store V1 headers for fallback to V1 authentication if V2 failed
val xmwsAuthenticationHeader = extractRequestHeader(req, MAuthRequest.X_MWS_AUTHENTICATION_HEADER_NAME)
val xmwsTimeHeader = extractRequestHeader(req, MAuthRequest.X_MWS_TIME_HEADER_NAME)
if (xmwsAuthenticationHeader.nonEmpty && xmwsTimeHeader.nonEmpty) {
mAuthRequest.setXmwsSignature(xmwsAuthenticationHeader)
mAuthRequest.setXmwsTime(xmwsTimeHeader)
}
onComplete(
authenticator.authenticate(
mAuthRequest
)(ec, requestValidationTimeout)
).flatMap[Unit] {
case Success(true) => pass
case _ => reject(MdsolAuthFailedRejection)
}
case _ =>
logger.error(s"MAUTH: Non-Strict Entity in Request")
reject(MdsolAuthFailedRejection)
}
isAuthed
}
onComplete(
authenticator.authenticate(
mAuthRequest
)(requestValidationTimeout)
).flatMap[Unit] {
case Success(true) => pass
case _ => reject(MdsolAuthFailedRejection)
}
case _ =>
logger.error(s"MAUTH: Non-Strict Entity in Request")
reject(MdsolAuthFailedRejection)
}
}
isAuthed
}
}
}

Expand Down Expand Up @@ -168,7 +167,7 @@ trait MAuthDirectives extends StrictLogging {
* the request is rejected with a MdsolAuthMissingHeaderRejection if the expected header is not present
*/
def extractLatestAuthenticationHeaders(v2OnlyAuthenticate: Boolean): Directive1[MauthHeaderValues] = {
extractRequest.flatMap { request: HttpRequest =>
extractRequest.flatMap { (request: HttpRequest) =>
// Try to extract and verify V2 headers
val authenticationHeaderStr = extractRequestHeader(request, MAuthRequest.MCC_AUTHENTICATION_HEADER_NAME)
if (authenticationHeaderStr.nonEmpty) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class MauthPublicKeyProvider(configuration: AuthenticatorConfiguration, signer:
ec: ExecutionContext,
system: ActorSystem,
materializer: Materializer
) extends ClientPublicKeyProvider
) extends ClientPublicKeyProvider[Future]
with StrictLogging {

private val cCache = Caffeine.newBuilder().build[String, Entry[Option[PublicKey]]]()
Expand Down Expand Up @@ -70,12 +70,12 @@ class MauthPublicKeyProvider(configuration: AuthenticatorConfiguration, signer:
logger.error(s"Unexpected response returned by server -- status: ${response.status} response: $body")
None
}
}.handleError { error: Throwable =>
}.handleError { (error: Throwable) =>
logger.error("Request to get MAuth public key couldn't be signed", error)
None
}
}
.handleError { error: Throwable =>
.handleError { (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 @@ -44,7 +44,7 @@ class MAuthDirectivesSpec extends AnyWordSpec with Matchers with ScalatestRouteT

private implicit val timeout: FiniteDuration = 30.seconds
private implicit val requestValidationTimeout: Duration = 10.seconds
private val client = mock[ClientPublicKeyProvider]
private val client = mock[ClientPublicKeyProvider[Future]]
private val mockEpochTimeProvider: EpochTimeProvider = mock[EpochTimeProvider]
private val authenticator: RequestAuthenticator = new RequestAuthenticator(client, mockEpochTimeProvider)
private val authenticatorV2: RequestAuthenticator = new RequestAuthenticator(client, mockEpochTimeProvider, v2OnlyAuthenticate = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import enumeratum._
import org.typelevel.ci._
import org.typelevel.log4cats.slf4j.Slf4jLogger

import scala.concurrent.ExecutionContext
import scala.concurrent.duration.Duration
import scala.util.Try

Expand All @@ -24,7 +23,7 @@ final case class MdsolAuthMissingHeaderRejection(headerName: String) extends Thr
sealed trait HeaderVersion extends EnumEntry

object HeaderVersion extends Enum[HeaderVersion] {
val values = findValues
val values: IndexedSeq[HeaderVersion] = findValues

case object V1 extends HeaderVersion {
val authHeaderName = ci"${MAuthRequest.X_MWS_AUTHENTICATION_HEADER_NAME}"
Expand All @@ -38,10 +37,9 @@ object HeaderVersion extends Enum[HeaderVersion] {

object MAuthMiddleware {
import HeaderVersion._
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] =
def apply[G[_]: Sync, F[_]](requestValidationTimeout: Duration, authenticator: Authenticator[F], fk: F ~> G)(
http: Http[G, F]
)(implicit F: Async[F]): Http[G, F] =
Kleisli { request =>
val logger = Slf4jLogger.getLogger[G]

Expand Down Expand Up @@ -96,7 +94,7 @@ object MAuthMiddleware {
mAuthRequest
} else mAuthRequest

F.fromFuture(F.delay(authenticator.authenticate(req)(ec, requestValidationTimeout)))
authenticator.authenticate(req)(requestValidationTimeout)
}
}).flatMap(b =>
if (b) http(request)
Expand All @@ -106,11 +104,11 @@ object MAuthMiddleware {
}
}

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

def httpApp[F[_]: Async](requestValidationTimeout: Duration, authenticator: Authenticator)(httpRoutes: HttpApp[F])(implicit
ec: ExecutionContext
def httpApp[F[_]: Async](requestValidationTimeout: Duration, authenticator: Authenticator[F])(
httpRoutes: HttpApp[F]
): HttpApp[F] = apply(requestValidationTimeout, authenticator, FunctionK.id[F])(httpRoutes)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.mdsol.mauth.http4s

import cats.effect.Async
import com.fasterxml.jackson.databind.ObjectMapper
import com.github.benmanes.caffeine.cache.Caffeine
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 com.typesafe.scalalogging.StrictLogging
import org.http4s.client.Client
import org.http4s.{Response, Status}
import scalacache.caffeine.CaffeineCache
import scalacache.memoization.memoizeF
import scalacache.{Cache, Entry}

import java.net.URI
import java.security.PublicKey
import java.util.UUID
import scala.concurrent.duration._
import scala.util.{Failure, Success, Try}
import cats.implicits._

class MauthPublicKeyProvider[F[_]](configuration: AuthenticatorConfiguration, signer: MAuthRequestSigner, val client: Client[F])(implicit F: Async[F])
extends ClientPublicKeyProvider[F]
with StrictLogging {

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

/** 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))
client.run(signedRequest.toHttp4sRequest[F]).use(retrievePublicKey)
}

private def retrievePublicKey(mauthPublicKeyFetcher: Response[F]): F[Option[PublicKey]] = {
if (mauthPublicKeyFetcher.status == Status.Ok) {
mauthPublicKeyFetcher
.as[String]
.map(str =>
Try(
MAuthKeysHelper.getPublicKeyFromString(
mapper.readTree(str).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: ${mauthPublicKeyFetcher.status} response: ${mauthPublicKeyFetcher.body}")
F.pure[Option[PublicKey]](None)
}
}

protected def getRequestUrlPath(appUUID: UUID): String =
configuration.getRequestUrlPath + String.format(configuration.getSecurityTokensUrlPath, appUUID.toString)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import cats.effect._
import cats.syntax.all._
import com.mdsol.mauth.MAuthRequest
import com.mdsol.mauth.exception.MAuthValidationException
import com.mdsol.mauth.scaladsl.RequestAuthenticator
import com.mdsol.mauth.scaladsl.RequestAuthenticatorF
import com.mdsol.mauth.scaladsl.utils.ClientPublicKeyProvider
import com.mdsol.mauth.test.utils.TestFixtures
import com.mdsol.mauth.util.{EpochTimeProvider, MAuthKeysHelper}
Expand All @@ -17,7 +17,6 @@ import org.http4s.Method._

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

class MAuthMiddlewareSuite extends CatsEffectSuite {
Expand Down Expand Up @@ -62,22 +61,23 @@ class MAuthMiddlewareSuite extends CatsEffectSuite {
private val timeHeader: Long = 1509041057L
private val publicKey = MAuthKeysHelper.getPublicKeyFromString(TestFixtures.PUBLIC_KEY_1)

private val client = new ClientPublicKeyProvider {
override def getPublicKey(appUUID: UUID): Future[Option[PublicKey]] =
private val client = new ClientPublicKeyProvider[IO] {

override def getPublicKey(appUUID: UUID): IO[Option[PublicKey]] =
if (appUUID == appUuid) {
Future(publicKey.some)
} else Future.failed(new Throwable("Wrong app UUID"))
IO.pure(publicKey.some)
} else IO.raiseError(new Throwable("Wrong app UUID"))
}

private val epochTimeProvider = new EpochTimeProvider {
override def inSeconds(): Long = timeHeader
}

private implicit val authenticator: RequestAuthenticator = new RequestAuthenticator(client, epochTimeProvider)
private implicit val authenticator: RequestAuthenticatorF[IO] = new RequestAuthenticatorF(client, epochTimeProvider)

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

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

Expand Down Expand Up @@ -138,14 +138,14 @@ class MAuthMiddlewareSuite extends CatsEffectSuite {
}

test("reject if public key cannot be found") {
val localClient = new ClientPublicKeyProvider {
override def getPublicKey(appUUID: UUID): Future[Option[PublicKey]] =
val localClient = new ClientPublicKeyProvider[IO] {
override def getPublicKey(appUUID: UUID): IO[Option[PublicKey]] =
if (appUUID == appUuid) {
Future(none)
} else Future.failed(new Throwable("Wrong app UUID"))
IO.pure(none)
} else IO.raiseError(new Throwable("Wrong app UUID"))
}

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

Expand Down
Loading