Skip to content

Commit

Permalink
[MCC-750736]support mauth-protocol-test-suite (#133)
Browse files Browse the repository at this point in the history
  • Loading branch information
bgong-mdsol authored Apr 15, 2021
1 parent 7e5d49c commit 74f3cd1
Show file tree
Hide file tree
Showing 14 changed files with 487 additions and 2 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "mauth-protocol-test-suite"]
path = mauth-protocol-test-suite
url = https://github.com/mdsol/mauth-protocol-test-suite.git
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- parsing code to test with mauth-protocol-test-suite.

### Changed
- Dependency update
Expand Down
11 changes: 11 additions & 0 deletions CONTRIBUTING.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ $ cd mauth-jvm-clients
$ git checkout develop
----

This repo contains the submodule `mauth-protocol-test-suite` so requires a flag when initially cloning in order to clone and init submodules.
----
$ git clone --recurse-submodules git@github.com:mdsol/mauth-jvm-clients.git
----

If you have already cloned before the submodule was introduced, then run:
----
$ cd mauth-protocol-test-suite
$ git submodule update --init
----

== Git Hooks
Install desired hook(s)

Expand Down
4 changes: 2 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,11 @@ lazy val `mauth-authenticator` = javaModuleProject("mauth-authenticator")
)

lazy val `mauth-authenticator-scala` = scalaModuleProject("mauth-authenticator-scala")
.dependsOn(`mauth-authenticator`, `mauth-test-utils` % "test")
.dependsOn(`mauth-authenticator`, `mauth-signer` % "test", `mauth-test-utils` % "test")
.settings(
publishSettings,
libraryDependencies ++=
Dependencies.test(logbackClassic, scalaMock, scalaTest).map(withExclusions)
Dependencies.test(logbackClassic, scalaMock, scalaTest, scalaLibCompat).map(withExclusions)
)

lazy val `mauth-authenticator-apachehttp` = javaModuleProject("mauth-authenticator-apachehttp")
Expand Down
1 change: 1 addition & 0 deletions mauth-protocol-test-suite
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package com.mdsol.mauth

import java.util.UUID

import com.mdsol.mauth.test.utils.model._
import com.mdsol.mauth.test.utils.ProtocolTestSuiteHelper
import com.mdsol.mauth.util.MAuthKeysHelper.getPrivateKeyFromString
import com.mdsol.mauth.util.{EpochTimeProvider, MAuthKeysHelper, MAuthSignatureHelper}
import com.mdsol.mauth.utils.ClientPublicKeyProvider
import org.scalamock.scalatest.MockFactory
import org.scalatest.BeforeAndAfterAll
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

import scala.jdk.CollectionConverters._

class MauthProtocolSuiteSpec extends AnyFlatSpec with BeforeAndAfterAll with Matchers with MockFactory {

val REQUEST_VALIDATION_TIMEOUT_SECONDS: Long = 300L
val mockClientPublicKeyProvider: ClientPublicKeyProvider = mock[ClientPublicKeyProvider]
val mockEpochTimeProvider: EpochTimeProvider = mock[EpochTimeProvider]
val authenticatorV2: RequestAuthenticator =
new RequestAuthenticator(mockClientPublicKeyProvider, REQUEST_VALIDATION_TIMEOUT_SECONDS, mockEpochTimeProvider, true)

behavior of "MauthProtocolSuiteSpec"

val signingConfig = ProtocolTestSuiteHelper.SIGNING_CONFIG
if (signingConfig == null) {
fail("Signing Configuration is not available.")
}

if (ProtocolTestSuiteHelper.PUBLIC_KEY == null) {
fail("Public Key is not available.")
}

val publicKey = MAuthKeysHelper.getPublicKeyFromString(ProtocolTestSuiteHelper.PUBLIC_KEY)
val uuid = UUID.fromString(signingConfig.getAppUuid)
val privateKey = getPrivateKeyFromString(signingConfig.getPrivateKey)
val mAuthSigner = new DefaultSigner(uuid, privateKey, mockEpochTimeProvider, java.util.Arrays.asList(MAuthVersion.MWSV2))

// run the tests
val testSpecifications = ProtocolTestSuiteHelper.TEST_SPECIFICATIONS
testSpecifications.foreach { testSpec =>
testSpec.getType match {
case CaseType.AUTHENTICATION_ONLY =>
val authenticationOnly = testSpec.asInstanceOf[AuthenticationOnly]
s"Test Case: ${authenticationOnly.getName}" should "pass authentication" in {
doAuthentication(
authenticationOnly.getUnsignedRequest,
authenticationOnly.getAuthenticationHeader
) shouldBe true
}

case CaseType.SIGNING_AUTHENTICATION =>
val signingAuthentication = testSpec.asInstanceOf[SigningAuthentication]
val authHeader = signingAuthentication.getAuthenticationHeader
val unsignedRequest = signingAuthentication.getUnsignedRequest

val httpVerb = unsignedRequest.getHttpVerb
val resourcePath = unsignedRequest.getResourcePath
val queryString = unsignedRequest.getQueryString
val bodyInBytes = unsignedRequest.getBodyInBytes
s"Test Case: ${signingAuthentication.getName}" should "correctly generate the string to sign" in {
MAuthSignatureHelper.generateStringToSignV2(
uuid,
httpVerb,
resourcePath,
queryString,
bodyInBytes,
signingConfig.getRequestTime
) shouldBe signingAuthentication.getStringToSign
}

it should "correctly generate the signature" in {
MAuthSignatureHelper.encryptSignatureRSA(
privateKey,
signingAuthentication.getStringToSign
) shouldBe signingAuthentication.getSignature
}

it should "correctly generate the authentication headers" in {
(mockEpochTimeProvider.inSeconds _: () => Long).expects().returns(signingConfig.getRequestTime.toLong)
val headers: Map[String, String] =
mAuthSigner
.generateRequestHeaders(httpVerb, resourcePath, bodyInBytes, queryString)
.asScala
.toMap
headers(MAuthRequest.MCC_AUTHENTICATION_HEADER_NAME) shouldBe authHeader.getMccAuthentication
headers(MAuthRequest.MCC_TIME_HEADER_NAME) shouldBe authHeader.getMccTime.toString
}

it should "pass authentication" in {
doAuthentication(unsignedRequest, authHeader) shouldBe true
}
}
}

private def doAuthentication(unsignedRequest: UnsignedRequest, authHeader: AuthenticationHeader): Boolean = {
val mauthRequest = MAuthRequest.Builder.get
.withAuthenticationHeaderValue(authHeader.getMccAuthentication)
.withTimeHeaderValue(String.valueOf(authHeader.getMccTime))
.withHttpMethod(unsignedRequest.getHttpVerb)
.withResourcePath(unsignedRequest.getResourcePath)
.withQueryParameters(unsignedRequest.getQueryString)
.withMessagePayload(unsignedRequest.getBodyInBytes)
.build()

(mockEpochTimeProvider.inSeconds _: () => Long).expects().returns(signingConfig.getRequestTime.toLong + 3)
(mockClientPublicKeyProvider.getPublicKey _)
.expects(uuid)
.returns(publicKey)
authenticatorV2.authenticate(mauthRequest)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package com.mdsol.mauth.test.utils;

import com.mdsol.mauth.test.utils.model.AuthenticationHeader;
import com.mdsol.mauth.test.utils.model.AuthenticationOnly;
import com.mdsol.mauth.test.utils.model.SigningAuthentication;
import com.mdsol.mauth.test.utils.model.SigningConfig;
import com.mdsol.mauth.test.utils.model.TestCase;
import com.mdsol.mauth.test.utils.model.UnsignedRequest;

import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

public class ProtocolTestSuiteHelper {

final static String TEST_SUITE_RELATIVE_PATH = "../../mauth-protocol-test-suite/";
final static String MWSV2_TEST_CASE_PATH = getFullFilePath("protocols/MWSV2/");

public final static SigningConfig SIGNING_CONFIG;
static {
SigningConfig tmp = null;
String configFile = getFullFilePath("signing-config.json");
try {
ObjectMapper objectMapper = new ObjectMapper();
byte[] jsonData = Files.readAllBytes(normalizePath(configFile));
tmp = objectMapper.readValue(jsonData, SigningConfig.class);
} catch (IOException ex) {
throw new IllegalStateException("Failed to load the config file " + configFile, ex);
}
SIGNING_CONFIG = tmp;
}

public final static String PUBLIC_KEY ;
static {
String tmp = null;
String filePath = getFullFilePath("signing-params/rsa-key-pub");
try {
tmp = new String(Files.readAllBytes(normalizePath(filePath)), StandardCharsets.UTF_8);
} catch (IOException ex) {
throw new IllegalStateException("Failed to load the public key " + filePath, ex);
}
PUBLIC_KEY = tmp;
}

public final static TestCase[] TEST_SPECIFICATIONS;
static {
List<TestCase> testCaseList = new ArrayList<TestCase>();
String[] cases = retrieveV2TestCases();

for (String caseName : cases) {
String caseFile = caseName.concat("/").concat(caseName);
AuthenticationHeader authHeader = loadAuthenticationHeader(caseFile.concat(".authz"));
UnsignedRequest unsignedRequest = loadUnsignedRequest(caseFile.concat(".req"));
byte[] bodyInBytes = getTestRequestBody(unsignedRequest, caseName);
unsignedRequest.setBodyInBytes(bodyInBytes);

boolean isAuthenticationOnly = caseName.contains("authentication-only");
if(isAuthenticationOnly) {
testCaseList.add(new AuthenticationOnly(caseName, unsignedRequest, authHeader));
} else {
String stringToSign = loadTestDataAsString(caseFile.concat(".sts"));
String signature = loadTestDataAsString(caseFile.concat(".sig"));
testCaseList.add(new SigningAuthentication(caseName, unsignedRequest, authHeader, stringToSign, signature));
}
}
TEST_SPECIFICATIONS = testCaseList.toArray(new TestCase[0]);
}

public static String loadPrivateKey(String keyFile) {
String privateKey = "";
Path filePath = normalizePath(getFullFilePath(keyFile));
try {
privateKey = new String(Files.readAllBytes(filePath), Charset.defaultCharset());
} catch (IOException ex) {
throw new IllegalStateException("Failed to load the private key " + filePath, ex);
}
return privateKey;
}

private static String[] retrieveV2TestCases() {
File file = new File(MWSV2_TEST_CASE_PATH);
String[] directories = file.list(new FilenameFilter() {
@Override
public boolean accept(File current, String name) {
return new File(current, name).isDirectory();
}
});
return directories;
}

private static byte[] loadTestData(String filePath) {
byte[] bytes = {};
Path path = normalizePath(MWSV2_TEST_CASE_PATH + filePath);
try {
bytes = Files.readAllBytes(path);
} catch (IOException ex) {
throw new IllegalStateException("Failed to load " + filePath, ex);
}
return bytes;
}

private static String loadTestDataAsString(String filePath) {
byte[] bytes = loadTestData(filePath);
return bytes.length==0 ? "" : new String(bytes, Charset.defaultCharset());
}

private static UnsignedRequest loadUnsignedRequest(String filePath) {
UnsignedRequest unsignedRequest = new UnsignedRequest();
byte[] jsonData = loadTestData(filePath);
try {
ObjectMapper objectMapper = new ObjectMapper();
unsignedRequest = objectMapper.readValue(jsonData, UnsignedRequest.class);
} catch (IOException ex) {
throw new IllegalStateException("Failed to load " + filePath, ex);
}
return unsignedRequest;
}

private static byte[] getTestRequestBody(UnsignedRequest unsignedRequest, String caseName) {
byte[] bytes = new byte[0];
if (unsignedRequest.getBodyFilepath() != null) {
String bodyFile = caseName.concat("/").concat(unsignedRequest.getBodyFilepath());
bytes = ProtocolTestSuiteHelper.loadTestData(bodyFile);
}
else if (unsignedRequest.getHttpVerb() != null) {
bytes = unsignedRequest.getBody().getBytes(StandardCharsets.UTF_8);
}
return bytes;
}

private static AuthenticationHeader loadAuthenticationHeader(String filePath) {
AuthenticationHeader authenticationHeader = new AuthenticationHeader();
byte[] jsonData = loadTestData(filePath);
try {
ObjectMapper objectMapper = new ObjectMapper();
authenticationHeader = objectMapper.readValue(jsonData, AuthenticationHeader.class);
} catch (IOException ex) {
throw new IllegalStateException("Failed to load " + filePath, ex);
}
return authenticationHeader;
}

private static Path normalizePath(String filepath) {
return Paths.get(filepath).normalize();
}

private static String getFullFilePath(String filePath) {
return TEST_SUITE_RELATIVE_PATH + filePath;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.mdsol.mauth.test.utils.model;

import com.fasterxml.jackson.annotation.JsonProperty;

public class AuthenticationHeader {
@JsonProperty("MCC-Authentication")
String mccAuthentication;
@JsonProperty("MCC-Time")
Long mccTime;

public String getMccAuthentication() { return mccAuthentication; }

public void setMccAuthentication(String mccAuthentication) { this.mccAuthentication = mccAuthentication; }

public long getMccTime() { return mccTime; }

public void setMccTime(Long mccTime) { this.mccTime = mccTime; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.mdsol.mauth.test.utils.model;

public class AuthenticationOnly implements TestCase {
String name;
AuthenticationHeader authHeader;
UnsignedRequest unsignedRequest;

public AuthenticationOnly(
String name,
UnsignedRequest unsignedRequest,
AuthenticationHeader authHeader) {
this.name = name;
this.unsignedRequest = unsignedRequest;
this.authHeader = authHeader;
}

@Override
public String getName() { return name; }

@Override
public CaseType getType() { return CaseType.AUTHENTICATION_ONLY; }

public UnsignedRequest getUnsignedRequest() { return unsignedRequest; }

public void setUnsignedRequest(UnsignedRequest unsignedRequest) { this.unsignedRequest = unsignedRequest; }

public AuthenticationHeader getAuthenticationHeader() { return authHeader; }

public void setAuthenticationHeader(AuthenticationHeader authHeader) { this.authHeader = authHeader; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.mdsol.mauth.test.utils.model;

public enum CaseType {
AUTHENTICATION_ONLY,
SIGNING_AUTHENTICATION
}
Loading

0 comments on commit 74f3cd1

Please sign in to comment.