Skip to content

Commit

Permalink
Merge pull request #36 from mdsol/feature/MCC-433771
Browse files Browse the repository at this point in the history
[MCC-433771] Fix MAuth authentication with Binary HTTP content
  • Loading branch information
Herry Kurniawan authored Oct 9, 2018
2 parents a6403e8 + 44be350 commit 50be15c
Show file tree
Hide file tree
Showing 26 changed files with 293 additions and 193 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changes in Medidata.MAuth

## v3.0.4
- **[Core]** Fixed an issue with HTTP requests having binary content (the authentication was failing in this case)

## v3.0.3
- **[Core]** Fixed concurrency and memory issues with the `MAuthRequestRetrier`

Expand Down
3 changes: 3 additions & 0 deletions src/Medidata.MAuth.Core/Constants.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Text;
using System.Text.RegularExpressions;

namespace Medidata.MAuth.Core
Expand Down Expand Up @@ -27,5 +28,7 @@ internal static class Constants
public static readonly string KeyNormalizeLinesStartRegexPattern = "^(?<begin>-----BEGIN [A-Z ]+[-]+)";

public static readonly string KeyNormalizeLinesEndRegexPattern = "(?<end>-----END [A-Z ]+[-]+)$";

public static readonly byte[] NewLine = Encoding.UTF8.GetBytes("\n");
}
}
2 changes: 1 addition & 1 deletion src/Medidata.MAuth.Core/MAuthAuthenticator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public async Task<bool> AuthenticateRequest(HttpRequestMessage request)
var authInfo = request.GetAuthenticationInfo();
var appInfo = await GetApplicationInfo(authInfo.ApplicationUuid);

return await authInfo.Payload.Verify(await request.GetSignature(authInfo), appInfo.PublicKey);
return authInfo.Payload.Verify(await request.GetSignature(authInfo), appInfo.PublicKey);
}
catch (ArgumentException ex)
{
Expand Down
47 changes: 33 additions & 14 deletions src/Medidata.MAuth.Core/MAuthCoreExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@ public static async Task<string> CalculatePayload(
/// If the signed data matches the signature, it returns <see langword="true"/>; otherwise it
/// returns <see langword="false"/>.
/// </returns>
public static Task<bool> Verify(this byte[] signedData, byte[] signature, string publicKey)
public static bool Verify(this byte[] signedData, byte[] signature, string publicKey)
{
Pkcs1Encoding.StrictLengthEnabled = false;
var cipher = CipherUtilities.GetCipher("RSA/ECB/PKCS1Padding");
cipher.Init(false, publicKey.AsCipherParameters());

return Task.Run(() => cipher.DoFinal(signedData).SequenceEqual(signature));
return cipher.DoFinal(signedData).SequenceEqual(signature);
}

/// <summary>
Expand All @@ -73,11 +73,16 @@ public static Task<bool> Verify(this byte[] signedData, byte[] signature, string
/// </param>
/// <returns>A Task object which will result the SHA512 hash of the signature when it completes.</returns>
public static async Task<byte[]> GetSignature(this HttpRequestMessage request, AuthenticationInfo authInfo) =>
($"{request.Method.Method}\n" +
$"{request.RequestUri.AbsolutePath}\n" +
$"{(request.Content != null ? await request.Content.ReadAsStringAsync() : string.Empty)}\n" +
$"{authInfo.ApplicationUuid.ToHyphenString()}\n" +
$"{authInfo.SignedTime.ToUnixTimeSeconds()}")
new byte[][]
{
request.Method.Method.ToBytes(), Constants.NewLine,
request.RequestUri.AbsolutePath.ToBytes(), Constants.NewLine,
(request.Content != null ? await request.Content.ReadAsByteArrayAsync() : new byte[] { }),
Constants.NewLine,
authInfo.ApplicationUuid.ToHyphenString().ToBytes(), Constants.NewLine,
authInfo.SignedTime.ToUnixTimeSeconds().ToString().ToBytes()
}
.Concat()
.AsSHA512Hash();

/// <summary>
Expand Down Expand Up @@ -192,23 +197,21 @@ public static string ToHyphenString(this Guid uuid) =>
/// <param name="headers">The collection of the HTTP headers to search in.</param>
/// <param name="key">The key to search in the headers collection.</param>
/// <returns>The value if found; otherwise a default value for the given type.</returns>
public static TValue GetFirstValueOrDefault<TValue>(this HttpHeaders headers, string key)
{
IEnumerable<string> values;

return headers.TryGetValues(key, out values) ?
public static TValue GetFirstValueOrDefault<TValue>(this HttpHeaders headers, string key) =>
headers.TryGetValues(key, out IEnumerable<string> values) ?
(TValue)Convert.ChangeType(values.First(), typeof(TValue)) :
default(TValue);
}

/// <summary>
/// Provides an SHA512 hash value of a string.
/// </summary>
/// <param name="value">The value for calculating the hash.</param>
/// <returns>The SHA512 hash of the input value as a hex-encoded byte array.</returns>
public static byte[] AsSHA512Hash(this string value) =>
Hex.Encode(SHA512.Create().ComputeHash(Encoding.UTF8.GetBytes(value)));
AsSHA512Hash(Encoding.UTF8.GetBytes(value));

public static byte[] AsSHA512Hash(this byte[] value) =>
Hex.Encode(SHA512.Create().ComputeHash(value));

/// <summary>
/// Provides a string PEM (ASN.1) format key as an <see cref="ICipherParameters"/> object.
Expand Down Expand Up @@ -261,5 +264,21 @@ private static string InsertLineBreakAfterBegin(this string key) =>

private static string InsertLineBreakBeforeEnd(this string key) =>
Regex.Replace(key, Constants.KeyNormalizeLinesEndRegexPattern, "\n${end}");


private static byte[] ToBytes(this string value) => Encoding.UTF8.GetBytes(value);

private static byte[] Concat(this byte[][] values)
{
var result = new byte[values.Sum(x => x.Length)];
var offset = 0;
foreach (var value in values)
{
Buffer.BlockCopy(value, 0, result, offset, value.Length);
offset += value.Length;
}

return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ protected override async Task<HttpResponseMessage> SendAsync(

var authInfo = request.GetAuthenticationInfo();

if (!await authInfo.Payload.Verify(
if (!authInfo.Payload.Verify(
await request.GetSignature(authInfo),
TestExtensions.ServerPublicKey
))
Expand Down
38 changes: 38 additions & 0 deletions tests/Medidata.MAuth.Tests/Infrastructure/RequestData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Net.Http;
using System.Runtime.Serialization;
using Medidata.MAuth.Core;

namespace Medidata.MAuth.Tests.Infrastructure
{
[DataContract]
internal class RequestData
{
[DataMember]
public Uri Url { get; set; }

[DataMember]
public string Method { get; set; }

[DataMember]
public DateTimeOffset SignedTime { get; set; }

[DataMember]
public string Base64Content { get; set; }

[DataMember]
public Guid ApplicationUuid { get; set; }

[DataMember]
public string Payload { get; set; }

[IgnoreDataMember]
public long SignedTimeUnixSeconds => SignedTime.ToUnixTimeSeconds();

[IgnoreDataMember]
public string MAuthHeader => $"MWS {ApplicationUuidString}:{Payload}";

[IgnoreDataMember]
public string ApplicationUuidString => ApplicationUuid.ToHyphenString();
}
}
42 changes: 0 additions & 42 deletions tests/Medidata.MAuth.Tests/Infrastructure/TestData.cs

This file was deleted.

28 changes: 28 additions & 0 deletions tests/Medidata.MAuth.Tests/Infrastructure/TestExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using System;
using System.IO;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Medidata.MAuth.Core;
using Newtonsoft.Json;

namespace Medidata.MAuth.Tests.Infrastructure
{
Expand Down Expand Up @@ -57,9 +60,34 @@ public static Task<string> GetStringFromResource(string resourceName)
}
}

public static async Task<RequestData> FromResource(this string requestDataName) =>
JsonConvert.DeserializeObject<RequestData>(
await GetStringFromResource($"Medidata.MAuth.Tests.Mocks.RequestData.{requestDataName}.json")
);

private static Task<string> GetKeyFromResource(string keyName)
{
return GetStringFromResource($"Medidata.MAuth.Tests.Mocks.Keys.{keyName}.pem");
}

public static HttpRequestMessage ToHttpRequestMessage(this RequestData data)
{
var result = new HttpRequestMessage(new HttpMethod(data.Method), data.Url)
{
Content = !string.IsNullOrEmpty(data.Base64Content) ?
new ByteArrayContent(Convert.FromBase64String(data.Base64Content)) :
null,
};

result.Headers.Add(Constants.MAuthHeaderKey, $"MWS {data.ApplicationUuidString}:{data.Payload}");
result.Headers.Add(Constants.MAuthTimeHeaderKey, data.SignedTimeUnixSeconds.ToString());

return result;
}

public static string ToStringContent(this string base64Content) =>
base64Content == null ? null : Encoding.UTF8.GetString(Convert.FromBase64String(base64Content));

public static HttpMethod ToHttpMethod(this string method) => new HttpMethod(method);
}
}
23 changes: 12 additions & 11 deletions tests/Medidata.MAuth.Tests/MAuthAspNetCoreTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Medidata.MAuth.AspNetCore;
using Medidata.MAuth.Core;
Expand All @@ -21,7 +22,7 @@ public class MAuthAspNetCoreTests
public async Task MAuthMiddleware_WithValidRequest_WillAuthenticate(string method)
{
// Arrange
var testData = await TestData.For(method);
var testData = await method.FromResource();

using (var server = new TestServer(new WebHostBuilder().Configure(app =>
{
Expand All @@ -38,8 +39,7 @@ public async Task MAuthMiddleware_WithValidRequest_WillAuthenticate(string metho
})))
{
// Act
var response = await server.CreateClient().SendAsync(
await testData.Request.Sign(TestExtensions.ClientOptions(testData.SignedTime)));
var response = await server.CreateClient().SendAsync(testData.ToHttpRequestMessage());

// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Expand All @@ -54,7 +54,7 @@ public async Task MAuthMiddleware_WithValidRequest_WillAuthenticate(string metho
public async Task MAuthMiddleware_WithoutMAuthHeader_WillNotAuthenticate(string method)
{
// Arrange
var testData = await TestData.For(method);
var testData = await method.FromResource();

using (var server = new TestServer(new WebHostBuilder().Configure(app =>
{
Expand All @@ -70,7 +70,8 @@ public async Task MAuthMiddleware_WithoutMAuthHeader_WillNotAuthenticate(string
})))
{
// Act
var response = await server.CreateClient().SendAsync(testData.Request);
var response = await server.CreateClient().SendAsync(
new HttpRequestMessage(testData.Method.ToHttpMethod(), testData.Url));

// Assert
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
Expand All @@ -85,7 +86,7 @@ public async Task MAuthMiddleware_WithoutMAuthHeader_WillNotAuthenticate(string
public async Task MAuthMiddleware_WithEnabledExceptions_WillThrowException(string method)
{
// Arrange
var testData = await TestData.For(method);
var testData = await method.FromResource();

using (var server = new TestServer(new WebHostBuilder().Configure(app =>
{
Expand All @@ -101,7 +102,8 @@ public async Task MAuthMiddleware_WithEnabledExceptions_WillThrowException(strin
{
// Act, Assert
var ex = await Assert.ThrowsAsync<AuthenticationException>(
() => server.CreateClient().SendAsync(testData.Request));
() => server.CreateClient().SendAsync(
new HttpRequestMessage(testData.Method.ToHttpMethod(), testData.Url)));

Assert.Equal("The request has invalid MAuth authentication headers.", ex.Message);
Assert.NotNull(ex.InnerException);
Expand All @@ -116,7 +118,7 @@ public async Task MAuthMiddleware_WithEnabledExceptions_WillThrowException(strin
public async Task MAuthMiddleware_WithNonSeekableBodyStream_WillRestoreBodyStream(string method)
{
// Arrange
var testData = await TestData.For(method);
var testData = await method.FromResource();
var canSeek = false;
var body = string.Empty;
using (var server = new TestServer(new WebHostBuilder().Configure(app =>
Expand All @@ -139,12 +141,11 @@ public async Task MAuthMiddleware_WithNonSeekableBodyStream_WillRestoreBodyStrea
})))
{
// Act
var response = await server.CreateClient().SendAsync(
await testData.Request.Sign(TestExtensions.ClientOptions(testData.SignedTime)));
var response = await server.CreateClient().SendAsync(testData.ToHttpRequestMessage());

// Assert
Assert.True(canSeek);
Assert.Equal(testData.Content ?? string.Empty, body);
Assert.Equal(testData.Base64Content.ToStringContent() ?? string.Empty, body);
}
}
}
Expand Down
Loading

0 comments on commit 50be15c

Please sign in to comment.