From 0d21bbdb3c8c3d895971e30b29b5dc9f91005483 Mon Sep 17 00:00:00 2001 From: Jacob Wang Date: Mon, 8 Jun 2020 22:34:09 +1200 Subject: [PATCH] Support signing sttp requests - Add support for signing com.softwaremill.sttp requests - Consolidate logic for extracting the path & query parameter from the request type of each library using java.net.URI (since every library normally provides a well-tested conversion to java.net.URI). akka-http and sttp signer now uses it - Disable mauth-test-utils publishing - Rename FixturesLoader to TestFixtures. Consolidate test constants from mauth-signer-akka-http's tests --- .travis.yml | 9 +- build.sbt | 17 ++- .../mauth/akka/http/MAuthDirectivesSpec.scala | 6 +- .../mauth/RequestAuthenticatorBaseSpec.scala | 30 ++--- .../mauth/MAuthSignatureHelperSpec.scala | 28 ++--- .../com/mdsol/mauth/MAuthRequestSigner.scala | 3 +- .../mdsol/mauth/MAuthRequestSignerSpec.scala | 93 +++++--------- .../apache/HttpClientRequestSignerSpec.scala | 14 +-- .../com/mdsol/mauth/MAuthSttpSigner.scala | 61 +++++++++ .../mauth/SttpAkkaMAuthRequestSender.scala | 21 ++++ .../mdsol/mauth/SttpMAuthRequestSender.scala | 9 ++ .../com/mdsol/mauth/MAuthSttpSignerSpec.scala | 116 ++++++++++++++++++ .../SttpAkkaMAuthRequestSenderSpec.scala | 115 +++++++++++++++++ .../src/main/java/com/mdsol/mauth/Signer.java | 2 +- .../java/com/mdsol/mauth/SignerUtils.java | 28 +++++ .../com/mdsol/mauth/DefaultSignerSpec.scala | 24 ++-- .../mauth/test/utils/FixturesLoader.java | 63 ---------- .../mdsol/mauth/test/utils/TestFixtures.scala | 86 +++++++++++++ project/BuildSettings.scala | 4 + project/Dependencies.scala | 35 +++--- 20 files changed, 568 insertions(+), 196 deletions(-) create mode 100644 modules/mauth-signer-sttp/src/main/scala/com/mdsol/mauth/MAuthSttpSigner.scala create mode 100644 modules/mauth-signer-sttp/src/main/scala/com/mdsol/mauth/SttpAkkaMAuthRequestSender.scala create mode 100644 modules/mauth-signer-sttp/src/main/scala/com/mdsol/mauth/SttpMAuthRequestSender.scala create mode 100644 modules/mauth-signer-sttp/src/test/scala/com/mdsol/mauth/MAuthSttpSignerSpec.scala create mode 100644 modules/mauth-signer-sttp/src/test/scala/com/mdsol/mauth/SttpAkkaMAuthRequestSenderSpec.scala create mode 100644 modules/mauth-signer/src/main/java/com/mdsol/mauth/SignerUtils.java delete mode 100644 modules/mauth-test-utils/src/main/java/com/mdsol/mauth/test/utils/FixturesLoader.java create mode 100644 modules/mauth-test-utils/src/main/java/com/mdsol/mauth/test/utils/TestFixtures.scala diff --git a/.travis.yml b/.travis.yml index 4900233a..8293db22 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,12 +21,17 @@ before_install: jobs: include: - stage: test - script: sbt scalafmtSbtCheck scalafmtCheckAll +test:compile +suggestNextVersionAcrossAllProjects +test + name: Test + script: sbt scalafmtSbtCheck scalafmtCheckAll + + - stage: test + name: Format check + script: sbt +test:compile +suggestNextVersionAcrossAllProjects +test - stage: release script: .travis/deploy.sh if: tag IS present stages: - - test + - Test - release diff --git a/build.sbt b/build.sbt index 2da21ac3..21b538cc 100644 --- a/build.sbt +++ b/build.sbt @@ -43,7 +43,7 @@ lazy val `mauth-common` = (project in file("modules/mauth-common")) lazy val `mauth-test-utils` = (project in file("modules/mauth-test-utils")) .settings( basicSettings, - publishSettings, + noPublishSettings, javaProjectSettings, name := "mauth-test-utils", libraryDependencies ++= @@ -91,6 +91,20 @@ lazy val `mauth-signer-akka-http` = (project in file("modules/mauth-signer-akka- Dependencies.test(scalaMock, scalaTest, wiremock).map(withExclusions) ) +lazy val `mauth-signer-sttp` = (project in file("modules/mauth-signer-sttp")) + .dependsOn(`mauth-signer`, `mauth-test-utils` % "test") + .settings( + basicSettings, + publishSettings, + scalaProjectSettings, + name := "mauth-signer-sttp", + libraryDependencies ++= + Dependencies.compile(catsEffect, akkaHttp, akkaStream, scalaLibCompat, sttp, scalaLogging).map(withExclusions) ++ + Dependencies.test(scalaMock, scalaTest, wiremock, sttpAkkaHttpBackend).map(withExclusions), + // TODO remove once published + mimaPreviousArtifacts := Set.empty + ) + lazy val `mauth-authenticator` = (project in file("modules/mauth-authenticator")) .dependsOn(`mauth-common`) .settings( @@ -190,6 +204,7 @@ lazy val `mauth-jvm-clients` = (project in file(".")) `mauth-proxy`, `mauth-signer`, `mauth-signer-akka-http`, + `mauth-signer-sttp`, `mauth-signer-apachehttp`, `mauth-test-utils` ) diff --git a/modules/mauth-authenticator-akka-http/src/test/scala/com/mdsol/mauth/akka/http/MAuthDirectivesSpec.scala b/modules/mauth-authenticator-akka-http/src/test/scala/com/mdsol/mauth/akka/http/MAuthDirectivesSpec.scala index 98878db1..71d587fb 100644 --- a/modules/mauth-authenticator-akka-http/src/test/scala/com/mdsol/mauth/akka/http/MAuthDirectivesSpec.scala +++ b/modules/mauth-authenticator-akka-http/src/test/scala/com/mdsol/mauth/akka/http/MAuthDirectivesSpec.scala @@ -11,7 +11,7 @@ import com.mdsol.mauth.MAuthRequest import com.mdsol.mauth.http.{`X-MWS-Authentication`, `X-MWS-Time`} import com.mdsol.mauth.scaladsl.RequestAuthenticator import com.mdsol.mauth.scaladsl.utils.ClientPublicKeyProvider -import com.mdsol.mauth.test.utils.FixturesLoader +import com.mdsol.mauth.test.utils.TestFixtures import com.mdsol.mauth.util.{EpochTimeProvider, MAuthKeysHelper} import org.bouncycastle.jce.provider.BouncyCastleProvider import org.scalamock.scalatest.MockFactory @@ -51,7 +51,7 @@ class MAuthDirectivesSpec extends AnyWordSpec with Matchers with ScalatestRouteT "authenticate" should { lazy val route: Route = authenticate(authenticator, timeout, requestValidationTimeout).apply(complete(HttpResponse())) - val publicKey = MAuthKeysHelper.getPublicKeyFromString(FixturesLoader.getPublicKey) + val publicKey = MAuthKeysHelper.getPublicKeyFromString(TestFixtures.PUBLIC_KEY_1) "pass successfully authenticated request" in { (client.getPublicKey _).expects(appUuid).returns(Future(Some(publicKey))) @@ -228,7 +228,7 @@ class MAuthDirectivesSpec extends AnyWordSpec with Matchers with ScalatestRouteT "authenticate when v2OnlyAuthenticate = true" should { lazy val route: Route = authenticate(authenticatorV2, timeout, requestValidationTimeout).apply(complete(HttpResponse())) - val publicKey = MAuthKeysHelper.getPublicKeyFromString(FixturesLoader.getPublicKey) + val publicKey = MAuthKeysHelper.getPublicKeyFromString(TestFixtures.PUBLIC_KEY_1) "pass successfully authenticated request with both v1 and v2 headers, with V2 headers taking precedence" in { (client.getPublicKey _).expects(appUuid).returns(Future(Some(publicKey))) diff --git a/modules/mauth-authenticator-scala/src/test/scala/com/mdsol/mauth/RequestAuthenticatorBaseSpec.scala b/modules/mauth-authenticator-scala/src/test/scala/com/mdsol/mauth/RequestAuthenticatorBaseSpec.scala index 8be95d4b..f219851d 100644 --- a/modules/mauth-authenticator-scala/src/test/scala/com/mdsol/mauth/RequestAuthenticatorBaseSpec.scala +++ b/modules/mauth-authenticator-scala/src/test/scala/com/mdsol/mauth/RequestAuthenticatorBaseSpec.scala @@ -4,7 +4,7 @@ import java.nio.charset.StandardCharsets import java.security.Security import com.mdsol.mauth.test.utils.FakeMAuthServer.EXISTING_CLIENT_APP_UUID -import com.mdsol.mauth.test.utils.FixturesLoader +import com.mdsol.mauth.test.utils.TestFixtures import com.mdsol.mauth.util.EpochTimeProvider import org.apache.http.client.methods.{HttpGet, HttpPost} import org.bouncycastle.jce.provider.BouncyCastleProvider @@ -17,7 +17,7 @@ trait RequestAuthenticatorBaseSpec extends AnyFlatSpec with BeforeAndAfterAll wi val CLIENT_X_MWS_TIME_HEADER_VALUE = "1444672122" val CLIENT_UNICODE_X_MWS_TIME_HEADER_VALUE = "1444748974" val CLIENT_NO_BODY_X_MWS_TIME_HEADER_VALUE = "1424700000" - val PUBLIC_KEY: String = FixturesLoader.getPublicKey + val PUBLIC_KEY: String = TestFixtures.PUBLIC_KEY_1 val REQUEST_VALIDATION_TIMEOUT_SECONDS = 300L val CLIENT_REQUEST_SIGNATURE: String = """fFQzIOo4S1MxxmEDB9v7v0IYNytnS3I5aHNeJfEfFe1v1gTE/cH36BfLG/zp @@ -138,30 +138,30 @@ trait RequestAuthenticatorBaseSpec extends AnyFlatSpec with BeforeAndAfterAll wi .build } - val CLIENT_REQUEST_BINARY_APP_UUID = FixturesLoader.APP_UUID_V2 - val CLIENT_REQUEST_BINARY_TIME_HEADER_VALUE = FixturesLoader.EPOCH_TIME_V2 - val PUBLIC_KEY2: String = FixturesLoader.getPublicKey2 - val CLIENT_REQUEST_AUTHENTICATION_BINARY_HEADER_V1: String = "MWS " + FixturesLoader.APP_UUID_V2 + ":" + FixturesLoader.SIGNATURE_V1_BINARY + val CLIENT_REQUEST_BINARY_APP_UUID = TestFixtures.APP_UUID_V2 + val CLIENT_REQUEST_BINARY_TIME_HEADER_VALUE = TestFixtures.EPOCH_TIME + val PUBLIC_KEY2: String = TestFixtures.PUBLIC_KEY_2 + val CLIENT_REQUEST_AUTHENTICATION_BINARY_HEADER_V1: String = "MWS " + TestFixtures.APP_UUID_V2 + ":" + TestFixtures.SIGNATURE_V1_BINARY def getRequestWithBinaryBodyV1: MAuthRequest = { MAuthRequest.Builder.get .withAuthenticationHeaderValue(CLIENT_REQUEST_AUTHENTICATION_BINARY_HEADER_V1) .withTimeHeaderValue(CLIENT_REQUEST_BINARY_TIME_HEADER_VALUE) - .withHttpMethod(FixturesLoader.REQUEST_METHOD_V2) - .withMessagePayload(FixturesLoader.getBinaryFileBody) - .withResourcePath(FixturesLoader.REQUEST_PATH_V2) - .withQueryParameters(FixturesLoader.REQUEST_QUERY_PARAMETERS_V2) + .withHttpMethod(TestFixtures.REQUEST_METHOD_V2) + .withMessagePayload(TestFixtures.BINARY_FILE_BODY) + .withResourcePath(TestFixtures.REQUEST_PATH_V2) + .withQueryParameters(TestFixtures.REQUEST_QUERY_PARAMETERS_V2) .build } - val CLIENT_REQUEST_AUTHENTICATION_BINARY_HEADER_V2: String = "MWSV2 " + CLIENT_REQUEST_BINARY_APP_UUID + ":" + FixturesLoader.SIGNATURE_V2_BINARY + val CLIENT_REQUEST_AUTHENTICATION_BINARY_HEADER_V2: String = "MWSV2 " + CLIENT_REQUEST_BINARY_APP_UUID + ":" + TestFixtures.SIGNATURE_V2_BINARY def getRequestWithBinaryBodyV2: MAuthRequest = { MAuthRequest.Builder.get .withAuthenticationHeaderValue(CLIENT_REQUEST_AUTHENTICATION_BINARY_HEADER_V2) .withTimeHeaderValue(CLIENT_REQUEST_BINARY_TIME_HEADER_VALUE) - .withHttpMethod(FixturesLoader.REQUEST_METHOD_V2) - .withMessagePayload(FixturesLoader.getBinaryFileBody) - .withResourcePath(FixturesLoader.REQUEST_PATH_V2) - .withQueryParameters(FixturesLoader.REQUEST_QUERY_PARAMETERS_V2) + .withHttpMethod(TestFixtures.REQUEST_METHOD_V2) + .withMessagePayload(TestFixtures.BINARY_FILE_BODY) + .withResourcePath(TestFixtures.REQUEST_PATH_V2) + .withQueryParameters(TestFixtures.REQUEST_QUERY_PARAMETERS_V2) .build } diff --git a/modules/mauth-common/src/test/scala/com/mdsol/mauth/MAuthSignatureHelperSpec.scala b/modules/mauth-common/src/test/scala/com/mdsol/mauth/MAuthSignatureHelperSpec.scala index fb656740..03f3da07 100644 --- a/modules/mauth-common/src/test/scala/com/mdsol/mauth/MAuthSignatureHelperSpec.scala +++ b/modules/mauth-common/src/test/scala/com/mdsol/mauth/MAuthSignatureHelperSpec.scala @@ -4,7 +4,7 @@ import java.nio.charset.StandardCharsets import java.security.Security import java.util.UUID -import com.mdsol.mauth.test.utils.FixturesLoader +import com.mdsol.mauth.test.utils.TestFixtures import com.mdsol.mauth.util.MAuthKeysHelper.{getPrivateKeyFromString, getPublicKeyFromString} import com.mdsol.mauth.util.MAuthSignatureHelper import org.bouncycastle.jce.provider.BouncyCastleProvider @@ -21,14 +21,14 @@ class MAuthSignatureHelperSpec extends AnyFlatSpec with Matchers { private val CLIENT_REQUEST_PAYLOAD = "message here" private val CLIENT_REQUEST_QUERY_PARAMETERS = "key1=value1&key2=value2" private val TEST_EPOCH_TIME = 1424700000L - private val TEST_PRIVATE_KEY = getPrivateKeyFromString(FixturesLoader.getPrivateKey2) + private val TEST_PRIVATE_KEY = getPrivateKeyFromString(TestFixtures.PRIVATE_KEY_2) // the same test data with ruby and python - private val CLIENT_APP_UUID_V2 = FixturesLoader.APP_UUID_V2 - private val CLIENT_REQUEST_METHOD_V2 = FixturesLoader.REQUEST_METHOD_V2 - private val CLIENT_REQUEST_PATH_V2 = FixturesLoader.REQUEST_PATH_V2 - private val CLIENT_REQUEST_QUERY_PARAMETERS_V2 = FixturesLoader.REQUEST_QUERY_PARAMETERS_V2 - private val TEST_EPOCH_TIME_V2 = FixturesLoader.EPOCH_TIME_V2 + private val CLIENT_APP_UUID_V2 = TestFixtures.APP_UUID_V2 + private val CLIENT_REQUEST_METHOD_V2 = TestFixtures.REQUEST_METHOD_V2 + private val CLIENT_REQUEST_PATH_V2 = TestFixtures.REQUEST_PATH_V2 + private val CLIENT_REQUEST_QUERY_PARAMETERS_V2 = TestFixtures.REQUEST_QUERY_PARAMETERS_V2 + private val TEST_EPOCH_TIME_V2 = TestFixtures.EPOCH_TIME behavior of "MAuthSignatureHelper" @@ -136,8 +136,8 @@ class MAuthSignatureHelperSpec extends AnyFlatSpec with Matchers { it should "verify signature for V2" in { val testString = "Hello world" - val signString = MAuthSignatureHelper.encryptSignatureRSA(getPrivateKeyFromString(FixturesLoader.getPrivateKey), testString) - MAuthSignatureHelper.verifyRSA(testString, signString, getPublicKeyFromString(FixturesLoader.getPublicKey)) shouldBe true + val signString = MAuthSignatureHelper.encryptSignatureRSA(getPrivateKeyFromString(TestFixtures.PRIVATE_KEY_1), testString) + MAuthSignatureHelper.verifyRSA(testString, signString, getPublicKeyFromString(TestFixtures.PUBLIC_KEY_1)) shouldBe true } it should "correctly generate signature of binary body for V2" in { @@ -146,10 +146,10 @@ class MAuthSignatureHelperSpec extends AnyFlatSpec with Matchers { CLIENT_REQUEST_METHOD_V2, CLIENT_REQUEST_PATH_V2, CLIENT_REQUEST_QUERY_PARAMETERS_V2, - FixturesLoader.getBinaryFileBody, + TestFixtures.BINARY_FILE_BODY, TEST_EPOCH_TIME_V2 ) - MAuthSignatureHelper.encryptSignatureRSA(TEST_PRIVATE_KEY, testString) shouldBe FixturesLoader.SIGNATURE_V2_BINARY + MAuthSignatureHelper.encryptSignatureRSA(TEST_PRIVATE_KEY, testString) shouldBe TestFixtures.SIGNATURE_V2_BINARY } it should "correctly generate signature with empty body for V2" in { @@ -161,7 +161,7 @@ class MAuthSignatureHelperSpec extends AnyFlatSpec with Matchers { Array.empty, TEST_EPOCH_TIME_V2 ) - MAuthSignatureHelper.encryptSignatureRSA(TEST_PRIVATE_KEY, testString) shouldBe FixturesLoader.SIGNATURE_V2_EMPTY + MAuthSignatureHelper.encryptSignatureRSA(TEST_PRIVATE_KEY, testString) shouldBe TestFixtures.SIGNATURE_V2_EMPTY } it should "normalize path: correctly generate signature for V2" in { @@ -172,10 +172,10 @@ class MAuthSignatureHelperSpec extends AnyFlatSpec with Matchers { CLIENT_REQUEST_METHOD_V2, resourcePath, CLIENT_REQUEST_QUERY_PARAMETERS_V2, - FixturesLoader.getBinaryFileBody, + TestFixtures.BINARY_FILE_BODY, TEST_EPOCH_TIME_V2 ) - MAuthSignatureHelper.encryptSignatureRSA(TEST_PRIVATE_KEY, testString) shouldBe FixturesLoader.SIGNATURE_V2_BINARY + MAuthSignatureHelper.encryptSignatureRSA(TEST_PRIVATE_KEY, testString) shouldBe TestFixtures.SIGNATURE_V2_BINARY } } diff --git a/modules/mauth-signer-akka-http/src/main/scala/com/mdsol/mauth/MAuthRequestSigner.scala b/modules/mauth-signer-akka-http/src/main/scala/com/mdsol/mauth/MAuthRequestSigner.scala index efb2b2d5..a8ef802f 100644 --- a/modules/mauth-signer-akka-http/src/main/scala/com/mdsol/mauth/MAuthRequestSigner.scala +++ b/modules/mauth-signer-akka-http/src/main/scala/com/mdsol/mauth/MAuthRequestSigner.scala @@ -81,7 +81,8 @@ class MAuthRequestSigner(appUUID: UUID, privateKey: PrivateKey, epochTimeProvide } override def signRequest(request: NewUnsignedRequest): NewSignedRequest = { - val headers = generateRequestHeaders(request.httpMethod, request.uri.getRawPath, request.body, request.uri.getRawQuery).asScala.toMap + val javaUri = request.uri + val headers = SignerUtils.signWithUri(this, request.httpMethod, javaUri, request.body).asScala.toMap NewSignedRequest( request, headers diff --git a/modules/mauth-signer-akka-http/src/test/scala/com/mdsol/mauth/MAuthRequestSignerSpec.scala b/modules/mauth-signer-akka-http/src/test/scala/com/mdsol/mauth/MAuthRequestSignerSpec.scala index 3923263c..34b78b70 100644 --- a/modules/mauth-signer-akka-http/src/test/scala/com/mdsol/mauth/MAuthRequestSignerSpec.scala +++ b/modules/mauth-signer-akka-http/src/test/scala/com/mdsol/mauth/MAuthRequestSignerSpec.scala @@ -12,7 +12,7 @@ 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.FixturesLoader +import com.mdsol.mauth.test.utils.TestFixtures._ import com.mdsol.mauth.util.EpochTimeProvider import org.apache.http.HttpStatus import org.bouncycastle.jce.provider.BouncyCastleProvider @@ -28,22 +28,21 @@ class MAuthRequestSignerSpec extends AnyFlatSpec with Matchers with HttpClient w implicit val system: ActorSystem = ActorSystem() var service = new WireMockServer(wireMockConfig.dynamicPort) - val TIME_CONSTANT = 1509041057L - val epochTimeProvider: EpochTimeProvider = new EpochTimeProvider() { override def inSeconds(): Long = TIME_CONSTANT } - val testUUID = "2a6790ab-f6c6-45be-86fc-9e9be76ec12a" Security.addProvider(new BouncyCastleProvider) - val signer: MAuthRequestSigner = MAuthRequestSigner( - UUID.fromString(testUUID), - FixturesLoader.getPrivateKey, - epochTimeProvider + 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 ) - val signerV2: MAuthRequestSigner = MAuthRequestSigner( - UUID.fromString(testUUID), - FixturesLoader.getPrivateKey, - epochTimeProvider, + val signerV2: MAuthRequestSigner = new MAuthRequestSigner( + UUID.fromString(APP_UUID_1), + PRIVATE_KEY_1, + CONST_EPOCH_TIME_PROVIDER, v2OnlySignRequests = true ) @@ -53,100 +52,70 @@ class MAuthRequestSignerSpec extends AnyFlatSpec with Matchers with HttpClient w override protected def afterAll(): Unit = service.stop() - val EXPECTED_GET_TIME_HEADER = "1509041057" - - val simpleUnsignedRequest: UnsignedRequest = UnsignedRequest(uri = new URI("/")) + val simpleUnsignedRequest: UnsignedRequest = UnsignedRequest(uri = URI_EMPTY_PATH) val simpleNewUnsignedRequest: NewUnsignedRequest = - NewUnsignedRequest.fromStringBodyUtf8(httpMethod = "GET", uri = new URI("/"), body = "", headers = Map.empty) - - val EXPECTED_GET_X_MWS_AUTHENTICATION_HEADER_FOR_SIMPLE_REQUEST: String = - s"""MWS $testUUID:ih3xq6OvQ2/D5ktPDaZ4F6tanzdn2XGzZ+KOaFXC+YKVjNcSCfUiKB6T/552K3AmKm/yZF4rdEOps - |MZ0QkuFqEZdwQ8R3iWUwdrNPsmNXSVvF50pRAlcI77UP5gUKV01xjZxfZ/M/vhzVn513bAgJ6CM8X4dtG20ki5xLsO3 - |5e2eZs5i9IwA/hEaKSm/PH2pEHwxz5c9MMGtHiFgzgXGacziVn0fr2c6X5jb3cDjHnfNVX8o57kFjL5E0YOoeEKDwHy - |flGhbfFNZys29jo83JCK2MQj9s+fZq5NsgmwuACRE6BnqKSPqwDWN4OK3N/iPcTwCsMKz/c5/3CEbMTlH8A==""".stripMargin.replaceAll("\n", "") - val EXPECTED_GET_MCC_AUTHENTICATION_HEADER_FOR_SIMPLE_REQUEST: String = - s"""MWSV2 $testUUID:h0MJYf5/zlX9VqqchANLr7XUln0RydMV4msZSXzLq2sbr3X+TGeJ60K9ZSlSuRrzyHbzzwuZABA - |3P2j3l9t+gyBC1c/JSa8mldMrIXXYzp0lYLxLkghH09hm3k0pEW2la94K/Num3xgNymn6D/B9dJ1onRIgl+T+e/m4k6 - |T3apKHcV/6cJ9asm+jDjzB8OuCVWVsLZQKQbtiydUYNisYerKVxWPLs9SHNZ6GmAqq4ZCCpyEQZuMNF6cMmXgQ0Pxe9 - |X/yNA1Xc3Fakuga47lUQ6Bn7xvhkH6P+ZP0k4U7kidziXpxpkDts8fEXTpkvFX0PR7vaxjbMZzWsU413jyNsw==;""".stripMargin.replaceAll("\n", "") + NewUnsignedRequest.fromStringBodyUtf8(httpMethod = "GET", uri = URI_EMPTY_PATH, body = "", headers = Map.empty) val unsignedRequest: NewUnsignedRequest = - NewUnsignedRequest.fromStringBodyUtf8(httpMethod = "GET", uri = new URI("/" + "?key2=data2&key1=data1"), body = "Request Body", headers = Map.empty) - val EXPECTED_GET_X_MWS_AUTHENTICATION_HEADER: String = - s"""MWS $testUUID:OoxiQ/Z6EjTUAoAGNKD5FS6ka+9IcWW5rtuzbXRDLRGj4pzSdeI0FPIlT0E/ZR96xR0a5EJlJ3E - |8usr5qas/uzNEDajAqpjqOaO4m3j+4juXt0QrdBvj3sgStD6ozOJrfhyeSWvFp3d9SBx8tPkPrqv6z5ewQliaSOaI - |20yir4+RStwj6P7j/5/ZlDRMBEFBiFuAyAWMAbKnefRwK+0yUqO9pEBQx43YqBzs+Xb9sTM0hKd5IohAW8O8xj1coB - |YP/NGRvhM5Z+VMXnbRXwkqUlEXIDvZ3fKjPNGEQxo+m9oFH1dLI8oGI9xoC9P3liwUqY5h+g+hSQ4KLIfDm0qvLQ==""".stripMargin.replaceAll("\n", "") - val EXPECTED_GET_MCC_AUTHENTICATION_HEADER: String = - s"""MWSV2 $testUUID:n5+io+SgpPMgatLarleDkX18r1ZVBtp7YWgu3yeP0k/P8otp4ThEtBJ6Du3b2Pet+7xlkfK90 - |RXrcwiKA0SS8vpPX8nCtLa92hE3G1e0A41Cn00MuasVwV7JlkQeffJH8qQjvapwRsQ9dbFTPOktS4u0fm/7L9hI6k - |m99lqCP72i0tP7vGCst4Gc1OewGMR+60FUNR7eN66z8wbeXxX5gzMNGpppP/3P2YROGkONlsxbd1UxrEN62r6yQBF - |i9hTFF0FCqDM63UiLxTt3ooTpj4iUx/3htvPJ2AlSW5TaoviQUjQFYdb+CB6xDi0LFp93V5289lEXdPOVCULUGesqDA==;""".stripMargin.replaceAll("\n", "") - + 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).right.get.timeHeader shouldBe EXPECTED_GET_TIME_HEADER + signer.signRequest(simpleUnsignedRequest).right.get.timeHeader shouldBe EXPECTED_TIME_HEADER_1 } it should "add authentication header to a request for V1" in { - signer.signRequest(simpleUnsignedRequest).right.get.authHeader shouldBe EXPECTED_GET_X_MWS_AUTHENTICATION_HEADER_FOR_SIMPLE_REQUEST + signer.signRequest(simpleUnsignedRequest).right.get.authHeader shouldBe EXPECTED_AUTH_NO_BODY_V1 } it should "add authentication header to a request with body for V1" in { - signer.signRequest(UnsignedRequest(uri = new URI("/"), body = Some("Request Body"))).right.get.authHeader shouldBe "MWS " + - "2a6790ab-f6c6-45be-86fc-9e9be76ec12a:OoxiQ/Z6EjTUAoAGNKD5FS6ka+9IcWW5rtuzbXRDLRGj4pzSdeI0FPIlT0E/ZR96xR0a5EJlJ3E8usr" + - "5qas/uzNEDajAqpjqOaO4m3j+4juXt0QrdBvj3sgStD6ozOJrfhyeSWvFp3d9SBx8tPkPrqv6z5ewQliaSOaI20yir4+RStwj6P7j/5/ZlDRMBEFBiFuA" + - "yAWMAbKnefRwK+0yUqO9pEBQx43YqBzs+Xb9sTM0hKd5IohAW8O8xj1coBYP/NGRvhM5Z+VMXnbRXwkqUlEXIDvZ3fKjPNGEQxo+m9oFH1dLI8oGI9xoC9P3liwUqY5h+g+hSQ4KLIfDm0qvLQ==" + signer.signRequest(UnsignedRequest(uri = URI_EMPTY_PATH, body = Some(SIMPLE_REQUEST_BODY))).right.get.authHeader shouldBe EXPECTED_AUTH_SIMPLE_BODY_V1 } it should "add authentication header to a request" in { val authHeaders = signer.signRequest(simpleNewUnsignedRequest).mauthHeaders - authHeaders(MAuthRequest.X_MWS_AUTHENTICATION_HEADER_NAME) shouldBe EXPECTED_GET_X_MWS_AUTHENTICATION_HEADER_FOR_SIMPLE_REQUEST - authHeaders(MAuthRequest.MCC_AUTHENTICATION_HEADER_NAME) shouldBe EXPECTED_GET_MCC_AUTHENTICATION_HEADER_FOR_SIMPLE_REQUEST + authHeaders(MAuthRequest.X_MWS_AUTHENTICATION_HEADER_NAME) shouldBe EXPECTED_AUTH_NO_BODY_V1 + authHeaders(MAuthRequest.MCC_AUTHENTICATION_HEADER_NAME) shouldBe EXPECTED_AUTH_NO_BODY_V2 } it should "add authentication header to a request with body" in { signer - .signRequest(NewUnsignedRequest.fromStringBodyUtf8(httpMethod = "GET", uri = new URI("/"), body = "Request Body", headers = Map.empty)) - .mauthHeaders(MAuthRequest.X_MWS_AUTHENTICATION_HEADER_NAME) shouldBe "MWS " + - "2a6790ab-f6c6-45be-86fc-9e9be76ec12a:OoxiQ/Z6EjTUAoAGNKD5FS6ka+9IcWW5rtuzbXRDLRGj4pzSdeI0FPIlT0E/ZR96xR0a5EJlJ3E8usr" + - "5qas/uzNEDajAqpjqOaO4m3j+4juXt0QrdBvj3sgStD6ozOJrfhyeSWvFp3d9SBx8tPkPrqv6z5ewQliaSOaI20yir4+RStwj6P7j/5/ZlDRMBEFBiFuA" + - "yAWMAbKnefRwK+0yUqO9pEBQx43YqBzs+Xb9sTM0hKd5IohAW8O8xj1coBYP/NGRvhM5Z+VMXnbRXwkqUlEXIDvZ3fKjPNGEQxo+m9oFH1dLI8oGI9xoC9P3liwUqY5h+g+hSQ4KLIfDm0qvLQ==" + .signRequest(NewUnsignedRequest.fromStringBodyUtf8(httpMethod = "GET", uri = URI_EMPTY_PATH, body = SIMPLE_REQUEST_BODY, headers = Map.empty)) + .mauthHeaders(MAuthRequest.X_MWS_AUTHENTICATION_HEADER_NAME) shouldBe EXPECTED_AUTH_SIMPLE_BODY_V1 } it should "add authentication header to a request with body and params" in { val authHeaders = signer.signRequest(unsignedRequest).mauthHeaders - authHeaders(MAuthRequest.X_MWS_AUTHENTICATION_HEADER_NAME) shouldBe EXPECTED_GET_X_MWS_AUTHENTICATION_HEADER - authHeaders(MAuthRequest.MCC_AUTHENTICATION_HEADER_NAME) shouldBe EXPECTED_GET_MCC_AUTHENTICATION_HEADER + authHeaders(MAuthRequest.X_MWS_AUTHENTICATION_HEADER_NAME) shouldBe EXPECTED_AUTH_BODY_AND_PARAM_V1 + authHeaders(MAuthRequest.MCC_AUTHENTICATION_HEADER_NAME) shouldBe EXPECTED_AUTH_BODY_AND_PARAM_V2 } "MAuthRequestSigner with V2 only enabled" should "add time header to a request for V2 only " in { val authHeaders = signerV2.signRequest(simpleNewUnsignedRequest).mauthHeaders authHeaders.get(MAuthRequest.X_MWS_TIME_HEADER_NAME) shouldBe None - authHeaders(MAuthRequest.MCC_TIME_HEADER_NAME) shouldBe EXPECTED_GET_TIME_HEADER + authHeaders(MAuthRequest.MCC_TIME_HEADER_NAME) shouldBe EXPECTED_TIME_HEADER_1 } it should "add authentication header to a request for V2 only" in { val authHeaders = signerV2.signRequest(simpleNewUnsignedRequest).mauthHeaders authHeaders.get(MAuthRequest.X_MWS_AUTHENTICATION_HEADER_NAME) shouldBe None - authHeaders(MAuthRequest.MCC_AUTHENTICATION_HEADER_NAME) shouldBe EXPECTED_GET_MCC_AUTHENTICATION_HEADER_FOR_SIMPLE_REQUEST + authHeaders(MAuthRequest.MCC_AUTHENTICATION_HEADER_NAME) shouldBe EXPECTED_AUTH_NO_BODY_V2 } it should "add authentication header to a request with body and params for V2 only" in { val authHeaders = signerV2.signRequest(unsignedRequest).mauthHeaders authHeaders.get(MAuthRequest.X_MWS_AUTHENTICATION_HEADER_NAME) shouldBe None - authHeaders(MAuthRequest.MCC_AUTHENTICATION_HEADER_NAME) shouldBe EXPECTED_GET_MCC_AUTHENTICATION_HEADER + authHeaders(MAuthRequest.MCC_AUTHENTICATION_HEADER_NAME) shouldBe EXPECTED_AUTH_BODY_AND_PARAM_V2 } it should "add authentication header to a request for V2 with the encoded-normalize path" in { - val TEST_UUID = FixturesLoader.APP_UUID_V2 - val EXPECTED_SIGNATURE_V2 = FixturesLoader.SIGNATURE_NORMALIZE_PATH_V2 + val TEST_UUID = APP_UUID_V2 + val EXPECTED_SIGNATURE_V2 = SIGNATURE_NORMALIZE_PATH_V2 val EXPECTED_AUTHENTICATION_HEADER = s"""MWSV2 $TEST_UUID:$EXPECTED_SIGNATURE_V2;""" - val eTimeProvider: EpochTimeProvider = new EpochTimeProvider() { override def inSeconds(): Long = FixturesLoader.EPOCH_TIME_V2.toLong } - val newSigner: MAuthRequestSigner = MAuthRequestSigner(UUID.fromString(TEST_UUID), FixturesLoader.getPrivateKey2, eTimeProvider, v2OnlySignRequests = true) + val eTimeProvider: EpochTimeProvider = new EpochTimeProvider() { override def inSeconds(): Long = EPOCH_TIME.toLong } + val newSigner: MAuthRequestSigner = MAuthRequestSigner(UUID.fromString(TEST_UUID), PRIVATE_KEY_2, eTimeProvider, v2OnlySignRequests = true) newSigner .signRequest( - NewUnsignedRequest.fromStringBodyUtf8(httpMethod = "GET", uri = new URI(FixturesLoader.REQUEST_NORMALIZE_PATH), body = "", headers = Map.empty) + NewUnsignedRequest.fromStringBodyUtf8(httpMethod = "GET", uri = new URI(REQUEST_NORMALIZE_PATH), body = "", headers = Map.empty) ) .mauthHeaders(MAuthRequest.MCC_AUTHENTICATION_HEADER_NAME) shouldBe EXPECTED_AUTHENTICATION_HEADER } diff --git a/modules/mauth-signer-apachehttp/src/test/scala/com/mdsol/mauth/apache/HttpClientRequestSignerSpec.scala b/modules/mauth-signer-apachehttp/src/test/scala/com/mdsol/mauth/apache/HttpClientRequestSignerSpec.scala index 4ea25ad4..01797ea0 100644 --- a/modules/mauth-signer-apachehttp/src/test/scala/com/mdsol/mauth/apache/HttpClientRequestSignerSpec.scala +++ b/modules/mauth-signer-apachehttp/src/test/scala/com/mdsol/mauth/apache/HttpClientRequestSignerSpec.scala @@ -4,7 +4,7 @@ import java.security.Security import java.util.UUID import com.mdsol.mauth.MAuthRequest -import com.mdsol.mauth.test.utils.FixturesLoader +import com.mdsol.mauth.test.utils.TestFixtures import com.mdsol.mauth.util.EpochTimeProvider import org.apache.http.client.methods.{HttpGet, HttpPost} import org.apache.http.entity.StringEntity @@ -20,7 +20,7 @@ class HttpClientRequestSignerSpec extends AnyFlatSpec with Matchers with MockFac private val testUUID = UUID.fromString("2a6790ab-f6c6-45be-86fc-9e9be76ec12a") private val AUTHENTICATION_HEADER_PATTERN_V2 = s"MWSV2 $testUUID:[^;]*;" private val TEST_REQUEST_BODY = "Request Body" - private val privateKeyString = FixturesLoader.getPrivateKey + private val privateKeyString = TestFixtures.PRIVATE_KEY_1 private val mockEpochTimeProvider = mock[EpochTimeProvider] private val mAuthRequestSigner = new HttpClientRequestSigner(testUUID, privateKeyString, mockEpochTimeProvider) private val mAuthRequestSignerV2 = new HttpClientRequestSigner(testUUID, privateKeyString, mockEpochTimeProvider, true) @@ -111,15 +111,15 @@ class HttpClientRequestSignerSpec extends AnyFlatSpec with Matchers with MockFac it should "sign requests adds expected headers for V2 with the encoded-normalize path" in { //noinspection ConvertibleToMethodValue - val TEST_UUID = FixturesLoader.APP_UUID_V2 - val request_time = FixturesLoader.EPOCH_TIME_V2.toLong - val EXPECTED_SIGNATURE_V2 = FixturesLoader.SIGNATURE_NORMALIZE_PATH_V2 + val TEST_UUID = TestFixtures.APP_UUID_V2 + val request_time = TestFixtures.EPOCH_TIME.toLong + val EXPECTED_SIGNATURE_V2 = TestFixtures.SIGNATURE_NORMALIZE_PATH_V2 val EXPECTED_AUTHENTICATION_HEADER = s"""MWSV2 $TEST_UUID:$EXPECTED_SIGNATURE_V2;""" (mockEpochTimeProvider.inSeconds _: () => Long).expects().returns(request_time) - val mAuthSigner = new HttpClientRequestSigner(UUID.fromString(TEST_UUID), FixturesLoader.getPrivateKey2, mockEpochTimeProvider, true) + val mAuthSigner = new HttpClientRequestSigner(UUID.fromString(TEST_UUID), TestFixtures.PRIVATE_KEY_2, mockEpochTimeProvider, true) - val get = new HttpGet("http://mauth.imedidata.com" + FixturesLoader.REQUEST_NORMALIZE_PATH) + val get = new HttpGet("http://mauth.imedidata.com" + TestFixtures.REQUEST_NORMALIZE_PATH) mAuthSigner.signRequest(get) get.getFirstHeader(MAuthRequest.MCC_AUTHENTICATION_HEADER_NAME).getValue shouldBe EXPECTED_AUTHENTICATION_HEADER get.getFirstHeader(MAuthRequest.MCC_TIME_HEADER_NAME).getValue shouldBe String.valueOf(request_time) diff --git a/modules/mauth-signer-sttp/src/main/scala/com/mdsol/mauth/MAuthSttpSigner.scala b/modules/mauth-signer-sttp/src/main/scala/com/mdsol/mauth/MAuthSttpSigner.scala new file mode 100644 index 00000000..8c1dcc92 --- /dev/null +++ b/modules/mauth-signer-sttp/src/main/scala/com/mdsol/mauth/MAuthSttpSigner.scala @@ -0,0 +1,61 @@ +package com.mdsol.mauth + +import java.nio.charset.StandardCharsets +import java.security.PrivateKey +import java.util.UUID + +import com.mdsol.mauth.util.EpochTimeProvider + +import scala.jdk.CollectionConverters._ +import sttp.client.{BasicRequestBody, ByteArrayBody, ByteBufferBody, FileBody, InputStreamBody, MultipartBody, NoBody, Request, StreamBody, StringBody} +import sttp.model.Header + +trait MAuthSttpSigner { + def signSttpRequest[T](request: Request[T, Nothing]): Request[T, Nothing] +} + +/** Sign an sttp request by adding MAuth headers to the request */ +class MAuthSttpSignerImpl(signer: Signer) extends MAuthSttpSigner { + + def this(appUuid: UUID, privateKey: PrivateKey, epochTimeProvider: EpochTimeProvider, v2OnlySignRequest: Boolean = false) = { + this(new DefaultSigner(appUuid, privateKey, epochTimeProvider, v2OnlySignRequest)) + } + + def signSttpRequest[T](request: Request[T, Nothing]): Request[T, Nothing] = { + val bodyBytes: Array[Byte] = request.body match { + case NoBody => Array.empty[Byte] + case body: BasicRequestBody => + body match { + case strBody: StringBody => strBody.s.getBytes(StandardCharsets.UTF_8) + case ByteArrayBody(bytes, _) => bytes + case ByteBufferBody(byteBuffer, _) => byteBuffer.array() + // $COVERAGE-OFF$ + case _: InputStreamBody => + throw new IllegalArgumentException("Request with InputStream body not supported for mauth signing") + case _: FileBody => + throw new IllegalArgumentException("MAuth signing not yet implemented for request with multipart body") + } + case StreamBody(_) => + throw new IllegalArgumentException("Request with stream body not supported for mauth signing") + case MultipartBody(_) => + throw new IllegalArgumentException("MAuth signing not yet implemented for request with multipart body") + // $COVERAGE-ON$ + } + + val requestUri = request.uri.toJavaUri + val mauthHeaders: List[Header] = SignerUtils + .signWithUri( + signer, + request.method.method, + requestUri, + bodyBytes + ) + .asScala + .map { + case (k, v) => + Header(k, v) + } + .toList + request.headers(mauthHeaders: _*) + } +} diff --git a/modules/mauth-signer-sttp/src/main/scala/com/mdsol/mauth/SttpAkkaMAuthRequestSender.scala b/modules/mauth-signer-sttp/src/main/scala/com/mdsol/mauth/SttpAkkaMAuthRequestSender.scala new file mode 100644 index 00000000..490b24f4 --- /dev/null +++ b/modules/mauth-signer-sttp/src/main/scala/com/mdsol/mauth/SttpAkkaMAuthRequestSender.scala @@ -0,0 +1,21 @@ +package com.mdsol.mauth + +import akka.stream.scaladsl.Source +import akka.util.ByteString +import sttp.client.{Request, Response, SttpBackend} +import cats.effect.{ContextShift, IO} + +import scala.concurrent.Future + +class SttpAkkaMAuthRequestSender( + signer: MAuthSttpSigner, + sttpBackend: SttpBackend[Future, Source[ByteString, Any], Nothing], + contextShift: ContextShift[IO] +) extends SttpMAuthRequestSender[IO] { + override def send[T](request: Request[T, Nothing]): IO[Response[T]] = + IO.fromFuture( + IO( + signer.signSttpRequest(request).send()(sttpBackend, implicitly) + ) + )(contextShift) +} diff --git a/modules/mauth-signer-sttp/src/main/scala/com/mdsol/mauth/SttpMAuthRequestSender.scala b/modules/mauth-signer-sttp/src/main/scala/com/mdsol/mauth/SttpMAuthRequestSender.scala new file mode 100644 index 00000000..96d8d927 --- /dev/null +++ b/modules/mauth-signer-sttp/src/main/scala/com/mdsol/mauth/SttpMAuthRequestSender.scala @@ -0,0 +1,9 @@ +package com.mdsol.mauth + +import sttp.client.{Request, Response} + +trait SttpMAuthRequestSender[F[_]] { + + /** Takes an unsigned sttp request, populate the request headers with MAuth signatures and then sends it */ + def send[T](request: Request[T, Nothing]): F[Response[T]] +} diff --git a/modules/mauth-signer-sttp/src/test/scala/com/mdsol/mauth/MAuthSttpSignerSpec.scala b/modules/mauth-signer-sttp/src/test/scala/com/mdsol/mauth/MAuthSttpSignerSpec.scala new file mode 100644 index 00000000..b4c9ce04 --- /dev/null +++ b/modules/mauth-signer-sttp/src/test/scala/com/mdsol/mauth/MAuthSttpSignerSpec.scala @@ -0,0 +1,116 @@ +package com.mdsol.mauth + +import java.net.URI +import java.security.Security +import java.util.UUID + +import org.scalatest.wordspec.AnyWordSpec +import sttp.model.Uri +import sttp.client._ +import com.mdsol.mauth.test.utils.TestFixtures._ +import org.scalatest.matchers.should.Matchers +import MAuthSttpSignerSpec._ +import com.mdsol.mauth.util.EpochTimeProvider +import com.mdsol.mauth.util.MAuthKeysHelper.getPrivateKeyFromString +import org.bouncycastle.jce.provider.BouncyCastleProvider + +class MAuthSttpSignerSpec extends AnyWordSpec with Matchers { + + Security.addProvider(new BouncyCastleProvider) + + val emptyPathNoBodyReq = basicRequest.get(Uri(URI_EMPTY_PATH)) + val emptyPathWithSimpleBodyReq = basicRequest.get(Uri(URI_EMPTY_PATH)).body(SIMPLE_REQUEST_BODY) + + "signSttpRequest" should { + "add authentication and time header to a request for V1" in { + val signedReq = v1v2Signer.signSttpRequest(emptyPathNoBodyReq) + signedReq.getV1TimeHeaderValue shouldBe Some(EXPECTED_TIME_HEADER_1) + signedReq.getV1AuthHeaderValue shouldBe Some(EXPECTED_AUTH_NO_BODY_V1) + + signedReq.getV2TimeHeaderValue shouldBe Some(EXPECTED_TIME_HEADER_1) + signedReq.getV2AuthHeaderValue shouldBe Some(EXPECTED_AUTH_NO_BODY_V2) + } + + "add authentication header to a request with body for V1 and V2" in { + val signedReq = v1v2Signer.signSttpRequest(emptyPathWithSimpleBodyReq) + signedReq.getV1AuthHeaderValue shouldBe Some(EXPECTED_AUTH_SIMPLE_BODY_V1) + signedReq.getV2AuthHeaderValue shouldBe Some(EXPECTED_AUTH_SIMPLE_BODY_V2) + } + + "add authentication header to a request with body and params" in { + val req = basicRequest.get(Uri(URI_EMPTY_PATH_WITH_PARAM)).body(SIMPLE_REQUEST_BODY) + val signedReq = v1v2Signer.signSttpRequest(req) + signedReq.getV1AuthHeaderValue shouldBe Some(EXPECTED_AUTH_BODY_AND_PARAM_V1) + signedReq.getV2AuthHeaderValue shouldBe Some(EXPECTED_AUTH_BODY_AND_PARAM_V2) + } + + "When v2 only is set" should { + "add v2 uth and time header to the request (no v1 headers)" in { + val signedReq = v2OnlySigner.signSttpRequest(emptyPathNoBodyReq) + signedReq.getV1TimeHeaderValue shouldBe None + signedReq.getV1AuthHeaderValue shouldBe None + + signedReq.getV2TimeHeaderValue shouldBe Some(EXPECTED_TIME_HEADER_1) + signedReq.getV2AuthHeaderValue shouldBe Some(EXPECTED_AUTH_NO_BODY_V2) + } + + "add authentication header to a request with body and params for V2 only" in { + val signedReq = v2OnlySigner.signSttpRequest(emptyPathWithSimpleBodyReq) + signedReq.getV1TimeHeaderValue shouldBe None + signedReq.getV1AuthHeaderValue shouldBe None + + signedReq.getV2TimeHeaderValue shouldBe Some(EXPECTED_TIME_HEADER_1) + signedReq.getV2AuthHeaderValue shouldBe Some(EXPECTED_AUTH_SIMPLE_BODY_V2) + } + + "add authentication header to a request for V2 with the encoded-normalize path" in { + import sttp.client._ + + val epochTimeProvider: EpochTimeProvider = () => EPOCH_TIME.toLong + + val signer = new MAuthSttpSignerImpl( + appUuid = UUID.fromString(APP_UUID_V2), + privateKey = getPrivateKeyFromString(PRIVATE_KEY_2), + epochTimeProvider = epochTimeProvider, + v2OnlySignRequest = true + ) + val req = basicRequest.get(Uri(new URI(s"http://host.com$REQUEST_NORMALIZE_PATH"))).body("") + val signedReq = signer.signSttpRequest(req) + + signedReq.getV2TimeHeaderValue shouldBe Some(EPOCH_TIME) + signedReq.getV2AuthHeaderValue shouldBe Some(s"""MWSV2 $APP_UUID_V2:$SIGNATURE_NORMALIZE_PATH_V2;""") + } + + } + + } + + lazy val CONST_EPOCH_TIME_PROVIDER: EpochTimeProvider = new EpochTimeProvider() { override def inSeconds(): Long = EXPECTED_TIME_HEADER_1.toLong } + + lazy val v1v2Signer = new MAuthSttpSignerImpl( + appUuid = UUID.fromString(APP_UUID_1), + privateKey = getPrivateKeyFromString(PRIVATE_KEY_1), + epochTimeProvider = CONST_EPOCH_TIME_PROVIDER, + v2OnlySignRequest = false + ) + + lazy val v2OnlySigner = new MAuthSttpSignerImpl( + appUuid = UUID.fromString(APP_UUID_1), + privateKey = getPrivateKeyFromString(PRIVATE_KEY_1), + epochTimeProvider = CONST_EPOCH_TIME_PROVIDER, + v2OnlySignRequest = true + ) + +} + +object MAuthSttpSignerSpec { + implicit class SttpRequestExtensions[T](val req: Request[T, Nothing]) extends AnyVal { + def getHeaderValue(str: String): Option[String] = + req.headers.find(_.is(str)).map(_.value) + + def getV1TimeHeaderValue: Option[String] = getHeaderValue(TIME_HEADER_V1) + def getV2TimeHeaderValue: Option[String] = getHeaderValue(TIME_HEADER_V2) + def getV1AuthHeaderValue: Option[String] = getHeaderValue(AUTH_HEADER_V1) + def getV2AuthHeaderValue: Option[String] = getHeaderValue(AUTH_HEADER_V2) + } +} diff --git a/modules/mauth-signer-sttp/src/test/scala/com/mdsol/mauth/SttpAkkaMAuthRequestSenderSpec.scala b/modules/mauth-signer-sttp/src/test/scala/com/mdsol/mauth/SttpAkkaMAuthRequestSenderSpec.scala new file mode 100644 index 00000000..4adc98a7 --- /dev/null +++ b/modules/mauth-signer-sttp/src/test/scala/com/mdsol/mauth/SttpAkkaMAuthRequestSenderSpec.scala @@ -0,0 +1,115 @@ +package com.mdsol.mauth + +import java.net.URI +import java.security.Security +import java.util.UUID + +import akka.actor.ActorSystem +import akka.stream.scaladsl.Source +import akka.util.ByteString +import cats.effect.IO +import com.github.tomakehurst.wiremock.WireMockServer +import com.github.tomakehurst.wiremock.client.WireMock._ +import org.scalatest.{BeforeAndAfter, BeforeAndAfterAll} +import org.scalatest.wordspec.AsyncWordSpec +import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig +import com.mdsol.mauth.util.EpochTimeProvider +import sttp.client.{basicRequest, SttpBackend} +import sttp.model.{MediaType, Uri} +import sttp.client.akkahttp.AkkaHttpBackend + +import scala.jdk.CollectionConverters._ +import org.scalatest.Inside._ +import org.scalatest.matchers.should.Matchers._ +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.MAuthKeysHelper.getPrivateKeyFromString +import org.bouncycastle.jce.provider.BouncyCastleProvider + +import scala.concurrent.{ExecutionContext, Future} + +class SttpAkkaMAuthRequestSenderSpec extends AsyncWordSpec with BeforeAndAfter with BeforeAndAfterAll { + + Security.addProvider(new BouncyCastleProvider) + + val wiremockServer: WireMockServer = new WireMockServer(wireMockConfig.dynamicPort()) + + val actorSystem = ActorSystem(this.getClass.getSimpleName) + val sttpBackend: SttpBackend[Future, Source[ByteString, Any], Nothing] = AkkaHttpBackend.usingActorSystem(actorSystem) + + "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 + .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 + .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, + false + ) + } + + lazy val requestSender = new SttpAkkaMAuthRequestSender(v1v2Signer, sttpBackend, IO.contextShift(ExecutionContext.global)) + + 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() + val _ = actorSystem.terminate() + } +} diff --git a/modules/mauth-signer/src/main/java/com/mdsol/mauth/Signer.java b/modules/mauth-signer/src/main/java/com/mdsol/mauth/Signer.java index fd902fef..81bb5119 100644 --- a/modules/mauth-signer/src/main/java/com/mdsol/mauth/Signer.java +++ b/modules/mauth-signer/src/main/java/com/mdsol/mauth/Signer.java @@ -34,7 +34,7 @@ Map generateRequestHeaders(String httpVerb, String requestPath, * @param httpVerb The HTTP verb of the request, e.g. GET, POST, etc. * @param requestPath The path of the request, not including protocol, host or query parameters. * @param requestPayload The payload of the request - * @param queryParameters The query parameters + * @param queryParameters The query parameters (URL-encoded) * @return MAuth headers which should be appended to the request before sending. * @throws MAuthSigningException when request cannot be signed */ diff --git a/modules/mauth-signer/src/main/java/com/mdsol/mauth/SignerUtils.java b/modules/mauth-signer/src/main/java/com/mdsol/mauth/SignerUtils.java new file mode 100644 index 00000000..9f4d34e7 --- /dev/null +++ b/modules/mauth-signer/src/main/java/com/mdsol/mauth/SignerUtils.java @@ -0,0 +1,28 @@ +package com.mdsol.mauth; + +import com.mdsol.mauth.exceptions.MAuthSigningException; + +import java.net.URI; +import java.util.Map; + +public class SignerUtils { + /** + * Sign a request with the provided Signer. Request path and query parameter are extracted from the Java URI + * This helper function is provided to avoid confusion with whether the path / query string needs to be encoded or not + * + * @param signer + * @param httpVerb + * @param uri + * @param requestPayload + * @return + * @throws MAuthSigningException + */ + public static Map signWithUri(Signer signer, String httpVerb, URI uri, byte[] requestPayload) throws MAuthSigningException { + return signer.generateRequestHeaders( + httpVerb, + uri.getRawPath(), + requestPayload, + uri.getRawQuery() + ); + } +} diff --git a/modules/mauth-signer/src/test/scala/com/mdsol/mauth/DefaultSignerSpec.scala b/modules/mauth-signer/src/test/scala/com/mdsol/mauth/DefaultSignerSpec.scala index f0c49be5..f13a010e 100644 --- a/modules/mauth-signer/src/test/scala/com/mdsol/mauth/DefaultSignerSpec.scala +++ b/modules/mauth-signer/src/test/scala/com/mdsol/mauth/DefaultSignerSpec.scala @@ -4,7 +4,7 @@ import java.security.Security import java.util.UUID import com.mdsol.mauth.exceptions.MAuthKeyException -import com.mdsol.mauth.test.utils.FixturesLoader +import com.mdsol.mauth.test.utils.TestFixtures import com.mdsol.mauth.util.EpochTimeProvider import org.bouncycastle.jce.provider.BouncyCastleProvider import org.scalamock.scalatest.MockFactory @@ -21,8 +21,8 @@ class DefaultSignerSpec extends AnyFlatSpec with Matchers with MockFactory { private val AUTHENTICATION_HEADER_PATTERN_V2: String = s"MWSV2 $testUUID:[^;]*;" private val mockEpochTimeProvider = mock[EpochTimeProvider] - private val mAuthRequestSigner = new DefaultSigner(testUUID, FixturesLoader.getPrivateKey, mockEpochTimeProvider) - private val mAuthRequestSignerV2 = new DefaultSigner(testUUID, FixturesLoader.getPrivateKey, mockEpochTimeProvider, true) + private val mAuthRequestSigner = new DefaultSigner(testUUID, TestFixtures.PRIVATE_KEY_1, mockEpochTimeProvider) + private val mAuthRequestSignerV2 = new DefaultSigner(testUUID, TestFixtures.PRIVATE_KEY_1, mockEpochTimeProvider, true) Security.addProvider(new BouncyCastleProvider) @@ -114,7 +114,7 @@ class DefaultSignerSpec extends AnyFlatSpec with Matchers with MockFactory { it should "generated headers for both V1 and V2 if V2 only is disabled" in { //noinspection ConvertibleToMethodValue - val mAuthSigner = new DefaultSigner(testUUID, FixturesLoader.getPrivateKey, mockEpochTimeProvider, false) + val mAuthSigner = new DefaultSigner(testUUID, TestFixtures.PRIVATE_KEY_1, mockEpochTimeProvider, false) (mockEpochTimeProvider.inSeconds _: () => Long).expects().returns(TEST_EPOCH_TIME) val EXPECTED_POST_AUTHENTICATION_HEADER_V1: String = s"""MWS $testUUID:aDItoM9IOknNhPKH9a @@ -141,16 +141,16 @@ class DefaultSignerSpec extends AnyFlatSpec with Matchers with MockFactory { it should "generated headers for binary payload for both V1 and V2 if V2 only is disabled" in { //noinspection ConvertibleToMethodValue - val CLIENT_REQUEST_BINARY_APP_UUID = FixturesLoader.APP_UUID_V2 - val CLIENT_REQUEST_BINARY_EPOCH_TIME: Long = FixturesLoader.EPOCH_TIME_V2.toLong - val CLIENT_REQUEST_BINARY_PATH = FixturesLoader.REQUEST_PATH_V2 - val CLIENT_REQUEST_QUERY_PARAMETERS = FixturesLoader.REQUEST_QUERY_PARAMETERS_V2 - val mAuthSigner = new DefaultSigner(UUID.fromString(CLIENT_REQUEST_BINARY_APP_UUID), FixturesLoader.getPrivateKey2, mockEpochTimeProvider, false) + val CLIENT_REQUEST_BINARY_APP_UUID = TestFixtures.APP_UUID_V2 + val CLIENT_REQUEST_BINARY_EPOCH_TIME: Long = TestFixtures.EPOCH_TIME.toLong + val CLIENT_REQUEST_BINARY_PATH = TestFixtures.REQUEST_PATH_V2 + val CLIENT_REQUEST_QUERY_PARAMETERS = TestFixtures.REQUEST_QUERY_PARAMETERS_V2 + val mAuthSigner = new DefaultSigner(UUID.fromString(CLIENT_REQUEST_BINARY_APP_UUID), TestFixtures.PRIVATE_KEY_2, mockEpochTimeProvider, false) (mockEpochTimeProvider.inSeconds _: () => Long).expects().returns(CLIENT_REQUEST_BINARY_EPOCH_TIME) - val EXPECTED_AUTHENTICATION_HEADER_V1: String = s"""MWS $CLIENT_REQUEST_BINARY_APP_UUID:${FixturesLoader.SIGNATURE_V1_BINARY}""" - val EXPECTED_AUTHENTICATION_HEADER_V2: String = s"""MWSV2 $CLIENT_REQUEST_BINARY_APP_UUID:${FixturesLoader.SIGNATURE_V2_BINARY};""" + val EXPECTED_AUTHENTICATION_HEADER_V1: String = s"""MWS $CLIENT_REQUEST_BINARY_APP_UUID:${TestFixtures.SIGNATURE_V1_BINARY}""" + val EXPECTED_AUTHENTICATION_HEADER_V2: String = s"""MWSV2 $CLIENT_REQUEST_BINARY_APP_UUID:${TestFixtures.SIGNATURE_V2_BINARY};""" val headers: Map[String, String] = - mAuthSigner.generateRequestHeaders("PUT", CLIENT_REQUEST_BINARY_PATH, FixturesLoader.getBinaryFileBody, CLIENT_REQUEST_QUERY_PARAMETERS).asScala.toMap + mAuthSigner.generateRequestHeaders("PUT", CLIENT_REQUEST_BINARY_PATH, TestFixtures.BINARY_FILE_BODY, CLIENT_REQUEST_QUERY_PARAMETERS).asScala.toMap headers(MAuthRequest.X_MWS_AUTHENTICATION_HEADER_NAME) shouldBe EXPECTED_AUTHENTICATION_HEADER_V1 headers(MAuthRequest.MCC_AUTHENTICATION_HEADER_NAME) shouldBe EXPECTED_AUTHENTICATION_HEADER_V2 headers(MAuthRequest.X_MWS_TIME_HEADER_NAME) shouldBe String.valueOf(CLIENT_REQUEST_BINARY_EPOCH_TIME) diff --git a/modules/mauth-test-utils/src/main/java/com/mdsol/mauth/test/utils/FixturesLoader.java b/modules/mauth-test-utils/src/main/java/com/mdsol/mauth/test/utils/FixturesLoader.java deleted file mode 100644 index eb86165c..00000000 --- a/modules/mauth-test-utils/src/main/java/com/mdsol/mauth/test/utils/FixturesLoader.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.mdsol.mauth.test.utils; - -import org.apache.commons.io.IOUtils; - -import java.io.IOException; -import java.nio.charset.Charset; - -public class FixturesLoader { - - // fixtures of Cross platform testing for mAuth signatures (match with PrivateKey2 & PublicKey2) - public final static String APP_UUID_V2 ="5ff4257e-9c16-11e0-b048-0026bbfffe5e"; - public final static String EPOCH_TIME_V2 = "1309891855"; - - public final static String REQUEST_METHOD_V2 = "PUT"; - public final static String REQUEST_PATH_V2 = "/v1/pictures"; - public final static String REQUEST_QUERY_PARAMETERS_V2 = "key=-_.~!@#$%^*()+{}|:\"'`<>?&∞=v&キ=v&0=v&a=v&a=b&a=c&a=a&k=&k=v"; - public final static String SIGNATURE_V1_BINARY = "hDKYDRnzPFL2gzsru4zn7c7E7KpEvexeF4F5IR+puDxYXrMmuT2/fETZty5NkGGTZQ1nI6BTYGQGsU/73TkEAm7SvbJZcB2duLSCn8H5D0S1cafory1gnL1TpMPBlY8J/lq/Mht2E17eYw+P87FcpvDShINzy8GxWHqfquBqO8ml4XtirVEtAlI0xlkAsKkVq4nj7rKZUMS85mzogjUAJn3WgpGCNXVU+EK+qElW5QXk3I9uozByZhwBcYt5Cnlg15o99+53wKzMMmdvFmVjA1DeUaSO7LMIuw4ZNLVdDcHJx7ZSpAKZ/EA34u1fYNECFcw5CSKOjdlU7JFr4o8Phw=="; - public final static String SIGNATURE_V2_BINARY = "kNmQchPnfSZOo29GHHDcp+res452+IIiWK/h7HmPdFsTU510X+eWPLaYONmfd2fMAuVLncDAiOPxyOS4WXap69szL37k9537ujnEU15I+j+vINTspCnAIbtZ9ia35c+gQyPgNQo7F1RxNl1P3hfXJ4qNXIrMSc/DlKpieNzmXQFPFs9zZxK5VPvdS0QBsuQFSMN71o2Rupf+NRStxvH55pVej/mjJj4PbeCgAX2N6Vi0dqU2GLgcx+0U5j5FphLUIdqF6/6FKRqPRSCLX5hEyFf2c4stRnNWSpP/y/gGFtdIVxFzKEe42cL3FmYSM4YFTKn3wGgViw0W+CzkbDXJqQ=="; - public final static String SIGNATURE_V2_EMPTY = "rUB9ZnFqqjuNUv5new2vAplAjOh5eTEMYJjMm2G32jkPYqIhUmffEErkWbzOIrHzVfsW9zwgyEO+QRmGSvDDkYSa0ecDQ0/HDUokQoQ0yuZGInztXDJVPDKVy6MDxLcNwnwFo1qHtANL3dYrGAai11AamPU2Hvjf2BcNYLnryIbP3/8ChLRbQTu9rw/v7bmC+owG1BchLIBuBhsr33yiuGD/AfbxqOD9jVb3rZsFvM1/O6aLzT3enNgKxWlpZl+vgnnIHBtdiYl/HGLoX5BMuNsaxpDT7OS4cBIsgkkszQe17vNotnMUUV2WklOZ27x7Uv77LbrY0LgEdzofkv6v3Q=="; - - public final static String REQUEST_NORMALIZE_PATH = "/%cf%80"; - public final static String SIGNATURE_NORMALIZE_PATH_V2 = "QtD4t3uO5xYRhT/QDxJm3Dd9zH79hLsUjL9IRvUhana8cS9/JE7dxpsBC17Owh+6cSvTENa1FjKqcPoRgZKACR/pRqmx43+Ha2twdWN2p+Zt+plBZ56ylTv1yKpOCEO6FM/QPEnJtY1DezKmu/EILhkfaLdVh5JTq665SeD1LRv46MVyhAN1BoqQPJI/BLddQRpmmTvmPtKBK5VFhcJs6ZfD2YgFxsKajhGE+posgRjkQ18D1CV54v1kQBK9iiImN1h8G1k/bDKfDjpgTiC7A+n5kJa5PWU+pP9rseGnagEV2bIiA67cObLqRH7kjnKI8PK63cDKJoO7Zfv4zFnrbw=="; - - public static String getPublicKey() { - try { - return IOUtils.toString(FixturesLoader.class.getResourceAsStream("/keys/fake_publickey.pem"), Charset.defaultCharset()); - } catch (IOException ex) { - throw new IllegalStateException("Unable to load the public key.", ex); - } - } - - public static String getPublicKey2() { - try { - return IOUtils.toString(FixturesLoader.class.getResourceAsStream("/keys/fake_publickey2.pem"), Charset.defaultCharset()); - } catch (IOException ex) { - throw new IllegalStateException("Unable to load the public key2.", ex); - } - } - - public static String getPrivateKey() { - try { - return IOUtils.toString(FixturesLoader.class.getResourceAsStream("/keys/fake_privatekey.pem"), Charset.defaultCharset()); - } catch (IOException ex) { - throw new IllegalStateException("Unable to load the private key.", ex); - } - } - - public static String getPrivateKey2() { - try { - return IOUtils.toString(FixturesLoader.class.getResourceAsStream("/keys/fake_privatekey2.pem"), Charset.defaultCharset()); - } catch (IOException ex) { - throw new IllegalStateException("Unable to load the private key2.", ex); - } - } - - public static byte[] getBinaryFileBody() { - try { - return IOUtils.toByteArray(FixturesLoader.class.getResourceAsStream("/blank.jpeg")); - } catch (IOException ex) { - throw new IllegalStateException("Unable to load blank.jpeg.", ex); - } - } -} diff --git a/modules/mauth-test-utils/src/main/java/com/mdsol/mauth/test/utils/TestFixtures.scala b/modules/mauth-test-utils/src/main/java/com/mdsol/mauth/test/utils/TestFixtures.scala new file mode 100644 index 00000000..83c6abdf --- /dev/null +++ b/modules/mauth-test-utils/src/main/java/com/mdsol/mauth/test/utils/TestFixtures.scala @@ -0,0 +1,86 @@ +package com.mdsol.mauth.test.utils + +import java.net.URI + +import org.apache.commons.io.IOUtils +import java.nio.charset.Charset + +object TestFixtures { // fixtures of Cross platform testing for mAuth signatures (match with PrivateKey2 & PublicKey2) + + val TIME_HEADER_V1 = "x-mws-time" + val AUTH_HEADER_V1 = "x-mws-authentication" + val TIME_HEADER_V2 = "mcc-time" + val AUTH_HEADER_V2 = "mcc-authentication" + + val APP_UUID_1 = "2a6790ab-f6c6-45be-86fc-9e9be76ec12a" + val APP_UUID_V2 = "5ff4257e-9c16-11e0-b048-0026bbfffe5e" + val EXPECTED_TIME_HEADER_1 = "1509041057" + val EPOCH_TIME = "1309891855" + + val REQUEST_METHOD_V2 = "PUT" + val REQUEST_PATH_V2 = "/v1/pictures" + val REQUEST_QUERY_PARAMETERS_V2 = "key=-_.~!@#$%^*()+{}|:\"'`<>?&∞=v&キ=v&0=v&a=v&a=b&a=c&a=a&k=&k=v" + val SIGNATURE_V1_BINARY = + "hDKYDRnzPFL2gzsru4zn7c7E7KpEvexeF4F5IR+puDxYXrMmuT2/fETZty5NkGGTZQ1nI6BTYGQGsU/73TkEAm7SvbJZcB2duLSCn8H5D0S1cafory1gnL1TpMPBlY8J/lq/Mht2E17eYw+P87FcpvDShINzy8GxWHqfquBqO8ml4XtirVEtAlI0xlkAsKkVq4nj7rKZUMS85mzogjUAJn3WgpGCNXVU+EK+qElW5QXk3I9uozByZhwBcYt5Cnlg15o99+53wKzMMmdvFmVjA1DeUaSO7LMIuw4ZNLVdDcHJx7ZSpAKZ/EA34u1fYNECFcw5CSKOjdlU7JFr4o8Phw==" + val SIGNATURE_V2_BINARY = + "kNmQchPnfSZOo29GHHDcp+res452+IIiWK/h7HmPdFsTU510X+eWPLaYONmfd2fMAuVLncDAiOPxyOS4WXap69szL37k9537ujnEU15I+j+vINTspCnAIbtZ9ia35c+gQyPgNQo7F1RxNl1P3hfXJ4qNXIrMSc/DlKpieNzmXQFPFs9zZxK5VPvdS0QBsuQFSMN71o2Rupf+NRStxvH55pVej/mjJj4PbeCgAX2N6Vi0dqU2GLgcx+0U5j5FphLUIdqF6/6FKRqPRSCLX5hEyFf2c4stRnNWSpP/y/gGFtdIVxFzKEe42cL3FmYSM4YFTKn3wGgViw0W+CzkbDXJqQ==" + val SIGNATURE_V2_EMPTY = + "rUB9ZnFqqjuNUv5new2vAplAjOh5eTEMYJjMm2G32jkPYqIhUmffEErkWbzOIrHzVfsW9zwgyEO+QRmGSvDDkYSa0ecDQ0/HDUokQoQ0yuZGInztXDJVPDKVy6MDxLcNwnwFo1qHtANL3dYrGAai11AamPU2Hvjf2BcNYLnryIbP3/8ChLRbQTu9rw/v7bmC+owG1BchLIBuBhsr33yiuGD/AfbxqOD9jVb3rZsFvM1/O6aLzT3enNgKxWlpZl+vgnnIHBtdiYl/HGLoX5BMuNsaxpDT7OS4cBIsgkkszQe17vNotnMUUV2WklOZ27x7Uv77LbrY0LgEdzofkv6v3Q==" + val REQUEST_NORMALIZE_PATH = "/%cf%80" + val SIGNATURE_NORMALIZE_PATH_V1 = + "MKQr1ogwwzGqNOpv0mSSaA1iz1nbeve1ttmqCPtweEB0Wj+R93+i7eo67YO3CjXZ2nvQeMRzGc5TUyrSQtB3okG/QkYIriFgwhLAmwOXjiHaS2ZJVY8ngcaCGheC5hZchhftVXM+1j2akAfYWer5bAFddINI7UDOfavX83pDp81I3yGpPn2X1gncnn2UVzGiv0RvYT3vngb7KY1qavjC812YGv2/0w+7LSNmR3ZJAqqQ3mI8bEK45kSf1urKcuHr+/O8o7B3btE5F8j7qerVx3jbEEG9yK+syC0vKmf9bpiA9zkUiG50ZNewazotbsSSyb+0hWCDL+lr9Xl9FH5niw==" + val SIGNATURE_NORMALIZE_PATH_V2 = + "QtD4t3uO5xYRhT/QDxJm3Dd9zH79hLsUjL9IRvUhana8cS9/JE7dxpsBC17Owh+6cSvTENa1FjKqcPoRgZKACR/pRqmx43+Ha2twdWN2p+Zt+plBZ56ylTv1yKpOCEO6FM/QPEnJtY1DezKmu/EILhkfaLdVh5JTq665SeD1LRv46MVyhAN1BoqQPJI/BLddQRpmmTvmPtKBK5VFhcJs6ZfD2YgFxsKajhGE+posgRjkQ18D1CV54v1kQBK9iiImN1h8G1k/bDKfDjpgTiC7A+n5kJa5PWU+pP9rseGnagEV2bIiA67cObLqRH7kjnKI8PK63cDKJoO7Zfv4zFnrbw==" + + val URI_EMPTY_PATH = new URI("http://host.com/") + val URI_EMPTY_PATH_WITH_PARAM = new URI("http://host.com/?key2=data2&key1=data1") + val SIMPLE_REQUEST_BODY = "Request Body" + + val EXPECTED_AUTH_NO_BODY_V1: String = + s"""MWS $APP_UUID_1:ih3xq6OvQ2/D5ktPDaZ4F6tanzdn2XGzZ+KOaFXC+YKVjNcSCfUiKB6T/552K3AmKm/yZF4rdEOps + |MZ0QkuFqEZdwQ8R3iWUwdrNPsmNXSVvF50pRAlcI77UP5gUKV01xjZxfZ/M/vhzVn513bAgJ6CM8X4dtG20ki5xLsO3 + |5e2eZs5i9IwA/hEaKSm/PH2pEHwxz5c9MMGtHiFgzgXGacziVn0fr2c6X5jb3cDjHnfNVX8o57kFjL5E0YOoeEKDwHy + |flGhbfFNZys29jo83JCK2MQj9s+fZq5NsgmwuACRE6BnqKSPqwDWN4OK3N/iPcTwCsMKz/c5/3CEbMTlH8A==""".stripMargin.replaceAll("\n", "") + + val EXPECTED_AUTH_NO_BODY_V2: String = + s"""MWSV2 $APP_UUID_1:h0MJYf5/zlX9VqqchANLr7XUln0RydMV4msZSXzLq2sbr3X+TGeJ60K9ZSlSuRrzyHbzzwuZABA + |3P2j3l9t+gyBC1c/JSa8mldMrIXXYzp0lYLxLkghH09hm3k0pEW2la94K/Num3xgNymn6D/B9dJ1onRIgl+T+e/m4k6 + |T3apKHcV/6cJ9asm+jDjzB8OuCVWVsLZQKQbtiydUYNisYerKVxWPLs9SHNZ6GmAqq4ZCCpyEQZuMNF6cMmXgQ0Pxe9 + |X/yNA1Xc3Fakuga47lUQ6Bn7xvhkH6P+ZP0k4U7kidziXpxpkDts8fEXTpkvFX0PR7vaxjbMZzWsU413jyNsw==;""".stripMargin.replaceAll("\n", "") + + val EXPECTED_AUTH_SIMPLE_BODY_V1: String = "MWS " + + s"$APP_UUID_1:OoxiQ/Z6EjTUAoAGNKD5FS6ka+9IcWW5rtuzbXRDLRGj4pzSdeI0FPIlT0E/ZR96xR0a5EJlJ3E8usr" + + "5qas/uzNEDajAqpjqOaO4m3j+4juXt0QrdBvj3sgStD6ozOJrfhyeSWvFp3d9SBx8tPkPrqv6z5ewQliaSOaI20yir4+RStwj6P7j/5/ZlDRMBEFBiFuA" + + "yAWMAbKnefRwK+0yUqO9pEBQx43YqBzs+Xb9sTM0hKd5IohAW8O8xj1coBYP/NGRvhM5Z+VMXnbRXwkqUlEXIDvZ3fKjPNGEQxo+m9oFH1dLI8oGI9xoC9P3liwUqY5h+g+hSQ4KLIfDm0qvLQ==" + + val EXPECTED_AUTH_SIMPLE_BODY_V2: String = + s"MWSV2 $APP_UUID_1:qqdIEdENI5oxHpiSNCpKcG8s7JElNXf14IfMJ04L82tUvFa27o32Hg1Qz46qoFrFY9dvNxbz8W338/v0/Ce+865bZtxpK/RjhDtwd15ndUoARgILb8Q0y/j/JsCwWPSNBQGmWyUqsS0MnGP7TV80DcykyRjGml/jo853nzY/RrY786PmrwIvsLeLQ5lKNRPFLHO0fFAdLWclbZO2BXdI+qdtlVXUSRf5wLNRohcitscahrLalRW3uTtsa9i+IBwra9sVv8rZXC1HCjJ7T3FvUiw89nu5KQWWAftSww5vJIUggBmiIIzL/vCQ/d02LfZE/qrg3H/QyGzjxOtoNiVreA==;" + + val EXPECTED_AUTH_BODY_AND_PARAM_V1: String = + s"""MWS $APP_UUID_1:OoxiQ/Z6EjTUAoAGNKD5FS6ka+9IcWW5rtuzbXRDLRGj4pzSdeI0FPIlT0E/ZR96xR0a5EJlJ3E + |8usr5qas/uzNEDajAqpjqOaO4m3j+4juXt0QrdBvj3sgStD6ozOJrfhyeSWvFp3d9SBx8tPkPrqv6z5ewQliaSOaI + |20yir4+RStwj6P7j/5/ZlDRMBEFBiFuAyAWMAbKnefRwK+0yUqO9pEBQx43YqBzs+Xb9sTM0hKd5IohAW8O8xj1coB + |YP/NGRvhM5Z+VMXnbRXwkqUlEXIDvZ3fKjPNGEQxo+m9oFH1dLI8oGI9xoC9P3liwUqY5h+g+hSQ4KLIfDm0qvLQ==""".stripMargin.replaceAll("\n", "") + + val EXPECTED_AUTH_BODY_AND_PARAM_V2: String = + s"""MWSV2 $APP_UUID_1:n5+io+SgpPMgatLarleDkX18r1ZVBtp7YWgu3yeP0k/P8otp4ThEtBJ6Du3b2Pet+7xlkfK90 + |RXrcwiKA0SS8vpPX8nCtLa92hE3G1e0A41Cn00MuasVwV7JlkQeffJH8qQjvapwRsQ9dbFTPOktS4u0fm/7L9hI6k + |m99lqCP72i0tP7vGCst4Gc1OewGMR+60FUNR7eN66z8wbeXxX5gzMNGpppP/3P2YROGkONlsxbd1UxrEN62r6yQBF + |i9hTFF0FCqDM63UiLxTt3ooTpj4iUx/3htvPJ2AlSW5TaoviQUjQFYdb+CB6xDi0LFp93V5289lEXdPOVCULUGesqDA==;""".stripMargin.replaceAll("\n", "") + + val PUBLIC_KEY_1: String = + IOUtils.toString(this.getClass.getResourceAsStream("/keys/fake_publickey.pem"), Charset.defaultCharset) + + val PRIVATE_KEY_1: String = + IOUtils.toString(this.getClass.getResourceAsStream("/keys/fake_privatekey.pem"), Charset.defaultCharset) + + val PUBLIC_KEY_2: String = + IOUtils.toString(this.getClass.getResourceAsStream("/keys/fake_publickey2.pem"), Charset.defaultCharset) + + val PRIVATE_KEY_2: String = + IOUtils.toString(this.getClass.getResourceAsStream("/keys/fake_privatekey2.pem"), Charset.defaultCharset) + + val BINARY_FILE_BODY: Array[Byte] = + IOUtils.toByteArray(this.getClass.getResourceAsStream("/blank.jpeg")) + +} diff --git a/project/BuildSettings.scala b/project/BuildSettings.scala index 2a72f1f5..b50b14d7 100644 --- a/project/BuildSettings.scala +++ b/project/BuildSettings.scala @@ -55,6 +55,10 @@ object BuildSettings { "-P:silencer:pathFilters=target/.*" ) + lazy val noPublishSettings = Seq( + publish / skip := true + ) + lazy val publishSettings = Seq( sonatypeProfileName := "com.mdsol", publishMavenStyle := true, diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 0e17f415..1ec9f7ba 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -7,23 +7,28 @@ object Dependencies extends DependencyUtils { val akkaHttp = "10.1.11" val logback = "1.2.3" val silencer: String = "1.6.0" + val sttp = "2.1.5" } - val akkaHttp: ModuleID = "com.typesafe.akka" %% "akka-http" % Version.akkaHttp - val akkaStream: ModuleID = "com.typesafe.akka" %% "akka-stream" % Version.akka - val apacheHttpClient: ModuleID = "org.apache.httpcomponents" % "httpclient" % "4.5.11" - val bouncyCastlePkix: ModuleID = "org.bouncycastle" % "bcpkix-jdk15on" % "1.64" - val commonsCodec: ModuleID = "commons-codec" % "commons-codec" % "1.14" - val commonsLang3: ModuleID = "org.apache.commons" % "commons-lang3" % "3.9" - val guava: ModuleID = "com.google.guava" % "guava" % "23.0" - val jacksonDataBind: ModuleID = "com.fasterxml.jackson.core" % "jackson-databind" % "2.10.2" - val littleProxy: ModuleID = "org.littleshoot" % "littleproxy" % "1.1.2" - 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" % "1.7.30" - val typeSafeConfig: ModuleID = "com.typesafe" % "config" % "1.4.0" - val scalaCache: ModuleID = "com.github.cb372" %% "scalacache-guava" % "0.28.0" - val scalaLogging: ModuleID = "com.typesafe.scala-logging" %% "scala-logging" % "3.9.2" + val akkaHttp: ModuleID = "com.typesafe.akka" %% "akka-http" % Version.akkaHttp + val akkaStream: ModuleID = "com.typesafe.akka" %% "akka-stream" % Version.akka + val apacheHttpClient: ModuleID = "org.apache.httpcomponents" % "httpclient" % "4.5.11" + val bouncyCastlePkix: ModuleID = "org.bouncycastle" % "bcpkix-jdk15on" % "1.64" + val commonsCodec: ModuleID = "commons-codec" % "commons-codec" % "1.14" + val commonsLang3: ModuleID = "org.apache.commons" % "commons-lang3" % "3.9" + val guava: ModuleID = "com.google.guava" % "guava" % "23.0" + val jacksonDataBind: ModuleID = "com.fasterxml.jackson.core" % "jackson-databind" % "2.10.2" + val littleProxy: ModuleID = "org.littleshoot" % "littleproxy" % "1.1.2" + 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" % "1.7.30" + val typeSafeConfig: ModuleID = "com.typesafe" % "config" % "1.4.0" + val scalaCache: ModuleID = "com.github.cb372" %% "scalacache-guava" % "0.28.0" + val scalaLogging: ModuleID = "com.typesafe.scala-logging" %% "scala-logging" % "3.9.2" + val catsEffect: ModuleID = "org.typelevel" %% "cats-effect" % "2.1.3" + val sttp: ModuleID = "com.softwaremill.sttp.client" %% "core" % Version.sttp + val sttpAkkaHttpBackend: ModuleID = "com.softwaremill.sttp.client" %% "akka-http-backend" % Version.sttp + val scalaLibCompat: ModuleID = "org.scala-lang.modules" %% "scala-collection-compat" % "2.1.6" // TEST DEPENDENCIES // Not sure why they don't make the akka-http test kit depends on other test kits...