Skip to content

Commit

Permalink
Merge pull request #61 from mdsol/develop
Browse files Browse the repository at this point in the history
Release of v4.0.2
  • Loading branch information
Herry Kurniawan authored Feb 5, 2020
2 parents 22b3028 + aa23188 commit 512544b
Show file tree
Hide file tree
Showing 10 changed files with 112 additions and 53 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changes in Medidata.MAuth

## v4.0.2
- **[AspNetCore]** Update aspnetcore version to aspnetcore2.1 LTS.
- **[Core]** Fallback to V1 protocol when V2 athentication fails.

## v4.0.1
- **[Core]** Fixed default sigining with both MWS and MWSV2 instead of option selected by consuming application.
- **[Core]** Fixed an issue related to token request path which is same for both MWS and MWSV2 protocol.
Expand Down
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,8 @@ public async Task<HttpResponseMessage> SignAndSendRequest(HttpRequestMessage req
}
}
```
With the MAuth V2 protocol, there are two new options `MAuthVersion` and `DisableV1` added for Signing MAuth request.
`MAuthVersion` is passed either as `MAuthVersion.MWSV2` for signing with V2 protocol or `MAuthVersion.MWS` for continue
signing with V1 protocol. By default, `DisableV1` option is set to false (if not included). When we are ready to
With the release of support for MAuth V2 protocol, by default MAuth request signs with both V1 and V2 protocol.
Also by default, `DisableV1` option is set to false (if not included). When we are ready to
disable all the V1 request, then we need to include this disable option as : `DisableV1 = true`.
Signing with V2 protocol supports query string.

Expand All @@ -140,8 +139,7 @@ The `MAuthSigningOptions` has the following properties to determine the required
| ---- | ----------- |
| **ApplicationUuid** | Determines the unique identifier of the client application used for the MAuth service authentication requests. This uuid needs to be registered with the MAuth Server in order for the authenticating server application to be able to authenticate the signed request. |
| **PrivateKey** | Determines the RSA private key of the client for signing a request. This key must be in a PEM ASN.1 format. The value of this property can be set as a valid path to a readable key file as well. |
| **MAuthVersion** | Determines the MAuth version of the request used for signing. This is enumeration value which is `MAuthVersion.MWSV2` for V2 requests. |
| **DisableV1** | Determines the boolean value which controls whether to disable the signing requests with `MAuthVersion.MWS` requests or not. If not supplied, this value is `false`. |
| **DisableV1** | Determines the boolean value which controls whether to disable the signing requests with V1 protocol or not. If not supplied, this value is `false`. |

### Authenticating Incoming Requests with the OWIN and ASP.NET Core Middlewares

Expand Down Expand Up @@ -319,7 +317,7 @@ in your project in order to make Medidata.MAuth work for you.
##### Is there an .NET Standard/Core support?

Yes, for signing outgoing requests you can use the library with any framework which implements
the **.NET Standard 2.0** and onwards; additionally we support the **ASP.NET Core App 2.0** and onwards with a middleware
the **.NET Standard 2.0** and onwards; additionally we support the **ASP.NET Core App 2.1** and onwards with a middleware
for authenticating the incoming requests.

##### What Cryptographic provider is used for the encryption/decryption?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Import Project="..\..\build\common.props" />

<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Description>This package contains an ASP.NET Core middleware to validate signed http requests with the Medidata MAuth protocol. The middleware communicates with an MAuth server in order to confirm the validity of the request authentication header. Include this package in your ASP.NET Core web api if you want to authenticate the api requests signed with the MAuth protocol.</Description>
<AssemblyTitle>Medidata.MAuth.AspNetCore</AssemblyTitle>
<AssemblyName>Medidata.MAuth.AspNetCore</AssemblyName>
Expand All @@ -14,9 +14,9 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.1.1" />
</ItemGroup>

<ItemGroup>
Expand Down
53 changes: 35 additions & 18 deletions src/Medidata.MAuth.Core/MAuthAuthenticator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,17 @@ public async Task<bool> AuthenticateRequest(HttpRequestMessage request)
logger.LogInformation("Initiating Authentication of the request.");
var version = request.GetAuthHeaderValue().GetVersionFromAuthenticationHeader();

logger.LogInformation("Authentication is for the {version}.",version);

if (options.DisableV1 && version == MAuthVersion.MWS)
throw new InvalidVersionException($"Authentication with {version} version is disabled.");

var mAuthCore = MAuthCoreFactory.Instantiate(version);
var authInfo = GetAuthenticationInfo(request, version);
var appInfo = await GetApplicationInfo(authInfo.ApplicationUuid, version).ConfigureAwait(false);
var signature = await mAuthCore.GetSignature(request, authInfo).ConfigureAwait(false);

return mAuthCore.Verify(authInfo.Payload, signature, appInfo.PublicKey);
var authenticated = await Authenticate(request, version).ConfigureAwait(false);
if (!authenticated && version == MAuthVersion.MWSV2 && !options.DisableV1)
{
// fall back to V1 authentication
authenticated = await Authenticate(request, MAuthVersion.MWS).ConfigureAwait(false);
logger.LogWarning("Completed successful authentication attempt after fallback to V1");
}
return authenticated;
}
catch (ArgumentException ex)
{
Expand Down Expand Up @@ -89,10 +89,24 @@ public async Task<bool> AuthenticateRequest(HttpRequestMessage request)
}
}

private Task<ApplicationInfo> GetApplicationInfo(Guid applicationUuid, MAuthVersion version) =>
private async Task<bool> Authenticate(HttpRequestMessage request, MAuthVersion version)
{
var logMessage = "Mauth-client attempting to authenticate request from app with mauth app uuid" +
$" {options.ApplicationUuid} using version {version}";
logger.LogInformation(logMessage);

var mAuthCore = MAuthCoreFactory.Instantiate(version);
var authInfo = GetAuthenticationInfo(request, mAuthCore);
var appInfo = await GetApplicationInfo(authInfo.ApplicationUuid).ConfigureAwait(false);

var signature = await mAuthCore.GetSignature(request, authInfo).ConfigureAwait(false);
return mAuthCore.Verify(authInfo.Payload, signature, appInfo.PublicKey);
}

private Task<ApplicationInfo> GetApplicationInfo(Guid applicationUuid) =>
cache.GetOrCreateAsync(applicationUuid, async entry =>
{
var retrier = new MAuthRequestRetrier(options, version);
var retrier = new MAuthRequestRetrier(options);
var response = await retrier.GetSuccessfulResponse(
applicationUuid,
CreateRequest,
Expand All @@ -109,38 +123,41 @@ private Task<ApplicationInfo> GetApplicationInfo(Guid applicationUuid, MAuthVers
return result;
});

private HttpRequestMessage CreateRequest(Guid applicationUuid) =>
new HttpRequestMessage(HttpMethod.Get, new Uri(options.MAuthServiceUrl,
$"{Constants.MAuthTokenRequestPath}{applicationUuid.ToHyphenString()}.json"));

/// <summary>
/// Extracts the authentication information from a <see cref="HttpRequestMessage"/>.
/// </summary>
/// <param name="request">The request that has the authentication information.</param>
/// <param name="version">Enum value of the MAuthVersion.</param>
/// <param name="mAuthCore">Instantiation of mAuthCore class.</param>
/// <returns>The authentication information with the payload from the request.</returns>
internal PayloadAuthenticationInfo GetAuthenticationInfo(HttpRequestMessage request, MAuthVersion version)
internal static PayloadAuthenticationInfo GetAuthenticationInfo(HttpRequestMessage request, IMAuthCore mAuthCore)
{
var mAuthCore = MAuthCoreFactory.Instantiate(version);
var headerKeys = mAuthCore.GetHeaderKeys();
var authHeader = request.Headers.GetFirstValueOrDefault<string>(headerKeys.mAuthHeaderKey);

if (authHeader == null)
{
throw new ArgumentNullException(nameof(authHeader), "The MAuth header is missing from the request.");
}

var signedTime = request.Headers.GetFirstValueOrDefault<long>(headerKeys.mAuthTimeHeaderKey);

if (signedTime == default(long))
{
throw new ArgumentException("Invalid MAuth signed time header value.", nameof(signedTime));
}

var (uuid, payload) = authHeader.ParseAuthenticationHeader();

return new PayloadAuthenticationInfo()
return new PayloadAuthenticationInfo
{
ApplicationUuid = uuid,
Payload = Convert.FromBase64String(payload),
SignedTime = signedTime.FromUnixTimeSeconds()
};
}

private HttpRequestMessage CreateRequest(Guid applicationUuid) =>
new HttpRequestMessage(HttpMethod.Get, new Uri(options.MAuthServiceUrl,
$"{Constants.MAuthTokenRequestPath}{applicationUuid.ToHyphenString()}.json"));
}
}
3 changes: 1 addition & 2 deletions src/Medidata.MAuth.Core/MAuthRequestRetrier.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Medidata.MAuth.Core.Models;

namespace Medidata.MAuth.Core
{
internal class MAuthRequestRetrier
{
private readonly HttpClient client;

public MAuthRequestRetrier(MAuthOptionsBase options, MAuthVersion version)
public MAuthRequestRetrier(MAuthOptionsBase options)
{
var signingHandler = new MAuthSigningHandler(options: new MAuthSigningOptions()
{
Expand Down
4 changes: 1 addition & 3 deletions src/Medidata.MAuth.Core/Medidata.MAuth.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="System.ValueTuple" Version="4.3.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="1.1.2" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.Threading;
using System.Threading.Tasks;
using Medidata.MAuth.Core;
using Medidata.MAuth.Core.Models;
using Microsoft.Extensions.Logging.Abstractions;
using Newtonsoft.Json;

Expand All @@ -26,10 +25,7 @@ protected override async Task<HttpResponseMessage> SendAsync(

if (currentNumberOfAttempts < SucceedAfterThisManyAttempts)
return new HttpResponseMessage(HttpStatusCode.ServiceUnavailable);

var authenticator = new MAuthAuthenticator(TestExtensions.ServerOptions, NullLogger<MAuthAuthenticator>.Instance);

var authInfo = authenticator.GetAuthenticationInfo(request, version);
var authInfo = MAuthAuthenticator.GetAuthenticationInfo(request, mAuthCore);

if (!mAuthCore.Verify(authInfo.Payload,
await mAuthCore.GetSignature(request, authInfo),
Expand Down
64 changes: 55 additions & 9 deletions tests/Medidata.MAuth.Tests/MAuthAuthenticatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
using Medidata.MAuth.Core.Exceptions;
using Medidata.MAuth.Core.Models;
using Medidata.MAuth.Tests.Infrastructure;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Internal;
using Moq;
using Xunit;

namespace Medidata.MAuth.Tests
Expand Down Expand Up @@ -41,11 +44,6 @@ public static async Task AuthenticateRequest_WithValidMWSRequest_WillAuthenticat
{
// Arrange
var testData = await method.FromResource();

//var testOptions = TestExtensions.ServerOptions;
//testOptions.MAuthServerHandler = new MAuthServerHandler()
// { AuthenticateOnlyV1 = true };

var authenticator = new MAuthAuthenticator(TestExtensions.ServerOptions, NullLogger<MAuthAuthenticator>.Instance);
var mAuthCore = new MAuthCore();

Expand Down Expand Up @@ -313,10 +311,10 @@ public static async Task GetAuthenticationInfo_WithSignedRequest_ForMWSV2Version
var testData = await method.FromResourceV2();
var version = MAuthVersion.MWSV2;
var testOptions = TestExtensions.ServerOptions;
var authenticator = new MAuthAuthenticator(testOptions, NullLogger<MAuthAuthenticator>.Instance);
var mAuthCore = MAuthCoreFactory.Instantiate(version);

// Act
var actual = authenticator.GetAuthenticationInfo(testData.ToHttpRequestMessage(version), version);
var actual = MAuthAuthenticator.GetAuthenticationInfo(testData.ToHttpRequestMessage(version), mAuthCore);

// Assert
Assert.Equal(testData.ApplicationUuid, actual.ApplicationUuid);
Expand All @@ -335,15 +333,63 @@ public static async Task GetAuthenticationInfo_WithSignedRequest_ForMWSVersion_W
var testData = await method.FromResource();
var version = MAuthVersion.MWS;
var testOptions = TestExtensions.ServerOptions;
var authenticator = new MAuthAuthenticator(testOptions, NullLogger<MAuthAuthenticator>.Instance);
var mAuthCore = MAuthCoreFactory.Instantiate(version);

// Act
var actual = authenticator.GetAuthenticationInfo(testData.ToHttpRequestMessage(version), version);
var actual = MAuthAuthenticator.GetAuthenticationInfo(testData.ToHttpRequestMessage(version), mAuthCore);

// Assert
Assert.Equal(testData.ApplicationUuid, actual.ApplicationUuid);
Assert.Equal(Convert.FromBase64String(testData.Payload), actual.Payload);
Assert.Equal(testData.SignedTime, actual.SignedTime);
}

[Fact]
public static async Task AuthenticateRequest_WithDefaultRequest_WhenV2Fails_FallBackToV1AndAuthenticate()
{
// Arrange
var testData = await "GET".FromResource();
var mockLogger = new Mock<ILogger>();

var authenticator = new MAuthAuthenticator(TestExtensions.ServerOptions, mockLogger.Object);
var requestData = testData.ToDefaultHttpRequestMessage();

// Act
var isAuthenticated = await authenticator.AuthenticateRequest(requestData);

// Assert
Assert.True(isAuthenticated);
mockLogger.Verify(x => x.Log(
LogLevel.Warning, It.IsAny<EventId>(),
It.Is<FormattedLogValues>(v => v.ToString()
.Contains("Completed successful authentication attempt after fallback to V1")),
It.IsAny<Exception>(), It.IsAny<Func<object, Exception, string>>()
));
}

[Fact]
public static async Task AuthenticateRequest_WithDefaultRequest_AndDisableV1_WhenV2Fails_NotFallBackToV1()
{
// Arrange
var testData = await "GET".FromResource();
var mockLogger = new Mock<ILogger>();
var testOptions = TestExtensions.ServerOptions;
testOptions.DisableV1 = true;

var authenticator = new MAuthAuthenticator(testOptions, mockLogger.Object);
var requestData = testData.ToDefaultHttpRequestMessage();

// Act
var isAuthenticated = await authenticator.AuthenticateRequest(requestData);

// Assert
Assert.False(isAuthenticated);
mockLogger.Verify(x => x.Log(
LogLevel.Warning, It.IsAny<EventId>(),
It.Is<FormattedLogValues>(v => v.ToString()
.Contains("Completed successful authentication attempt after fallback to V1")),
It.IsAny<Exception>(), It.IsAny<Func<object, Exception, string>>()
), Times.Never);
}
}
}
11 changes: 6 additions & 5 deletions tests/Medidata.MAuth.Tests/Medidata.MAuth.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<Copyright>Copyright © Medidata Solutions, Inc. 2017</Copyright>
<AssemblyTitle>Medidata.MAuth.Tests</AssemblyTitle>
<Authors>Medidata Solutions, Inc.</Authors>
<TargetFrameworks>net461;netcoreapp2.0</TargetFrameworks>
<TargetFrameworks>net461;netcoreapp2.1</TargetFrameworks>
<AssemblyName>Medidata.MAuth.Tests</AssemblyName>
<PackageId>Medidata.MAuth.Tests</PackageId>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
Expand Down Expand Up @@ -60,17 +60,18 @@
<Compile Remove="MAuthAspNetCoreTests.cs" />
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp2.0' ">
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp2.1' ">
<ProjectReference Include="..\..\src\Medidata.MAuth.AspNetCore\Medidata.MAuth.AspNetCore.csproj" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="1.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="1.1.2" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.1.1" />
<Compile Remove="MAuthOwinTests.cs" />
<Compile Remove="MAuthWebApiTests.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
<PackageReference Include="Moq" Version="4.13.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
Expand Down
2 changes: 1 addition & 1 deletion version.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<PropertyGroup>
<Version>4.0.1</Version>
<Version>4.0.2</Version>
</PropertyGroup>
</Project>

0 comments on commit 512544b

Please sign in to comment.