diff --git a/CHANGELOG.md b/CHANGELOG.md index 0914269..336b97a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index d35bd4a..182bd2d 100644 --- a/README.md +++ b/README.md @@ -125,9 +125,8 @@ public async Task 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. @@ -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 @@ -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? diff --git a/src/Medidata.MAuth.AspNetCore/Medidata.MAuth.AspNetCore.csproj b/src/Medidata.MAuth.AspNetCore/Medidata.MAuth.AspNetCore.csproj index ed52a73..d17e2de 100644 --- a/src/Medidata.MAuth.AspNetCore/Medidata.MAuth.AspNetCore.csproj +++ b/src/Medidata.MAuth.AspNetCore/Medidata.MAuth.AspNetCore.csproj @@ -2,7 +2,7 @@ - netcoreapp2.0 + netcoreapp2.1 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. Medidata.MAuth.AspNetCore Medidata.MAuth.AspNetCore @@ -14,9 +14,9 @@ all runtime; build; native; contentfiles; analyzers - - - + + + diff --git a/src/Medidata.MAuth.Core/MAuthAuthenticator.cs b/src/Medidata.MAuth.Core/MAuthAuthenticator.cs index e20b1b7..9cb1bf7 100644 --- a/src/Medidata.MAuth.Core/MAuthAuthenticator.cs +++ b/src/Medidata.MAuth.Core/MAuthAuthenticator.cs @@ -45,17 +45,17 @@ public async Task 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) { @@ -89,10 +89,24 @@ public async Task AuthenticateRequest(HttpRequestMessage request) } } - private Task GetApplicationInfo(Guid applicationUuid, MAuthVersion version) => + private async Task 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 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, @@ -109,38 +123,41 @@ private Task GetApplicationInfo(Guid applicationUuid, MAuthVers return result; }); - private HttpRequestMessage CreateRequest(Guid applicationUuid) => - new HttpRequestMessage(HttpMethod.Get, new Uri(options.MAuthServiceUrl, - $"{Constants.MAuthTokenRequestPath}{applicationUuid.ToHyphenString()}.json")); - /// /// Extracts the authentication information from a . /// /// The request that has the authentication information. - /// Enum value of the MAuthVersion. + /// Instantiation of mAuthCore class. /// The authentication information with the payload from the request. - 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(headerKeys.mAuthHeaderKey); if (authHeader == null) + { throw new ArgumentNullException(nameof(authHeader), "The MAuth header is missing from the request."); + } var signedTime = request.Headers.GetFirstValueOrDefault(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")); } } diff --git a/src/Medidata.MAuth.Core/MAuthRequestRetrier.cs b/src/Medidata.MAuth.Core/MAuthRequestRetrier.cs index ac60a3a..bac3f16 100644 --- a/src/Medidata.MAuth.Core/MAuthRequestRetrier.cs +++ b/src/Medidata.MAuth.Core/MAuthRequestRetrier.cs @@ -1,7 +1,6 @@ using System; using System.Net.Http; using System.Threading.Tasks; -using Medidata.MAuth.Core.Models; namespace Medidata.MAuth.Core { @@ -9,7 +8,7 @@ internal class MAuthRequestRetrier { private readonly HttpClient client; - public MAuthRequestRetrier(MAuthOptionsBase options, MAuthVersion version) + public MAuthRequestRetrier(MAuthOptionsBase options) { var signingHandler = new MAuthSigningHandler(options: new MAuthSigningOptions() { diff --git a/src/Medidata.MAuth.Core/Medidata.MAuth.Core.csproj b/src/Medidata.MAuth.Core/Medidata.MAuth.Core.csproj index acbc03e..8707773 100644 --- a/src/Medidata.MAuth.Core/Medidata.MAuth.Core.csproj +++ b/src/Medidata.MAuth.Core/Medidata.MAuth.Core.csproj @@ -15,9 +15,7 @@ runtime; build; native; contentfiles; analyzers - - - + diff --git a/tests/Medidata.MAuth.Tests/Infrastructure/MAuthServerHandler.cs b/tests/Medidata.MAuth.Tests/Infrastructure/MAuthServerHandler.cs index 9bb4410..3499c89 100644 --- a/tests/Medidata.MAuth.Tests/Infrastructure/MAuthServerHandler.cs +++ b/tests/Medidata.MAuth.Tests/Infrastructure/MAuthServerHandler.cs @@ -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; @@ -26,10 +25,7 @@ protected override async Task SendAsync( if (currentNumberOfAttempts < SucceedAfterThisManyAttempts) return new HttpResponseMessage(HttpStatusCode.ServiceUnavailable); - - var authenticator = new MAuthAuthenticator(TestExtensions.ServerOptions, NullLogger.Instance); - - var authInfo = authenticator.GetAuthenticationInfo(request, version); + var authInfo = MAuthAuthenticator.GetAuthenticationInfo(request, mAuthCore); if (!mAuthCore.Verify(authInfo.Payload, await mAuthCore.GetSignature(request, authInfo), diff --git a/tests/Medidata.MAuth.Tests/MAuthAuthenticatorTests.cs b/tests/Medidata.MAuth.Tests/MAuthAuthenticatorTests.cs index 01ecfec..b112142 100644 --- a/tests/Medidata.MAuth.Tests/MAuthAuthenticatorTests.cs +++ b/tests/Medidata.MAuth.Tests/MAuthAuthenticatorTests.cs @@ -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 @@ -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.Instance); var mAuthCore = new MAuthCore(); @@ -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.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); @@ -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.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(); + + 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(), + It.Is(v => v.ToString() + .Contains("Completed successful authentication attempt after fallback to V1")), + It.IsAny(), It.IsAny>() + )); + } + + [Fact] + public static async Task AuthenticateRequest_WithDefaultRequest_AndDisableV1_WhenV2Fails_NotFallBackToV1() + { + // Arrange + var testData = await "GET".FromResource(); + var mockLogger = new Mock(); + 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(), + It.Is(v => v.ToString() + .Contains("Completed successful authentication attempt after fallback to V1")), + It.IsAny(), It.IsAny>() + ), Times.Never); + } } } diff --git a/tests/Medidata.MAuth.Tests/Medidata.MAuth.Tests.csproj b/tests/Medidata.MAuth.Tests/Medidata.MAuth.Tests.csproj index 53d5bf3..d0b83dc 100644 --- a/tests/Medidata.MAuth.Tests/Medidata.MAuth.Tests.csproj +++ b/tests/Medidata.MAuth.Tests/Medidata.MAuth.Tests.csproj @@ -5,7 +5,7 @@ Copyright © Medidata Solutions, Inc. 2017 Medidata.MAuth.Tests Medidata Solutions, Inc. - net461;netcoreapp2.0 + net461;netcoreapp2.1 Medidata.MAuth.Tests Medidata.MAuth.Tests true @@ -60,17 +60,18 @@ - + - - + + - + + all diff --git a/version.props b/version.props index 4716aab..3891445 100644 --- a/version.props +++ b/version.props @@ -1,6 +1,6 @@  - 4.0.1 + 4.0.2