Skip to content

Commit 843701f

Browse files
author
Herry Kurniawan
authored
Merge pull request #66 from mdsol/develop
Release of v5.0.0
2 parents 512544b + 50dfbd9 commit 843701f

33 files changed

+686
-159
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changes in Medidata.MAuth
22

3+
## v5.0.0
4+
- **[Core]** Added normalization of Uri AbsolutePath.
5+
- **[Core]** Added unescape step in query_string encoding to remove `double encoding`.
6+
- **[Core]** Replace `DisableV1`option with `SignVersions` option and change the default signing to `MAuthVersion.MWS` only.
7+
- **[Core]** Added parsing code to test with mauth-protocol-test-suite.
8+
- **[Core]** Fixed bug in sorting of query parameters.
9+
310
## v4.0.2
411
- **[AspNetCore]** Update aspnetcore version to aspnetcore2.1 LTS.
512
- **[Core]** Fallback to V1 protocol when V2 athentication fails.

CONTRIBUTING.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Contributing
2+
3+
## General Information
4+
* Clone this repo in your workspace. Checkout latest `develop` branch.
5+
* Make new changes or updates into `feature/bugfix` branch.
6+
* Make sure to add unit tests for it so that there is no breaking changes.
7+
* Commit and push your branch to compare and create PR against latest `develop` branch.
8+
9+
## Running Tests
10+
To run tests, go the folder `mauth-client-dotnet\tests\Medidata.MAuth.Tests`
11+
Next, run the tests as:
12+
13+
```
14+
dotnet test --filter "Category!=ProtocolTestSuite"
15+
```
16+
17+
## Running mauth-protocol-test-suite
18+
To run the mauth-protocol-test-suite clone the latest suite onto your machine and place it in the same parent directory as this repo (or supply the ENV var
19+
`TEST_SUITE_PATH` with the path to the test suite relative to this repo).
20+
Then navigate to :`mauth-client-dotnet\tests\Medidata.MAuth.Tests`
21+
And, run the tests as:
22+
23+
```
24+
dotnet test --filter "Category=ProtocolTestSuite"
25+
```

README.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,8 @@ public async Task<HttpResponseMessage> SignAndSendRequest(HttpRequestMessage req
114114
// The following can be either a path to the key file or the contents of the file itself
115115
PrivateKey = "ClientPrivateKey.pem",
116116

117-
// when ready to disable authentication of V1 protocol else default is false
118-
// signs with both V1 and V2.
119-
DisableV1 = true
117+
// Enumerations of signing protocols, if not provided defaults to `MAuthVersion.MWS`for sign-in.
118+
SignVersions = MAuthVersion.MWS | MAuthVersion.MWSV2
120119
});
121120

122121
using (var client = new HttpClient(signingHandler))
@@ -125,10 +124,12 @@ public async Task<HttpResponseMessage> SignAndSendRequest(HttpRequestMessage req
125124
}
126125
}
127126
```
128-
With the release of support for MAuth V2 protocol, by default MAuth request signs with both V1 and V2 protocol.
129-
Also by default, `DisableV1` option is set to false (if not included). When we are ready to
130-
disable all the V1 request, then we need to include this disable option as : `DisableV1 = true`.
131-
Signing with V2 protocol supports query string.
127+
The `SignVersions` parameter can be used to specify which protocol version to sign outgoing requests. Like as:
128+
`SignVersions = MAuthVersion.MWS`: signs with `MWS` protocol only.
129+
`SignVersions = MAuthVersion.MWS | MAuthVersion.MWSV2` : signs with both `MWS` and `MWSV2` protocol.
130+
If not supplied, it sign by `MWS` protocol by default.
131+
132+
Signing with `MWSV2` protocol supports query string.
132133

133134
The example above is creating a new instance of a `HttpClient` with the handler responsible for signing the
134135
requests and sends the request to its designation. Finally it returns the response from the remote server.
@@ -139,7 +140,7 @@ The `MAuthSigningOptions` has the following properties to determine the required
139140
| ---- | ----------- |
140141
| **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. |
141142
| **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. |
142-
| **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`. |
143+
| **SignVersions** | (optional) Enumerations of MAuth protocol versions to sign requests. If not supplied, defaults to `MWS`.
143144

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

@@ -330,6 +331,11 @@ On the .NET Framework side (WebAPI, Owin, Core) we are using the latest version
330331
[BouncyCastle](https://github.com/bcgit/bc-csharp) library; on the .NET Standard side (Core, AspNetCore) we are using
331332
the portable fork of the [BouncyCastle](https://github.com/onovotny/BouncyCastle-PCL) library.
332333

334+
##### What are the major changes in the 5.0.0 version?
335+
In this version we have removed the property `DisableV1` from `MAuthSigningOptions`. Instead, we have added new option as
336+
`SignVersions` in `MAuthSigningOptions` which takes enumeration values of MAuth protcol versions `MWS` and/ or `MWSV2` protocol.
337+
If this option is not provided, then it will sign in by `MWS` protocol as default.
338+
333339
##### What are the major changes in the 4.0.0 version?
334340

335341
In this version we have added support for V2 protocol which uses `MCC-Authentication` as MAuthHeader and `MCC-Time` as

build/build.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Write-Host "Running unit tests..." -ForegroundColor Cyan
6767

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

70-
dotnet test
70+
dotnet test --filter "Category!=ProtocolTestSuite"
7171

7272
Pop-Location
7373

src/Medidata.MAuth.AspNetCore/MAuthMiddleware.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ namespace Medidata.MAuth.AspNetCore
1212
/// </summary>
1313
internal class MAuthMiddleware
1414
{
15-
private readonly MAuthMiddlewareOptions options;
16-
private readonly MAuthAuthenticator authenticator;
17-
private readonly RequestDelegate next;
15+
private readonly MAuthMiddlewareOptions _options;
16+
private readonly MAuthAuthenticator _authenticator;
17+
private readonly RequestDelegate _next;
1818

1919
/// <summary>
2020
/// Creates a new <see cref="MAuthMiddleware"/>
@@ -24,11 +24,11 @@ internal class MAuthMiddleware
2424
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> representing the factory that used to create logger instances.</param>
2525
public MAuthMiddleware(RequestDelegate next, MAuthMiddlewareOptions options, ILoggerFactory loggerFactory)
2626
{
27-
this.next = next;
28-
this.options = options;
27+
_next = next;
28+
_options = options;
2929
loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
3030
ILogger logger = loggerFactory.CreateLogger<MAuthMiddleware>();
31-
this.authenticator = new MAuthAuthenticator(options, logger);
31+
_authenticator = new MAuthAuthenticator(options, logger);
3232
}
3333

3434
/// <summary>
@@ -40,16 +40,16 @@ public async Task Invoke(HttpContext context)
4040
{
4141
context.Request.EnableBuffering();
4242

43-
if (!options.Bypass(context.Request) &&
44-
!await context.TryAuthenticate(authenticator, options.HideExceptionsAndReturnUnauthorized).ConfigureAwait(false))
43+
if (!_options.Bypass(context.Request) &&
44+
!await context.TryAuthenticate(_authenticator, _options.HideExceptionsAndReturnUnauthorized).ConfigureAwait(false))
4545
{
4646
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
4747
return;
4848
}
4949

5050
context.Request.Body.Rewind();
5151

52-
await next.Invoke(context).ConfigureAwait(false);
52+
await _next.Invoke(context).ConfigureAwait(false);
5353
}
5454
}
5555
}

src/Medidata.MAuth.Core/Constants.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,9 @@ internal static class Constants
4545
public static readonly string MAuthTimeHeaderKeyV2 = "MCC-Time";
4646

4747
public static readonly string MAuthTokenRequestPath = "/mauth/v1/security_tokens/";
48+
49+
public static readonly Regex LowerCaseHexPattern = new Regex("%[a-f0-9]{2}", RegexOptions.Compiled);
50+
51+
public static readonly Regex SlashPattern = new Regex("//+", RegexOptions.Compiled);
4852
}
4953
}

src/Medidata.MAuth.Core/MAuthAuthenticator.cs

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ namespace Medidata.MAuth.Core
1212
{
1313
internal class MAuthAuthenticator
1414
{
15-
private readonly MAuthOptionsBase options;
16-
private readonly IMemoryCache cache = new MemoryCache(new MemoryCacheOptions());
17-
private readonly ILogger logger;
15+
private readonly MAuthOptionsBase _options;
16+
private readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions());
17+
private readonly ILogger _logger;
1818

19-
public Guid ApplicationUuid => options.ApplicationUuid;
19+
public Guid ApplicationUuid => _options.ApplicationUuid;
2020

2121
public MAuthAuthenticator(MAuthOptionsBase options, ILogger logger)
2222
{
@@ -29,8 +29,8 @@ public MAuthAuthenticator(MAuthOptionsBase options, ILogger logger)
2929
if (string.IsNullOrWhiteSpace(options.PrivateKey))
3030
throw new ArgumentNullException(nameof(options.PrivateKey));
3131

32-
this.options = options;
33-
this.logger = logger;
32+
_options = options;
33+
_logger = logger;
3434
}
3535

3636
/// <summary>
@@ -42,58 +42,61 @@ public async Task<bool> AuthenticateRequest(HttpRequestMessage request)
4242
{
4343
try
4444
{
45-
logger.LogInformation("Initiating Authentication of the request.");
46-
var version = request.GetAuthHeaderValue().GetVersionFromAuthenticationHeader();
45+
_logger.LogInformation("Initiating Authentication of the request.");
4746

48-
if (options.DisableV1 && version == MAuthVersion.MWS)
47+
var authHeader = request.GetAuthHeaderValue();
48+
var version = authHeader.GetVersionFromAuthenticationHeader();
49+
var parsedHeader = authHeader.ParseAuthenticationHeader();
50+
51+
if (_options.DisableV1 && version == MAuthVersion.MWS)
4952
throw new InvalidVersionException($"Authentication with {version} version is disabled.");
5053

51-
var authenticated = await Authenticate(request, version).ConfigureAwait(false);
52-
if (!authenticated && version == MAuthVersion.MWSV2 && !options.DisableV1)
54+
var authenticated = await Authenticate(request, version, parsedHeader.Uuid).ConfigureAwait(false);
55+
if (!authenticated && version == MAuthVersion.MWSV2 && !_options.DisableV1)
5356
{
5457
// fall back to V1 authentication
55-
authenticated = await Authenticate(request, MAuthVersion.MWS).ConfigureAwait(false);
56-
logger.LogWarning("Completed successful authentication attempt after fallback to V1");
58+
authenticated = await Authenticate(request, MAuthVersion.MWS, parsedHeader.Uuid).ConfigureAwait(false);
59+
_logger.LogWarning("Completed successful authentication attempt after fallback to V1");
5760
}
5861
return authenticated;
5962
}
6063
catch (ArgumentException ex)
6164
{
62-
logger.LogError(ex, "Unable to authenticate due to invalid MAuth authentication headers.");
65+
_logger.LogError(ex, "Unable to authenticate due to invalid MAuth authentication headers.");
6366
throw new AuthenticationException("The request has invalid MAuth authentication headers.", ex);
6467
}
6568
catch (RetriedRequestException ex)
6669
{
67-
logger.LogError(ex, "Unable to query the application information from MAuth server.");
70+
_logger.LogError(ex, "Unable to query the application information from MAuth server.");
6871
throw new AuthenticationException(
6972
"Could not query the application information for the application from the MAuth server.", ex);
7073
}
7174
catch (InvalidCipherTextException ex)
7275
{
73-
logger.LogWarning(ex, "Unable to authenticate due to invalid payload information.");
76+
_logger.LogWarning(ex, "Unable to authenticate due to invalid payload information.");
7477
throw new AuthenticationException(
7578
"The request verification failed due to an invalid payload information.", ex);
7679
}
7780
catch (InvalidVersionException ex)
7881
{
79-
logger.LogError(ex, "Unable to authenticate due to invalid version.");
82+
_logger.LogError(ex, "Unable to authenticate due to invalid version.");
8083
throw new InvalidVersionException(ex.Message, ex);
8184
}
8285
catch (Exception ex)
8386
{
84-
logger.LogError(ex, "Unable to authenticate due to unexpected error.");
87+
_logger.LogError(ex, "Unable to authenticate due to unexpected error.");
8588
throw new AuthenticationException(
8689
"An unexpected error occured during authentication. Please see the inner exception for details.",
8790
ex
8891
);
8992
}
9093
}
9194

92-
private async Task<bool> Authenticate(HttpRequestMessage request, MAuthVersion version)
95+
private async Task<bool> Authenticate(HttpRequestMessage request, MAuthVersion version, Guid signedAppUuid)
9396
{
94-
var logMessage = "Mauth-client attempting to authenticate request from app with mauth app uuid" +
95-
$" {options.ApplicationUuid} using version {version}";
96-
logger.LogInformation(logMessage);
97+
var logMessage = "Mauth-client attempting to authenticate request from app with mauth app uuid " +
98+
$"{signedAppUuid} to app with mauth app uuid {_options.ApplicationUuid} using version {version}";
99+
_logger.LogInformation(logMessage);
97100

98101
var mAuthCore = MAuthCoreFactory.Instantiate(version);
99102
var authInfo = GetAuthenticationInfo(request, mAuthCore);
@@ -104,13 +107,13 @@ private async Task<bool> Authenticate(HttpRequestMessage request, MAuthVersion v
104107
}
105108

106109
private Task<ApplicationInfo> GetApplicationInfo(Guid applicationUuid) =>
107-
cache.GetOrCreateAsync(applicationUuid, async entry =>
110+
_cache.GetOrCreateAsync(applicationUuid, async entry =>
108111
{
109-
var retrier = new MAuthRequestRetrier(options);
112+
var retrier = new MAuthRequestRetrier(_options);
110113
var response = await retrier.GetSuccessfulResponse(
111114
applicationUuid,
112115
CreateRequest,
113-
requestAttempts: (int)options.MAuthServiceRetryPolicy + 1
116+
requestAttempts: (int)_options.MAuthServiceRetryPolicy + 1
114117
).ConfigureAwait(false);
115118

116119
var result = await response.Content.FromResponse().ConfigureAwait(false);
@@ -157,7 +160,7 @@ internal static PayloadAuthenticationInfo GetAuthenticationInfo(HttpRequestMessa
157160
}
158161

159162
private HttpRequestMessage CreateRequest(Guid applicationUuid) =>
160-
new HttpRequestMessage(HttpMethod.Get, new Uri(options.MAuthServiceUrl,
163+
new HttpRequestMessage(HttpMethod.Get, new Uri(_options.MAuthServiceUrl,
161164
$"{Constants.MAuthTokenRequestPath}{applicationUuid.ToHyphenString()}.json"));
162165
}
163166
}

src/Medidata.MAuth.Core/MAuthCoreExtensions.cs

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -184,17 +184,53 @@ public static byte[] Concat(this byte[][] values)
184184
/// <returns>EncodedQueryParameter string.</returns>
185185
public static string BuildEncodedQueryParams(this string queryString)
186186
{
187-
var encodedQueryStrings = new List<string>();
187+
if (string.IsNullOrEmpty(queryString))
188+
return string.Empty;
189+
188190
var queryArray = queryString.Split('&');
189-
Array.Sort(queryArray, StringComparer.Ordinal);
190-
Array.ForEach(queryArray, x =>
191+
var unescapedKeysAndValues = new KeyValuePair<string, string>[queryArray.Length];
192+
193+
// unescaping
194+
for (int i = 0; i < queryArray.Length; i++)
191195
{
192-
var keyValue = x.Split('=');
193-
var escapedKey = Uri.EscapeDataString(keyValue[0]);
194-
var escapedValue = Uri.EscapeDataString(keyValue[1]);
195-
encodedQueryStrings.Add($"{escapedKey}={escapedValue}");
196-
});
197-
return string.Join("&", encodedQueryStrings);
196+
var keyValue = queryArray[i].Split('=');
197+
unescapedKeysAndValues[i] = new KeyValuePair<string, string>(
198+
Uri.UnescapeDataString(keyValue[0]), Uri.UnescapeDataString(keyValue[1]));
199+
}
200+
201+
// sorting and escaping
202+
var escapedKeyValues = unescapedKeysAndValues
203+
.OrderBy(kv => kv.Key, StringComparer.Ordinal)
204+
.ThenBy(kv => kv.Value, StringComparer.Ordinal)
205+
.Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}");
206+
207+
// Above encoding converts space as `%20` and `+` as `%2B`
208+
// But space and `+` both needs to be converted as `%20` as per
209+
// reference https://github.com/mdsol/mauth-client-ruby/blob/v6.0.0/lib/mauth/request_and_response.rb#L113
210+
// so this convert `%2B` into `%20` to match encodedqueryparams to that of other languages.
211+
return string.Join("&", escapedKeyValues).Replace("%2B", "%20");
212+
}
213+
214+
/// <summary>
215+
/// Normalizes the UriPath
216+
/// </summary>
217+
/// <param name="path"></param>
218+
/// <returns>Normalized Uri Resource Path</returns>
219+
public static string NormalizeUriPath(this string path)
220+
{
221+
if (string.IsNullOrEmpty(path))
222+
return string.Empty;
223+
224+
// Normalize percent encoding to uppercase i.e. %cf%80 => %CF%80
225+
var matches = Constants.LowerCaseHexPattern.Matches(path);
226+
var normalizedPath = new StringBuilder(path);
227+
foreach(var item in matches)
228+
{
229+
normalizedPath.Replace(item.ToString(), item.ToString().ToUpper());
230+
}
231+
232+
// Replaces multiple slashes into single "/"
233+
return Constants.SlashPattern.Replace(normalizedPath.ToString(), "/");
198234
}
199235
}
200236
}

src/Medidata.MAuth.Core/MAuthCoreV2.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,18 @@ public bool Verify(byte[] signedData, byte[] signature, string publicKey)
5959
public async Task<byte[]> GetSignature(HttpRequestMessage request, AuthenticationInfo authInfo)
6060
{
6161
var encodedHttpVerb = request.Method.Method.ToBytes();
62-
var encodedResourceUriPath = request.RequestUri.AbsolutePath.ToBytes();
62+
var encodedResourceUriPath = request.RequestUri.AbsolutePath.NormalizeUriPath().ToBytes();
6363
var encodedAppUUid = authInfo.ApplicationUuid.ToHyphenString().ToBytes();
6464

6565
var requestBody = request.Content != null ?
6666
await request.Content.ReadAsByteArrayAsync().ConfigureAwait(false) : new byte[] { };
6767
var requestBodyDigest = requestBody.AsSHA512Hash();
6868

6969
var encodedCurrentSecondsSinceEpoch = authInfo.SignedTime.ToUnixTimeSeconds().ToString().ToBytes();
70-
var encodedQueryParams = !string.IsNullOrEmpty(request.RequestUri.Query) ?
71-
request.RequestUri.Query.Replace("?", "").BuildEncodedQueryParams().ToBytes() : new byte[] { };
70+
var queryString = request.RequestUri.Query;
71+
var encodedQueryParams = !string.IsNullOrEmpty(queryString)
72+
? queryString.Substring(1).BuildEncodedQueryParams().ToBytes()
73+
: new byte[] { };
7274

7375
return new byte[][]
7476
{

0 commit comments

Comments
 (0)