Skip to content

Commit

Permalink
Merge pull request #60 from mdsol/feature/MCC-575471
Browse files Browse the repository at this point in the history
[MCC-575471] Fall back to V1 authentication when V2 authentication fails
  • Loading branch information
Herry Kurniawan authored Jan 27, 2020
2 parents 9b5a8a7 + 440f91f commit aa23188
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 31 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 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.
Expand Down
55 changes: 35 additions & 20 deletions src/Medidata.MAuth.Core/MAuthAuthenticator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,14 @@ public async Task<bool> AuthenticateRequest(HttpRequestMessage request)
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 logMessage = "Mauth-client attempting to authenticate request from app with mauth app uuid" +
$" {authInfo.ApplicationUuid} using version {version}";
logger.LogInformation(logMessage);

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 @@ -91,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 @@ -111,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
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
59 changes: 55 additions & 4 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 @@ -308,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 @@ -330,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);
}
}
}
1 change: 1 addition & 0 deletions tests/Medidata.MAuth.Tests/Medidata.MAuth.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
<ItemGroup>
<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

0 comments on commit aa23188

Please sign in to comment.