Skip to content

Commit

Permalink
Merge pull request #45 from mdsol/feature/MCC-422164
Browse files Browse the repository at this point in the history
[MCC-422164] Upgrade mauth client dotnet to use MWSV2 signature
  • Loading branch information
Herry Kurniawan authored Aug 15, 2019
2 parents c4662d9 + 13676aa commit 437265f
Show file tree
Hide file tree
Showing 41 changed files with 1,016 additions and 90 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

## v4.0.0
- **[All]** Added implementation for MWSV2 signinig and authentication.

## v3.1.3
- **[Core]** Refactored `MAuthCoreExtensions.cs` and moved Signing and Verification method into `IMAuthCore.cs`.

Expand Down
56 changes: 50 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ An example:

```C#
using Medidata.MAuth.Core;
using Medidata.MAuth.Core.Models;

public async Task<HttpResponseMessage> SignAndSendRequest(HttpRequestMessage request)
{
Expand All @@ -111,7 +112,13 @@ public async Task<HttpResponseMessage> SignAndSendRequest(HttpRequestMessage req
ApplicationUuid = new Guid("7c872d75-986b-4c61-bb17-f2569d42bfb0"),

// The following can be either a path to the key file or the contents of the file itself
PrivateKey = "ClientPrivateKey.pem"
PrivateKey = "ClientPrivateKey.pem",

// With 4.0.0 version, V2 protocol is supported
MAuthVersion = (MAuthVersion.MWSV2 || MAuthVersion.MWS)

// when ready to disable authentication of V1 protococl
DisableV1 = true
});

using (var client = new HttpClient(signingHandler))
Expand All @@ -120,6 +127,11 @@ 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
disable all the V1 request, then we need to include this disable option as : `DisableV1 = true`.
Signing with V2 protocol supports query string.

The example above is creating a new instance of a `HttpClient` with the handler responsible for signing the
requests and sends the request to its designation. Finally it returns the response from the remote server.
Expand All @@ -130,6 +142,8 @@ 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`. |

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

Expand All @@ -138,6 +152,9 @@ provided by the Owin and AspNetCore NuGet packages.

The setting and usage is as follows in case of OWIN (in the application's `Startup` class):

With the update of MAuth V2 protocol, the authentication will first check for mauth header with `MWSV2` and if not found then only fallback to authenticate for `MWS` version.
Also, there is options of `DisableV1`, which is `false` by default and when we are ready to disable authentication of V1 protocl i.e. `MWS` version, then we need to pass this option too as `true`.

```C#
using Medidata.MAuth.Owin;

Expand All @@ -154,6 +171,9 @@ public class Startup
options.HideExceptionsAndReturnUnauthorized = true;
options.PrivateKey = "ServerPrivateKey.pem";
options.Bypass = (request) => request.Uri.AbsolutePath.StartsWith("/allowed");

// when ready to disable authentication of V1 protococl
options.DisableV1 = true;
});
}
}
Expand All @@ -177,6 +197,9 @@ public class Startup
options.HideExceptionsAndReturnUnauthorized = true;
options.PrivateKey = "ServerPrivateKey.pem";
options.Bypass = (request) => request.Uri.AbsolutePath.StartsWith("/allowed");

// when ready to disable authentication of V1 protococl
options.DisableV1 = true;
});
}
}
Expand All @@ -193,6 +216,7 @@ The middlewares take an `MAuthMiddlewareOptions` instance to set up the authenti
| **MAuthServiceRetryPolicy** | The policy for the retry attempts when communicating with the MAuth service. The following policies can be used: `NoRetry` (no retries), `RetryOnce` (one additional attempt), `RetryTwice` (two additional attempts) and `Agressive` (9 additional attempts) - the default value is **RetryOnce**. |
| **HideExceptionsAndReturnUnauthorized** | An optional parameter that determines if the middleware should swallow all exceptions and return an empty HTTP response with a status code Unauthorized (401) in case of any errors (including authentication and validation errors). The default is **true**. |
| **Bypass** | Determines a function which evaluates if a given request should bypass the MAuth authentication. |
| **DisableV1** | Determines the boolean value which controls whether to disable the signing requests with `MAuthVersion.MWS` requests or not. If not supplied, this default value is `false`. |

The **HideExceptionsAndReturnUnauthorized** parameter is useful (if set to **false**) when you have an exception handler
mechanism (for example a logger) in your middleware pipeline. In this case the MAuth middleware won't swallow the
Expand All @@ -209,6 +233,8 @@ should produce **true** as a result, if the given request satisfies the conditio
otherwise it should result **false** therefore an authentication attempt will occur. If no Bypass predicate provided
in the options, every request will be authenticated by default.

When authentication of V1 requests needs to be disabled, then **DisableV1** should be passed as **true**.

### Authenticating Incoming Requests with the WebApi Message Handler

If your application does not use the OWIN or ASP.NET Core middleware infrastructure, but it uses the ASP.NET WebAPI
Expand All @@ -232,7 +258,10 @@ public static class WebApiConfig
AuthenticateRequestTimeoutSeconds = 3,
MAuthServiceRetryPolicy = MAuthServiceRetryPolicy.RetryOnce,
HideExceptionsAndReturnUnauthorized = true,
PrivateKey = "ServerPrivateKey.pem"
PrivateKey = "ServerPrivateKey.pem",

// when ready to disable authentication of V1 protococl
options.DisableV1 = true
};

config.MessageHandlers.Add(new MAuthAuthenticatingHandler(options));
Expand Down Expand Up @@ -273,21 +302,38 @@ The framework is licensed under the [MIT licensing terms](https://github.com/mds

##### What is the current target .NET Framework version?

The current target is **.NET Framework 4.5.2** - this means that you have to use at least this target framework version
The current target is **.NET Framework 4.6.1** - this means that you have to use at least this target framework version
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 1.4** and onwards; additionally we support the **ASP.NET Core App 1.1** and onwards with a middleware
the **.NET Standard 2.0** and onwards; additionally we support the **ASP.NET Core App 2.0** and onwards with a middleware
for authenticating the incoming requests.

##### What Cryptographic provider is used for the encryption/decryption?

In the latest version of 4.0.0, we are using the available dotnet security [System.Security.Cryptography] which works
for both **.NET Framework 4.6.1** and **.NET Standard 2.0** in case of V2 protocol. However, for the continue support
of V1 protcol, we are still maintaining the BouncyCastle library as mentioned below.

On the .NET Framework side (WebAPI, Owin, Core) we are using the latest version (as of date 1.81) of the
[BouncyCastle](https://github.com/bcgit/bc-csharp) library; on the .NET Standard side (Core, AspNetCore) we are using
the portable fork of the [BouncyCastle](https://github.com/onovotny/BouncyCastle-PCL) library.

##### What are the major changes in the 4.0.0 version?

In this version we have added support for V2 protocol which uses `MCC-Authentication` as MAuthHeader and `MCC-Time` as
MAuthTimeHeader. And, this V2 protocol supports for signing and authenticating url with query string parameters.
For Signing, we added two new options in `MAuthSigningOptions`: `MAuthVersion` which is mandatory and takes enumeration
value of `MAuthVersion.MWSV2` for V2 protocol or `MAuthVersion.MWS` for continue of using V1 protocol.
Another option `DisableV1` is `false` by default if not provided. But, it is needed to provide as `true` when the client
need to sign on by no more supporting V1 protocol.

Also while authentication, the logic defaults to check for V2 protcol header `MWSV2` and if fails then only fallback to
check for V1 protocol header for `MWS`. Also, `MAuthOptionsBase` includes new option as `DisableV1` which is `false` by
default and need to be passed as `true` if the authenticating client no longer wants to support V1 protocol.

##### What are the major changes in the 2.0.0 version?

In this version we have only one major and a minor change: from this version the `MAuthSigningHandler` is accepting an
Expand All @@ -314,5 +360,3 @@ This policy will make the number of requests to the MAuth service to an overall
to receive a successful response from the MAuth service is gradually decreasing by the number of attempts (the more
the clients are sending requests to a presumably overloaded server the less the chance for a successful response) -
therefore we do not recommend to use this policy in any production scenario.


2 changes: 1 addition & 1 deletion build/build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Write-Host "Running unit tests..." -ForegroundColor Cyan

Push-Location -Path .\tests\Medidata.MAuth.Tests

dotnet xunit
dotnet test

Pop-Location

Expand Down
1 change: 1 addition & 0 deletions src/Medidata.MAuth.AspNetCore/MAuthAppBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public static IApplicationBuilder UseMAuthAuthentication(this IApplicationBuilde
if (options == null)
throw new ArgumentNullException(nameof(options));


return app.UseMiddleware<MAuthMiddleware>(options);
}

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>netcoreapp1.1</TargetFramework>
<TargetFramework>netcoreapp2.0</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 Down
19 changes: 16 additions & 3 deletions src/Medidata.MAuth.Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,32 @@ internal static class Constants
"(?:[0-9a-zA-Z+/]{4})*" +
"(?:[0-9a-zA-Z+/]{2}==|[0-9a-zA-Z+/]{3}=)" +
"?" +
")$"
")$", RegexOptions.Compiled
);

public static readonly string MAuthHeaderKey = "X-MWS-Authentication";

public static readonly string MAuthTimeHeaderKey = "X-MWS-Time";

public static readonly string MAuthTokenRequestPath = "/mauth/v1/security_tokens/";

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");

public static readonly Regex AuthenticationHeaderRegexV2 = new Regex(
"^MWSV2 " +
"(?<uuid>[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})" +
":" +
"(?<payload>" +
"(?:[0-9a-zA-Z+/]{4})*" +
"(?:[0-9a-zA-Z+/]{2}==|[0-9a-zA-Z+/]{3}=)" +
"?" +
");$", RegexOptions.Compiled
);

public static readonly string MAuthHeaderKeyV2 = "MCC-Authentication";

public static readonly string MAuthTimeHeaderKeyV2 = "MCC-Time";
}
}
24 changes: 24 additions & 0 deletions src/Medidata.MAuth.Core/Exceptions/InvalidVersionException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;

namespace Medidata.MAuth.Core.Exceptions
{
/// <summary>
/// The exception that is thrown when version no longer allowed is passed.
/// </summary>
public class InvalidVersionException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="InvalidVersionException"/> class with the specified message.
/// </summary>
/// <param name="message">A message that describes the invalid version failure.</param>
public InvalidVersionException(string message) : base(message) { }

/// <summary>
/// Initializes a new instance of the <see cref="InvalidVersionException"/> class with the specified message
/// and inner exception.
/// </summary>
/// <param name="message">A message that describes the invalid version failure.</param>
/// <param name="innerException">An exception that is the cause of the current exception.</param>
public InvalidVersionException(string message, Exception innerException) : base(message, innerException) { }
}
}
4 changes: 4 additions & 0 deletions src/Medidata.MAuth.Core/IMAuthCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,9 @@ internal interface IMAuthCore
bool Verify(byte[] signedData, byte[] signature, string publicKey);

Task<byte[]> GetSignature(HttpRequestMessage request, AuthenticationInfo authInfo);

string GetMAuthTokenRequestPath();

(string mAuthHeaderKey, string mAuthTimeHeaderKey) GetHeaderKeys();
}
}
49 changes: 35 additions & 14 deletions src/Medidata.MAuth.Core/MAuthAuthenticator.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Medidata.MAuth.Core.Exceptions;
using Microsoft.Extensions.Caching.Memory;
using Org.BouncyCastle.Crypto;
using Medidata.MAuth.Core.Models;

namespace Medidata.MAuth.Core
{
internal class MAuthAuthenticator
{
private readonly MAuthOptionsBase options;
private readonly MAuthRequestRetrier retrier;
private readonly IMemoryCache cache = new MemoryCache(new MemoryCacheOptions());
private IMAuthCore mAuthCore;

public Guid ApplicationUuid => options.ApplicationUuid;

Expand All @@ -27,18 +27,28 @@ public MAuthAuthenticator(MAuthOptionsBase options)
throw new ArgumentNullException(nameof(options.PrivateKey));

this.options = options;
retrier = new MAuthRequestRetrier(options);
}

/// <summary>
/// Verifies if the <see cref="HttpRequestMessage"/> request is authenticated or not.
/// </summary>
/// <param name="request">The <see cref="HttpRequestMessage"/> request. </param>
/// <returns>A task object of the boolean value that verifies if the request is authenticated or not.</returns>
public async Task<bool> AuthenticateRequest(HttpRequestMessage request)
{
try
{
mAuthCore = MAuthCoreFactory.Instantiate();
var authInfo = GetAuthenticationInfo(request);
var appInfo = await GetApplicationInfo(authInfo.ApplicationUuid);
var version = request.GetAuthHeaderValue().GetVersionFromAuthenticationHeader();

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);

return mAuthCore.Verify(authInfo.Payload, await mAuthCore.GetSignature(request, authInfo), appInfo.PublicKey);
return mAuthCore.Verify(authInfo.Payload, await mAuthCore.GetSignature(request, authInfo),
appInfo.PublicKey);
}
catch (ArgumentException ex)
{
Expand All @@ -51,9 +61,14 @@ public async Task<bool> AuthenticateRequest(HttpRequestMessage request)
}
catch (InvalidCipherTextException ex)
{

throw new AuthenticationException(
"The request verification failed due to an invalid payload information.", ex);
}
catch (InvalidVersionException ex)
{
throw new InvalidVersionException(ex.Message, ex);
}
catch (Exception ex)
{
throw new AuthenticationException(
Expand All @@ -63,12 +78,15 @@ public async Task<bool> AuthenticateRequest(HttpRequestMessage request)
}
}

private Task<ApplicationInfo> GetApplicationInfo(Guid applicationUuid) =>
private Task<ApplicationInfo> GetApplicationInfo(Guid applicationUuid, MAuthVersion version) =>
cache.GetOrCreateAsync(applicationUuid, async entry =>
{
var mAuthCore = MAuthCoreFactory.Instantiate(version);
var tokenRequestPath = mAuthCore.GetMAuthTokenRequestPath();
var retrier = new MAuthRequestRetrier(options, version);
var response = await retrier.GetSuccessfulResponse(
applicationUuid,
CreateRequest,
CreateRequest, tokenRequestPath,
requestAttempts: (int)options.MAuthServiceRetryPolicy + 1
);

Expand All @@ -82,23 +100,26 @@ private Task<ApplicationInfo> GetApplicationInfo(Guid applicationUuid) =>
return result;
});

private HttpRequestMessage CreateRequest(Guid applicationUuid) =>
private HttpRequestMessage CreateRequest(Guid applicationUuid, string tokenRequestPath) =>
new HttpRequestMessage(HttpMethod.Get, new Uri(options.MAuthServiceUrl,
$"{Constants.MAuthTokenRequestPath}{applicationUuid.ToHyphenString()}.json"));
$"{tokenRequestPath}{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>
/// <returns>The authentication information with the payload from the request.</returns>
internal PayloadAuthenticationInfo GetAuthenticationInfo(HttpRequestMessage request)
internal PayloadAuthenticationInfo GetAuthenticationInfo(HttpRequestMessage request, MAuthVersion version)
{
var authHeader = request.Headers.GetFirstValueOrDefault<string>(Constants.MAuthHeaderKey);
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>(Constants.MAuthTimeHeaderKey);
var signedTime = request.Headers.GetFirstValueOrDefault<long>(headerKeys.mAuthTimeHeaderKey);

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

0 comments on commit 437265f

Please sign in to comment.